Skip to content

Conversation

@zimeg
Copy link
Member

@zimeg zimeg commented Jan 27, 2026

Changelog

The slack run command supports file watching and live reloading Bolt for JavaScript and Bolt for Python projects. When a file is changed, the Slack CLI will automatically restart the app development server. When the manifest.json file is changed then the manifest update API is called (.slack/config.json must have manifest.source: "local").

Summary

This PR restarts the app development server on file changes when using the slack run command. An update to the hooks file adds options for this:

{
  "config": {
    "watch": {
      "manifest": {
        "paths": ["manifest.json"]
      },
      "app": {
        "paths": ["app.js", "listeners/"],
        "filter-regex": "\\.(ts|js)$"
      }
    }
  }
}

Preview

reload.mov

Reviewers

The changes of this PR can be built with update hook defaults of slackapi/node-slack-sdk#2480 👾

$ npm pack                # Package the CLI hooks
$ slack create asdf -t slack-samples/bolt-js-starter-template
$ cd asdf
$ npm install /path/to/cli-hooks/slack-cli-hooks-1.2.1.tgz
$ vim .slack/config.json  # Set manifest source to be "local"
$ slack run
$ vim manifest.json       # Update the manifest to reinstall 
$ vim app.js              # Update the app to restart server

Notes

  • This change remains backward compatible with top-level filter-regex and paths for manifest updates. A follow up PR might add warnings for upcoming deprecation?

Requirements

@zimeg zimeg self-assigned this Jan 27, 2026
@zimeg zimeg requested review from a team as code owners January 27, 2026 07:39
@zimeg zimeg added docs M-T: Documentation work only enhancement M-T: A feature request for new functionality semver:minor Use on pull requests to describe the release version increment labels Jan 27, 2026
@codecov
Copy link

codecov bot commented Jan 27, 2026

Codecov Report

❌ Patch coverage is 19.58042% with 115 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.64%. Comparing base (7ef1ac9) to head (5c176af).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
internal/pkg/platform/localserver.go 0.00% 101 Missing ⚠️
cmd/platform/run.go 0.00% 7 Missing ⚠️
internal/hooks/shell.go 0.00% 4 Missing ⚠️
internal/pkg/platform/run.go 0.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #310      +/-   ##
==========================================
- Coverage   64.93%   64.64%   -0.29%     
==========================================
  Files         212      212              
  Lines       17623    17750     +127     
==========================================
+ Hits        11443    11474      +31     
- Misses       5104     5202      +98     
+ Partials     1076     1074       -2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

@srtaalej srtaalej left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌟 very exciting changes! I got it working for app.js and /listeners but I'm having trouble getting a response from changes to manifest.json. might there be any other necessary config to get it listening to manifest?

App change detected: []/bolt-js-assistant-template/app.js, restarting server...
[DEBUG]  web-api:WebClient:0 initialized
[DEBUG]  bolt-app Initializing SocketModeReceiver
[DEBUG]  web-api:WebClient:0 initialized
[DEBUG]  socket-mode:SocketModeClient:0 The Socket Mode client has successfully initialized
[DEBUG]  web-api:WebClient:0 apiCall('auth.test') start
[DEBUG]  web-api:WebClient:0 http request url: https://slack.com/api/auth.test
[DEBUG]  web-api:WebClient:0 http request body: {"token":"[[REDACTED]]"}
[DEBUG]  web-api:WebClient:0 http request headers: {"Accept":"application/json, text/plain, */*","User-Agent":"@slack:bolt/4.6.0 @slack:web-api/7.13.0 node/24.8.0 darwin/25.2.0","Authorization":"[[REDACTED]]"}
[DEBUG]  socket-mode:SocketModeClient:0 Starting Socket Mode session ...
[DEBUG]  socket-mode:SocketModeClient:0 Going to retrieve a new WSS URL ...
[DEBUG]  web-api:WebClient:0 apiCall('apps.connections.open') start

case "on_cloud_run_watch_manifest_change_reinstalled":
cmd.Println(style.Secondary("App successfully reinstalled"))
case "on_cloud_run_watch_app_change":
path := event.DataToString("cloud_run_watch_app_change")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise ⭐

@srtaalej srtaalej self-requested a review January 29, 2026 22:01
Copy link
Contributor

@srtaalej srtaalej left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! 👏

Update
Got it working for manifest.json ⭐ ⭐ ⭐
in .slack/config.json change source from remote to local

{
  "manifest": {
    "source": "local"
  },

output:

Manifest change detected: []/bolt-js-assistant-template/manifest.json, reinstalling app...
Updating local app install for "ale sandbox"
App successfully reinstalled

Copy link
Member

@mwbrooks mwbrooks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Love Love Love this feature! 💾 ♻️ Great working on implementing this large feature! 👏🏻

🧪 Manual testing work well on both current projects and projects using the new @slack/cli-hooks.

  • Server restarts work well even when there is a syntax error that prevents the Bolt server from starting. ❤️

✏️ Can we tweak the PR Title to be something like "feat: add file watch support to restart Bolt app servers"

❓ Do we have a draft PR for Python?

| config | Object of key-value settings. | Optional |
| config.protocol-version | Array of strings representing the named CLI-SDK protocols supported by the SDK, in descending order of support, as in the first element in the array defines the preferred protocol for use by the SDK, the second element defines the next-preferred protocol, and so on. The only supported named protocol currently is `message-boundaries`. The CLI will use the v1 protocol if this field is not provided. | Optional |
| config.watch | Object with configuration settings for file-watching. | Optional |
| config.watch | Object with configuration settings for file-watching during `slack run`. Supports updating the `manifest` on change and reloading the `app` server. Read [Watch configurations](#watch-configurations) for details. | Optional |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: Out-of-scope of this PR, but I feel we should start to seriously consider a config.json version field. It would allow the CLI to know whether it supports the version of the config file and help developers upgrade when using an older CLI with a newer project.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mwbrooks Great callout! I agree so much. We've been careful to make backward compatible changes but adding that field soon would give me more confidence for whatever might happen later.

Comment on lines +482 to +483
// Begin watching for app file changes
go func() {
Copy link
Member

@mwbrooks mwbrooks Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Non-Blocking. There could be issues when multiple files are saved in quick succession. This may cause the Goroutine to restart the app server multiple times in a row.

A solution may be to add a "debouncing delay" where we wait 500ms after the last file change before restarting the server.

I think we should save this for future work and only address it when it's a confirmed issue.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mwbrooks Debouncing is such a great idea! For now I agree this might be good to save as follow up to keep this PR implementation more minimal. As feedback arrives let's be sure to take this approach to start 🙏 ✨

Comment on lines +96 to +97
// GetManifestWatchConfig returns manifest watch config
func (w *WatchOpts) GetManifestWatchConfig() (paths []string, filterRegex string, enabled bool) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: Since GetManifestWatchConfig and GetAppWatchConfig access a w *WatchOpts pointer, we may want to check that w != nil otherwise the CLI will panic when accessing a nil pointer.

Non-blocker for this PR because we have a lot of unchecked pointers like this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mwbrooks What a nice catch! Let's be diligent toward this in changes more 🤖

I added 5c176af to cover this case in hopes that errors are caught in code and expected!

r.clients.IO.PrintDebug(ctx, "Previous process stopped successfully")
case <-time.After(5 * time.Second):
r.clients.IO.PrintDebug(ctx, "Process did not exit in time, force killing")
_ = process.Kill()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: For completeness, I believe we should wait for the gorountine to complete after the process.Kill(). But, I'm happy to keep the code as-is in case we're concerned about indefinite hangs.

Suggested change
_ = process.Kill()
_ = process.Kill()
<-done // Wait for goroutine to complete after kill

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mwbrooks We might escape the select statement after this "kill" instead of blocking. I think exiting after whatever attempt, failed or not might be alright to keep, but if timeouts occur let's explore this more 🔭

Copy link
Member Author

@zimeg zimeg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@srtaalej @mwbrooks Thanks both for kind reviews and testing 👾 ✨

I'm leaving a few comments on final changes, but hope we can merge this now and follow up with updates to hooks and other changes in documentation as needed! 🚀

Comment on lines +96 to +97
// GetManifestWatchConfig returns manifest watch config
func (w *WatchOpts) GetManifestWatchConfig() (paths []string, filterRegex string, enabled bool) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mwbrooks What a nice catch! Let's be diligent toward this in changes more 🤖

I added 5c176af to cover this case in hopes that errors are caught in code and expected!

r.clients.IO.PrintDebug(ctx, "Previous process stopped successfully")
case <-time.After(5 * time.Second):
r.clients.IO.PrintDebug(ctx, "Process did not exit in time, force killing")
_ = process.Kill()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mwbrooks We might escape the select statement after this "kill" instead of blocking. I think exiting after whatever attempt, failed or not might be alright to keep, but if timeouts occur let's explore this more 🔭

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog Use on updates to be included in the release notes docs M-T: Documentation work only enhancement M-T: A feature request for new functionality semver:minor Use on pull requests to describe the release version increment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants