Skip to content

Commit 5e6abdd

Browse files
committed
Merge branch 'main' into mwbrooks-table-test-consistent-p2-map-pattern-align
2 parents 9d6867e + df8e8d4 commit 5e6abdd

File tree

9 files changed

+537
-48
lines changed

9 files changed

+537
-48
lines changed

cmd/platform/run.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -178,11 +178,14 @@ func newRunLogger(clients *shared.ClientFactory, cmd *cobra.Command) *logger.Log
178178
case "on_cloud_run_watch_error":
179179
message := event.DataToString("cloud_run_watch_error")
180180
clients.IO.PrintError(ctx, "Error: %s", message)
181-
case "on_cloud_run_watch_file_change":
182-
path := event.DataToString("cloud_run_watch_file_change")
183-
cmd.Println(style.Secondary(fmt.Sprintf("File change detected: %s, reinstalling app...", path)))
184-
case "on_cloud_run_watch_file_change_reinstalled":
181+
case "on_cloud_run_watch_manifest_change":
182+
path := event.DataToString("cloud_run_watch_manifest_change")
183+
cmd.Println(style.Secondary(fmt.Sprintf("Manifest change detected: %s, reinstalling app...", path)))
184+
case "on_cloud_run_watch_manifest_change_reinstalled":
185185
cmd.Println(style.Secondary("App successfully reinstalled"))
186+
case "on_cloud_run_watch_app_change":
187+
path := event.DataToString("cloud_run_watch_app_change")
188+
cmd.Println(style.Secondary(fmt.Sprintf("App change detected: %s, restarting server...", path)))
186189
case "on_cleanup_app_install_done":
187190
cmd.Println(style.Secondary(fmt.Sprintf(
188191
`Cleaned up local app install for "%s".`,

docs/reference/hooks/index.md

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,17 @@ The format for the JSON representing the CLI-SDK interface is as follows:
446446
},
447447
"config": {
448448
"protocol-version": ["message-boundaries"],
449-
"sdk-managed-connection-enabled": false
449+
"sdk-managed-connection-enabled": false,
450+
"watch": {
451+
"manifest": {
452+
"paths": ["manifest.json"]
453+
},
454+
"app": {
455+
"paths": ["app.js", "listeners/"],
456+
"filter-regex": "\\.(ts|js)$"
457+
}
458+
},
459+
"trigger-paths": ["triggers/"]
450460
}
451461
}
452462
```
@@ -457,7 +467,7 @@ The format for the JSON representing the CLI-SDK interface is as follows:
457467
| hooks | Object whose keys must match the hook names outlined in the above [Hooks Specification](#specification). Arguments can be provided within this string by separating them with spaces. | Required |
458468
| config | Object of key-value settings. | Optional |
459469
| 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 |
460-
| config.watch | Object with configuration settings for file-watching. | Optional |
470+
| 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 |
461471
| config.sdk-managed-connection-enabled | Boolean specifying whether the WebSocket connection between the CLI and Slack should be managed by the CLI or by the SDK during `slack run` executions. If `true`, the SDK will manage this connection. If `false` or not provided, the CLI will manage this connection. | Optional |
462472
| config.trigger-paths | Array of strings that are paths to files of trigger definitions. | Optional |
463473

@@ -466,6 +476,37 @@ This format must be adhered to, in order of preference, either:
466476
1. As the response to `get-hooks`, or
467477
2. Comprising the contents of the `hooks.json` file
468478

479+
### Watch configurations {#watch-configurations}
480+
481+
The `config.watch` setting looks for file changes during local development with the `slack run` command. The CLI supports separate file watchers for **manifest** changes and changes to **application code** as options for reinstalling the app or reloading the server.
482+
483+
```json
484+
{
485+
"config": {
486+
"watch": {
487+
"manifest": {
488+
"paths": ["manifest.json"]
489+
},
490+
"app": {
491+
"paths": ["app.js", "listeners/"],
492+
"filter-regex": "\\.(ts|js)$"
493+
}
494+
}
495+
}
496+
}
497+
```
498+
499+
| Field | Description | Required |
500+
| --------------------------- | ---------------------------------------------------------------------------------- | -------- |
501+
| watch.manifest | Object configuring the manifest watcher for reinstalling the app. | Optional |
502+
| watch.manifest.paths | Array of file paths or directories to watch for manifest changes. | Required |
503+
| watch.manifest.filter-regex | Regex pattern to filter which files trigger manifest reinstall (e.g., `\\.json$`). | Optional |
504+
| watch.app | Object configuring the app watcher for restarting the app server. | Optional |
505+
| watch.app.paths | Array of file paths or directories to watch for app/code changes. | Required |
506+
| watch.app.filter-regex | Regex pattern to filter which files trigger server reload (e.g., `\\.(ts\|js)$`). | Optional |
507+
508+
**Note:** For backward compatibility, top-level `paths` and `filter-regex` fields are treated as manifest watching configuration only. No server reloading will occur with the legacy structure.
509+
469510
## Hook resolution {#hook-resolution}
470511

471512
The CLI will employ the following algorithm in order to resolve the command to be executed for a particular hook:
@@ -516,8 +557,13 @@ The CLI will employ the following algorithm in order to resolve the command to b
516557
},
517558
"config": {
518559
"watch": {
519-
"filter-regex": "^manifest\\.(ts|js|json)$",
520-
"paths": ["."]
560+
"manifest": {
561+
"paths": ["manifest.json"]
562+
},
563+
"app": {
564+
"paths": ["app.js", "listeners/"],
565+
"filter-regex": "\\.(ts|js)$"
566+
}
521567
},
522568
"sdk-managed-connection-enabled": "true"
523569
}
@@ -543,6 +589,8 @@ The CLI will employ the following algorithm in order to resolve the command to b
543589
}
544590
```
545591

592+
**Note:** The legacy format (top-level `paths` and `filter-regex`) is treated as manifest watching only. No server reloading will occur with this configuration.
593+
546594
## Terms {#terms}
547595

548596
### Types of developers

internal/hooks/sdk_config.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package hooks
1616

1717
import (
18+
"fmt"
1819
"strings"
1920

2021
"github.com/slackapi/slack-cli/internal/slackerror"
@@ -67,7 +68,22 @@ func (pv ProtocolVersions) Preferred() Protocol {
6768
return HookProtocolDefault
6869
}
6970

71+
// WatchOpts contains details of file watcher configurations
7072
type WatchOpts struct {
73+
Manifest *ManifestWatchOpts `json:"manifest,omitempty"`
74+
App *AppWatchOpts `json:"app,omitempty"`
75+
FilterRegex string `json:"filter-regex,omitempty"` // Legacy for manifest watching
76+
Paths []string `json:"paths,omitempty"` // Legacy for manifest watching
77+
}
78+
79+
// ManifestWatchOpts configures watching for manifest changes for reinstall
80+
type ManifestWatchOpts struct {
81+
FilterRegex string `json:"filter-regex,omitempty"`
82+
Paths []string `json:"paths,omitempty"`
83+
}
84+
85+
// AppWatchOpts configures watching for app/code changes for server restart
86+
type AppWatchOpts struct {
7187
FilterRegex string `json:"filter-regex,omitempty"`
7288
Paths []string `json:"paths,omitempty"`
7389
}
@@ -76,3 +92,54 @@ type WatchOpts struct {
7692
func (w *WatchOpts) IsAvailable() bool {
7793
return w != nil
7894
}
95+
96+
// GetManifestWatchConfig returns manifest watch config
97+
func (w *WatchOpts) GetManifestWatchConfig() (paths []string, filterRegex string, enabled bool) {
98+
if w == nil {
99+
return nil, "", false
100+
}
101+
if w.Manifest != nil {
102+
return w.Manifest.Paths, w.Manifest.FilterRegex, len(w.Manifest.Paths) > 0
103+
}
104+
// Backward compatibility: top-level paths/filter-regex for manifest watching
105+
return w.Paths, w.FilterRegex, len(w.Paths) > 0
106+
}
107+
108+
// GetAppWatchConfig returns app watch config
109+
func (w *WatchOpts) GetAppWatchConfig() (paths []string, filterRegex string, enabled bool) {
110+
if w == nil {
111+
return nil, "", false
112+
}
113+
if w.App != nil {
114+
return w.App.Paths, w.App.FilterRegex, len(w.App.Paths) > 0
115+
}
116+
return nil, "", false
117+
}
118+
119+
// String formats WatchOpts for debug outputs
120+
func (w WatchOpts) String() string {
121+
var parts []string
122+
if w.Manifest != nil {
123+
parts = append(parts, fmt.Sprintf("Manifest:%s", w.Manifest.String()))
124+
} else if len(w.Paths) > 0 || w.FilterRegex != "" {
125+
parts = append(parts, fmt.Sprintf("Paths:%v", w.Paths))
126+
parts = append(parts, fmt.Sprintf("FilterRegex:%s", w.FilterRegex))
127+
}
128+
if w.App != nil {
129+
parts = append(parts, fmt.Sprintf("App:%s", w.App.String()))
130+
}
131+
if len(parts) == 0 {
132+
return "{}"
133+
}
134+
return fmt.Sprintf("{%s}", strings.Join(parts, " "))
135+
}
136+
137+
// String formats ManifestWatchOpts for debug outputs
138+
func (m ManifestWatchOpts) String() string {
139+
return fmt.Sprintf("{Paths:%v FilterRegex:%s}", m.Paths, m.FilterRegex)
140+
}
141+
142+
// String formats AppWatchOpts for debug outputs
143+
func (a AppWatchOpts) String() string {
144+
return fmt.Sprintf("{Paths:%v FilterRegex:%s}", a.Paths, a.FilterRegex)
145+
}

internal/hooks/sdk_config_test.go

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,194 @@ func Test_WatchOpts_IsAvailable(t *testing.T) {
121121
})
122122
}
123123
}
124+
125+
func Test_WatchOpts_GetManifestWatchConfig(t *testing.T) {
126+
tests := map[string]struct {
127+
watchOpts *WatchOpts
128+
expectedPaths []string
129+
expectedRegex string
130+
expectedEnabled bool
131+
}{
132+
"Nil WatchOpts pointer": {
133+
watchOpts: nil,
134+
expectedPaths: nil,
135+
expectedRegex: "",
136+
expectedEnabled: false,
137+
},
138+
"Nested manifest config": {
139+
watchOpts: &WatchOpts{
140+
Manifest: &ManifestWatchOpts{
141+
Paths: []string{"manifest.json", "workflows/"},
142+
FilterRegex: "\\.json$",
143+
},
144+
},
145+
expectedPaths: []string{"manifest.json", "workflows/"},
146+
expectedRegex: "\\.json$",
147+
expectedEnabled: true,
148+
},
149+
"Legacy flat config": {
150+
watchOpts: &WatchOpts{
151+
Paths: []string{"manifest.json", "src/"},
152+
FilterRegex: "\\.(json|ts)$",
153+
},
154+
expectedPaths: []string{"manifest.json", "src/"},
155+
expectedRegex: "\\.(json|ts)$",
156+
expectedEnabled: true,
157+
},
158+
"Nested config takes precedence over legacy": {
159+
watchOpts: &WatchOpts{
160+
Paths: []string{"old-path/"},
161+
FilterRegex: "old-regex",
162+
Manifest: &ManifestWatchOpts{
163+
Paths: []string{"new-path/"},
164+
FilterRegex: "new-regex",
165+
},
166+
},
167+
expectedPaths: []string{"new-path/"},
168+
expectedRegex: "new-regex",
169+
expectedEnabled: true,
170+
},
171+
"Empty nested manifest config": {
172+
watchOpts: &WatchOpts{
173+
Manifest: &ManifestWatchOpts{
174+
Paths: []string{},
175+
},
176+
},
177+
expectedPaths: []string{},
178+
expectedRegex: "",
179+
expectedEnabled: false,
180+
},
181+
"Empty legacy config": {
182+
watchOpts: &WatchOpts{
183+
Paths: []string{},
184+
},
185+
expectedPaths: []string{},
186+
expectedRegex: "",
187+
expectedEnabled: false,
188+
},
189+
}
190+
for name, tt := range tests {
191+
t.Run(name, func(t *testing.T) {
192+
paths, regex, enabled := tt.watchOpts.GetManifestWatchConfig()
193+
assert.Equal(t, tt.expectedPaths, paths)
194+
assert.Equal(t, tt.expectedRegex, regex)
195+
assert.Equal(t, tt.expectedEnabled, enabled)
196+
})
197+
}
198+
}
199+
200+
func Test_WatchOpts_GetAppWatchConfig(t *testing.T) {
201+
tests := map[string]struct {
202+
watchOpts *WatchOpts
203+
expectedPaths []string
204+
expectedRegex string
205+
expectedEnabled bool
206+
}{
207+
"Nil WatchOpts pointer": {
208+
watchOpts: nil,
209+
expectedPaths: nil,
210+
expectedRegex: "",
211+
expectedEnabled: false,
212+
},
213+
"Nested app config": {
214+
watchOpts: &WatchOpts{
215+
App: &AppWatchOpts{
216+
Paths: []string{"src/", "functions/"},
217+
FilterRegex: "\\.(ts|js)$",
218+
},
219+
},
220+
expectedPaths: []string{"src/", "functions/"},
221+
expectedRegex: "\\.(ts|js)$",
222+
expectedEnabled: true,
223+
},
224+
"Legacy config does not enable app watching": {
225+
watchOpts: &WatchOpts{
226+
Paths: []string{"manifest.json", "src/"},
227+
FilterRegex: "\\.(json|ts)$",
228+
},
229+
expectedPaths: nil,
230+
expectedRegex: "",
231+
expectedEnabled: false,
232+
},
233+
"Empty nested app config": {
234+
watchOpts: &WatchOpts{
235+
App: &AppWatchOpts{
236+
Paths: []string{},
237+
},
238+
},
239+
expectedPaths: []string{},
240+
expectedRegex: "",
241+
expectedEnabled: false,
242+
},
243+
"Nil app config": {
244+
watchOpts: &WatchOpts{},
245+
expectedPaths: nil,
246+
expectedRegex: "",
247+
expectedEnabled: false,
248+
},
249+
}
250+
for name, tt := range tests {
251+
t.Run(name, func(t *testing.T) {
252+
paths, regex, enabled := tt.watchOpts.GetAppWatchConfig()
253+
assert.Equal(t, tt.expectedPaths, paths)
254+
assert.Equal(t, tt.expectedRegex, regex)
255+
assert.Equal(t, tt.expectedEnabled, enabled)
256+
})
257+
}
258+
}
259+
260+
func Test_WatchOpts_String(t *testing.T) {
261+
tests := map[string]struct {
262+
watchOpts WatchOpts
263+
expectedString string
264+
}{
265+
"Nested config with both manifest and app": {
266+
watchOpts: WatchOpts{
267+
Manifest: &ManifestWatchOpts{
268+
Paths: []string{"manifest.json"},
269+
FilterRegex: "\\.json$",
270+
},
271+
App: &AppWatchOpts{
272+
Paths: []string{"src/", "functions/"},
273+
FilterRegex: "\\.(ts|js)$",
274+
},
275+
},
276+
expectedString: "{Manifest:{Paths:[manifest.json] FilterRegex:\\.json$} App:{Paths:[src/ functions/] FilterRegex:\\.(ts|js)$}}",
277+
},
278+
"Nested manifest only": {
279+
watchOpts: WatchOpts{
280+
Manifest: &ManifestWatchOpts{
281+
Paths: []string{"manifest.json"},
282+
FilterRegex: "\\.json$",
283+
},
284+
},
285+
expectedString: "{Manifest:{Paths:[manifest.json] FilterRegex:\\.json$}}",
286+
},
287+
"Nested app only": {
288+
watchOpts: WatchOpts{
289+
App: &AppWatchOpts{
290+
Paths: []string{"src/"},
291+
FilterRegex: "\\.(ts|js)$",
292+
},
293+
},
294+
expectedString: "{App:{Paths:[src/] FilterRegex:\\.(ts|js)$}}",
295+
},
296+
"Legacy config": {
297+
watchOpts: WatchOpts{
298+
Paths: []string{"manifest.json", "src/"},
299+
FilterRegex: "\\.(json|ts)$",
300+
},
301+
expectedString: "{Paths:[manifest.json src/] FilterRegex:\\.(json|ts)$}",
302+
},
303+
"Empty config": {
304+
watchOpts: WatchOpts{},
305+
expectedString: "{}",
306+
},
307+
}
308+
for name, tt := range tests {
309+
t.Run(name, func(t *testing.T) {
310+
result := tt.watchOpts.String()
311+
assert.Equal(t, tt.expectedString, result)
312+
})
313+
}
314+
}

0 commit comments

Comments
 (0)