From f80594a89a0b14485cc67bf74d2e6387de4c1659 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Thu, 22 Jan 2026 22:40:40 -0800 Subject: [PATCH 1/9] fix(cli-hooks): stop app process if start hook is exited --- packages/cli-hooks/src/start.js | 11 ++++++ packages/cli-hooks/src/start.spec.js | 59 ++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/packages/cli-hooks/src/start.js b/packages/cli-hooks/src/start.js index 0690b8698..3d882891c 100755 --- a/packages/cli-hooks/src/start.js +++ b/packages/cli-hooks/src/start.js @@ -35,6 +35,17 @@ export default function start(cwd) { app.on('close', (code) => { process.exit(code); }); + process.on('exit', () => { + app.kill(); + }); + process.on('SIGINT', () => { + app.kill('SIGINT'); + process.exit(); + }); + process.on('SIGTERM', () => { + app.kill('SIGTERM'); + process.exit(); + }); } /** diff --git a/packages/cli-hooks/src/start.spec.js b/packages/cli-hooks/src/start.spec.js index a6dfafc1f..f60c5067e 100755 --- a/packages/cli-hooks/src/start.spec.js +++ b/packages/cli-hooks/src/start.spec.js @@ -18,6 +18,7 @@ import start from './start.js'; * @property {MockStreams} stdout - Output logged to standard output streams. * @property {MockStreams} stderr - Output logged to standard error streams. * @property {sinon.SinonStub} on - A fallback event to mock the spawn closure. + * @property {sinon.SinonStub} [kill] - Forced exit code of a spawned process. */ describe('start implementation', async () => { @@ -41,6 +42,7 @@ describe('start implementation', async () => { stdout: { on: sinon.stub(), setEncoding: () => {} }, stderr: { on: sinon.stub() }, on: sinon.stub(), + kill: sinon.stub(), }; spawnStub = sinon.stub(childProcess, 'spawn').returns(/** @type {any} */ (mockSpawnProcess)); process.env.SLACK_CLI_XAPP = 'xapp-example'; @@ -126,4 +128,61 @@ describe('start implementation', async () => { }); }); }); + + describe('stops the app process', () => { + /** @type {sinon.SinonStub} */ + let processStub; + /** @type {sinon.SinonStub} */ + let exitStub; + /** @type {MockSpawnProcess} */ + let mockSpawnProcess; + /** @type {sinon.SinonStub} */ + let spawnStub; + + beforeEach(() => { + process.env.SLACK_CLI_CUSTOM_FILE_PATH = 'app.js'; + processStub = sinon.stub(process, 'on'); + exitStub = sinon.stub(process, 'exit'); + mockSpawnProcess = { + stdout: { on: sinon.stub(), setEncoding: () => {} }, + stderr: { on: sinon.stub() }, + on: sinon.stub(), + kill: sinon.stub(), + }; + spawnStub = sinon.stub(childProcess, 'spawn').returns(/** @type {any} */ (mockSpawnProcess)); + }); + + afterEach(() => { + sinon.restore(); + process.env.SLACK_CLI_CUSTOM_FILE_PATH = undefined; + }); + + it('stops app process on hook exit', () => { + start('./'); + assert.ok(spawnStub.called); + assert.ok(spawnStub.calledWith('node', [path.resolve('app.js')])); + const handler = processStub.getCalls().find((call) => call.args[0] === 'exit')?.args[1]; + assert.ok(handler, 'exit handler should be registered'); + handler(); + assert.ok(mockSpawnProcess.kill?.called); + }); + + it('stops app process on hook SIGINT', () => { + start('./'); + const handler = processStub.getCalls().find((call) => call.args[0] === 'SIGINT')?.args[1]; + assert.ok(handler, 'SIGINT handler should be registered'); + handler(); + assert.ok(mockSpawnProcess.kill?.calledWith('SIGINT')); + assert.ok(exitStub.called); + }); + + it('stops app process on hook SIGTERM', () => { + start('./'); + const handler = processStub.getCalls().find((call) => call.args[0] === 'SIGTERM')?.args[1]; + assert.ok(handler, 'SIGTERM handler should be registered'); + handler(); + assert.ok(mockSpawnProcess.kill?.calledWith('SIGTERM')); + assert.ok(exitStub.called); + }); + }); }); From 118788ed2f6fe2e0e42863bf21157ecd113d19ba Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 26 Jan 2026 23:22:17 -0800 Subject: [PATCH 2/9] feat(cli-hooks): add default app and manifest watch config --- packages/cli-hooks/src/get-hooks.js | 26 +++++++++++++++++++++--- packages/cli-hooks/src/get-hooks.spec.js | 16 +++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/packages/cli-hooks/src/get-hooks.js b/packages/cli-hooks/src/get-hooks.js index af920a5a4..64521165b 100755 --- a/packages/cli-hooks/src/get-hooks.js +++ b/packages/cli-hooks/src/get-hooks.js @@ -32,8 +32,22 @@ if (fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) { */ /** - * Information about the files to watch for specific changes. + * Information about file changes for CLI actions. * @typedef SDKConfigWatch + * @property {AppConfigWatch} app - Watch config for server restarts. + * @property {ManifestConfigWatch} manifest - Watch config for app reinstalls. + */ + +/** + * Information about the app files to watch for server restarts. + * @typedef AppConfigWatch + * @property {string} filter-regex - Regex pattern for finding filtered files. + * @property {string[]} paths - Specific locations to begin searching for files. + */ + +/** + * Information about the manifest files to watch for app reinstalls. + * @typedef ManifestConfigWatch * @property {string} filter-regex - Regex pattern for finding filtered files. * @property {string[]} paths - Specific locations to begin searching for files. */ @@ -52,8 +66,14 @@ export default function getHooks() { }, config: { watch: { - 'filter-regex': '^manifest\\.json$', - paths: ['.'], + app: { + paths: ['.'], + 'filter-regex': '.js$', + }, + manifest: { + paths: ['manifest.json'], + 'filter-regex': '^manifest\\.json$', + }, }, 'protocol-version': SUPPORTED_NAMED_PROTOCOLS, 'sdk-managed-connection-enabled': true, diff --git a/packages/cli-hooks/src/get-hooks.spec.js b/packages/cli-hooks/src/get-hooks.spec.js index 93527c17f..0a72b8c93 100755 --- a/packages/cli-hooks/src/get-hooks.spec.js +++ b/packages/cli-hooks/src/get-hooks.spec.js @@ -21,4 +21,20 @@ describe('get-hooks implementation', async () => { const { config } = getHooks(); assert(config['sdk-managed-connection-enabled']); }); + + it('should return watch config for app files', async () => { + const { config } = getHooks(); + assert(config.watch); + assert(config.watch.app); + assert.deepEqual(config.watch.app.paths, ['.']); + assert.equal(config.watch.app['filter-regex'], '.js$'); + }); + + it('should return watch config for manifest files', async () => { + const { config } = getHooks(); + assert(config.watch); + assert(config.watch.manifest); + assert.deepEqual(config.watch.manifest.paths, ['manifest.json']); + assert.equal(config.watch.manifest['filter-regex'], '^manifest\\.json$'); + }); }); From bfa23c82f2baf616c5fcfd744952bca848779470 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Thu, 29 Jan 2026 18:52:58 -0800 Subject: [PATCH 3/9] docs: changeset --- .changeset/neat-toys-wonder.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/neat-toys-wonder.md diff --git a/.changeset/neat-toys-wonder.md b/.changeset/neat-toys-wonder.md new file mode 100644 index 000000000..0178fee9b --- /dev/null +++ b/.changeset/neat-toys-wonder.md @@ -0,0 +1,7 @@ +--- +"@slack/cli-hooks": patch +--- + +fix(cli-hooks): stop app process if the start hook exits + +Fixes a CLI [issue](https://github.com/slackapi/slack-cli/issues/128) where daemon app processes were spawned if the CLI was exited without being interrupted. From 14899a9a54fdd38ad9ceddfaa5158f066f7b8e82 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Thu, 29 Jan 2026 22:29:14 -0800 Subject: [PATCH 4/9] fix: search for explicit .js file extensions --- packages/cli-hooks/src/get-hooks.js | 4 ++-- packages/cli-hooks/src/get-hooks.spec.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cli-hooks/src/get-hooks.js b/packages/cli-hooks/src/get-hooks.js index 64521165b..35c6b68db 100755 --- a/packages/cli-hooks/src/get-hooks.js +++ b/packages/cli-hooks/src/get-hooks.js @@ -67,12 +67,12 @@ export default function getHooks() { config: { watch: { app: { + 'filter-regex': '\\.js$', paths: ['.'], - 'filter-regex': '.js$', }, manifest: { - paths: ['manifest.json'], 'filter-regex': '^manifest\\.json$', + paths: ['manifest.json'], }, }, 'protocol-version': SUPPORTED_NAMED_PROTOCOLS, diff --git a/packages/cli-hooks/src/get-hooks.spec.js b/packages/cli-hooks/src/get-hooks.spec.js index 0a72b8c93..47ec256e0 100755 --- a/packages/cli-hooks/src/get-hooks.spec.js +++ b/packages/cli-hooks/src/get-hooks.spec.js @@ -26,15 +26,15 @@ describe('get-hooks implementation', async () => { const { config } = getHooks(); assert(config.watch); assert(config.watch.app); + assert.equal(config.watch.app['filter-regex'], '\\.js$'); assert.deepEqual(config.watch.app.paths, ['.']); - assert.equal(config.watch.app['filter-regex'], '.js$'); }); it('should return watch config for manifest files', async () => { const { config } = getHooks(); assert(config.watch); assert(config.watch.manifest); - assert.deepEqual(config.watch.manifest.paths, ['manifest.json']); assert.equal(config.watch.manifest['filter-regex'], '^manifest\\.json$'); + assert.deepEqual(config.watch.manifest.paths, ['manifest.json']); }); }); From cc8e5b729000514b67e4d5c36a9178b984c07e6a Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Thu, 29 Jan 2026 22:29:34 -0800 Subject: [PATCH 5/9] docs: changeset --- .changeset/green-items-write.md | 41 +++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .changeset/green-items-write.md diff --git a/.changeset/green-items-write.md b/.changeset/green-items-write.md new file mode 100644 index 000000000..aef0a2b1a --- /dev/null +++ b/.changeset/green-items-write.md @@ -0,0 +1,41 @@ +--- +"@slack/cli-hooks": minor +--- + +feat(cli-hooks): add default app and manifest watch config + +This package now provides default watch configurations for automatic file watching during [`slack run`](https://docs.slack.dev/tools/slack-cli/reference/commands/slack_platform_run). The CLI will restart your app server when source files change and reinstall your app when the manifest changes. + +**Requirements:** These features require Slack CLI v3.12.0+ with [file watching support](https://github.com/slackapi/slack-cli/pull/310). + +### Default Configuration + +The following watch settings are provided automatically when using this package: + +```json +{ + "config": { + "watch": { + "app": { + "filter-regex": "\\.js$", + "paths": ["."] + }, + "manifest": { + "filter-regex": "^manifest\\.json$", + "paths": ["manifest.json"] + } + } + } +} +``` + +- **app**: Watches for JavaScript file changes to restart the app server +- **manifest**: Watches the manifest file for changes to reinstall the app + +### Custom Configurations + +You can override these defaults in your `.slack/hooks.json` file to reduce the paths searched or change the file patterns. Read [Watch Configurations](https://docs.slack.dev/tools/slack-cli/reference/hooks/#watch-configurations) for more options. + +### TypeScript Development + +TypeScript developers should run `tsc --watch` in a separate terminal during development. This compiles `.ts` files to `.js` on changes, and the default watch configuration will detect changes to the compiled `dist/*.js` files and restart the app server. This approach works best with the default settings. From 2a849b9e89f8672ac8110ff7e71e4ed8965a9f62 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Thu, 29 Jan 2026 22:55:13 -0800 Subject: [PATCH 6/9] fix: remove manifest.json from filter regexes --- .changeset/green-items-write.md | 1 - packages/cli-hooks/src/get-hooks.js | 1 - packages/cli-hooks/src/get-hooks.spec.js | 1 - 3 files changed, 3 deletions(-) diff --git a/.changeset/green-items-write.md b/.changeset/green-items-write.md index aef0a2b1a..fb7cb9c52 100644 --- a/.changeset/green-items-write.md +++ b/.changeset/green-items-write.md @@ -21,7 +21,6 @@ The following watch settings are provided automatically when using this package: "paths": ["."] }, "manifest": { - "filter-regex": "^manifest\\.json$", "paths": ["manifest.json"] } } diff --git a/packages/cli-hooks/src/get-hooks.js b/packages/cli-hooks/src/get-hooks.js index 35c6b68db..371b8e4cf 100755 --- a/packages/cli-hooks/src/get-hooks.js +++ b/packages/cli-hooks/src/get-hooks.js @@ -71,7 +71,6 @@ export default function getHooks() { paths: ['.'], }, manifest: { - 'filter-regex': '^manifest\\.json$', paths: ['manifest.json'], }, }, diff --git a/packages/cli-hooks/src/get-hooks.spec.js b/packages/cli-hooks/src/get-hooks.spec.js index 47ec256e0..e83038f7a 100755 --- a/packages/cli-hooks/src/get-hooks.spec.js +++ b/packages/cli-hooks/src/get-hooks.spec.js @@ -34,7 +34,6 @@ describe('get-hooks implementation', async () => { const { config } = getHooks(); assert(config.watch); assert(config.watch.manifest); - assert.equal(config.watch.manifest['filter-regex'], '^manifest\\.json$'); assert.deepEqual(config.watch.manifest.paths, ['manifest.json']); }); }); From 5fcd109e80e2bcb012c02a11f945756151dda769 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Thu, 29 Jan 2026 22:57:11 -0800 Subject: [PATCH 7/9] docs: note local manifest source requirement for reinstallations --- .changeset/green-items-write.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.changeset/green-items-write.md b/.changeset/green-items-write.md index fb7cb9c52..d38009434 100644 --- a/.changeset/green-items-write.md +++ b/.changeset/green-items-write.md @@ -31,6 +31,16 @@ The following watch settings are provided automatically when using this package: - **app**: Watches for JavaScript file changes to restart the app server - **manifest**: Watches the manifest file for changes to reinstall the app +**Note:** Manifest watching requires a local manifest source in your `.slack/config.json` file. Remote manifests will not be updated on file changes. + +```json +{ + "manifest": { + "source": "local" + } +} +``` + ### Custom Configurations You can override these defaults in your `.slack/hooks.json` file to reduce the paths searched or change the file patterns. Read [Watch Configurations](https://docs.slack.dev/tools/slack-cli/reference/hooks/#watch-configurations) for more options. From 10b695b6089f95ebf669c1135bb2a0118a32c5af Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Wed, 4 Feb 2026 15:30:03 -0800 Subject: [PATCH 8/9] docs: make optional filter regex for app configuration watch Co-authored-by: Michael Brooks --- packages/cli-hooks/src/get-hooks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli-hooks/src/get-hooks.js b/packages/cli-hooks/src/get-hooks.js index 371b8e4cf..330deeec5 100755 --- a/packages/cli-hooks/src/get-hooks.js +++ b/packages/cli-hooks/src/get-hooks.js @@ -41,7 +41,7 @@ if (fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) { /** * Information about the app files to watch for server restarts. * @typedef AppConfigWatch - * @property {string} filter-regex - Regex pattern for finding filtered files. + * @property {string} [filter-regex] - Regex pattern for finding filtered files. * @property {string[]} paths - Specific locations to begin searching for files. */ From 7c7927d7ace58ac575d6235c1df6487f04eba3c6 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Wed, 4 Feb 2026 15:30:14 -0800 Subject: [PATCH 9/9] docs: make optional filter regex for app configuration watch Co-authored-by: Michael Brooks --- packages/cli-hooks/src/get-hooks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli-hooks/src/get-hooks.js b/packages/cli-hooks/src/get-hooks.js index 330deeec5..e8fe02c1d 100755 --- a/packages/cli-hooks/src/get-hooks.js +++ b/packages/cli-hooks/src/get-hooks.js @@ -48,7 +48,7 @@ if (fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) { /** * Information about the manifest files to watch for app reinstalls. * @typedef ManifestConfigWatch - * @property {string} filter-regex - Regex pattern for finding filtered files. + * @property {string} [filter-regex] - Regex pattern for finding filtered files. * @property {string[]} paths - Specific locations to begin searching for files. */