happy-stacks 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +314 -0
  2. package/bin/happys.mjs +168 -0
  3. package/docs/menubar.md +186 -0
  4. package/docs/mobile-ios.md +134 -0
  5. package/docs/remote-access.md +43 -0
  6. package/docs/server-flavors.md +79 -0
  7. package/docs/stacks.md +218 -0
  8. package/docs/tauri.md +62 -0
  9. package/docs/worktrees-and-forks.md +395 -0
  10. package/extras/swiftbar/auth-login.sh +31 -0
  11. package/extras/swiftbar/happy-stacks.5s.sh +218 -0
  12. package/extras/swiftbar/icons/happy-green.png +0 -0
  13. package/extras/swiftbar/icons/happy-orange.png +0 -0
  14. package/extras/swiftbar/icons/happy-red.png +0 -0
  15. package/extras/swiftbar/icons/logo-white.png +0 -0
  16. package/extras/swiftbar/install.sh +191 -0
  17. package/extras/swiftbar/lib/git.sh +330 -0
  18. package/extras/swiftbar/lib/icons.sh +105 -0
  19. package/extras/swiftbar/lib/render.sh +774 -0
  20. package/extras/swiftbar/lib/system.sh +190 -0
  21. package/extras/swiftbar/lib/utils.sh +205 -0
  22. package/extras/swiftbar/pnpm-term.sh +125 -0
  23. package/extras/swiftbar/pnpm.sh +21 -0
  24. package/extras/swiftbar/set-interval.sh +62 -0
  25. package/extras/swiftbar/set-server-flavor.sh +57 -0
  26. package/extras/swiftbar/wt-pr.sh +95 -0
  27. package/package.json +58 -0
  28. package/scripts/auth.mjs +272 -0
  29. package/scripts/build.mjs +204 -0
  30. package/scripts/cli-link.mjs +58 -0
  31. package/scripts/completion.mjs +364 -0
  32. package/scripts/daemon.mjs +349 -0
  33. package/scripts/dev.mjs +181 -0
  34. package/scripts/doctor.mjs +342 -0
  35. package/scripts/happy.mjs +79 -0
  36. package/scripts/init.mjs +232 -0
  37. package/scripts/install.mjs +379 -0
  38. package/scripts/menubar.mjs +107 -0
  39. package/scripts/mobile.mjs +305 -0
  40. package/scripts/run.mjs +236 -0
  41. package/scripts/self.mjs +298 -0
  42. package/scripts/server_flavor.mjs +125 -0
  43. package/scripts/service.mjs +526 -0
  44. package/scripts/stack.mjs +815 -0
  45. package/scripts/tailscale.mjs +278 -0
  46. package/scripts/uninstall.mjs +190 -0
  47. package/scripts/utils/args.mjs +17 -0
  48. package/scripts/utils/cli.mjs +24 -0
  49. package/scripts/utils/cli_registry.mjs +262 -0
  50. package/scripts/utils/config.mjs +40 -0
  51. package/scripts/utils/dotenv.mjs +30 -0
  52. package/scripts/utils/env.mjs +138 -0
  53. package/scripts/utils/env_file.mjs +59 -0
  54. package/scripts/utils/env_local.mjs +25 -0
  55. package/scripts/utils/fs.mjs +11 -0
  56. package/scripts/utils/paths.mjs +184 -0
  57. package/scripts/utils/pm.mjs +294 -0
  58. package/scripts/utils/ports.mjs +66 -0
  59. package/scripts/utils/proc.mjs +66 -0
  60. package/scripts/utils/runtime.mjs +30 -0
  61. package/scripts/utils/server.mjs +41 -0
  62. package/scripts/utils/smoke_help.mjs +45 -0
  63. package/scripts/utils/validate.mjs +47 -0
  64. package/scripts/utils/wizard.mjs +69 -0
  65. package/scripts/utils/worktrees.mjs +78 -0
  66. package/scripts/where.mjs +105 -0
  67. package/scripts/worktrees.mjs +1721 -0
@@ -0,0 +1,774 @@
1
+ #!/bin/bash
2
+
3
+ # Requires:
4
+ # - utils.sh
5
+ # - icons.sh
6
+ # - system.sh
7
+
8
+ level_from_server_daemon() {
9
+ local server_status="$1"
10
+ local daemon_status="$2"
11
+ if [[ "$server_status" == "running" && "$daemon_status" == "running" ]]; then
12
+ echo "green"
13
+ return
14
+ fi
15
+ if [[ "$server_status" == "running" || "$daemon_status" == "running" ]]; then
16
+ echo "orange"
17
+ return
18
+ fi
19
+ echo "red"
20
+ }
21
+
22
+ color_for_level() {
23
+ local level="$1"
24
+ if [[ "$level" == "green" ]]; then echo "$GREEN"; return; fi
25
+ if [[ "$level" == "orange" ]]; then echo "$YELLOW"; return; fi
26
+ echo "$RED"
27
+ }
28
+
29
+ sf_for_level() {
30
+ local level="$1"
31
+ if [[ "$level" == "green" ]]; then echo "checkmark.circle.fill"; return; fi
32
+ if [[ "$level" == "orange" ]]; then echo "exclamationmark.triangle.fill"; return; fi
33
+ echo "xmark.circle.fill"
34
+ }
35
+
36
+ print_item() {
37
+ local prefix="$1"
38
+ shift
39
+ echo "${prefix}$*"
40
+ }
41
+
42
+ print_sep() {
43
+ local prefix="$1"
44
+ print_item "$prefix" "---"
45
+ }
46
+
47
+ render_component_server() {
48
+ local prefix="$1" # "" for top-level, "--" for stack submenu
49
+ local stack_name="$2" # main | <name>
50
+ local port="$3"
51
+ local server_component="$4"
52
+ local server_status="$5"
53
+ local server_pid="$6"
54
+ local server_metrics="$7"
55
+ local tailscale_url="$8" # main only (optional)
56
+ local launch_label="${9:-}" # optional (com.happy.stacks[.<stack>])
57
+
58
+ local level="red"
59
+ [[ "$server_status" == "running" ]] && level="green"
60
+
61
+ local label="Server (${server_component})"
62
+ local sf="$(sf_for_level "$level")"
63
+ local color="$(color_for_level "$level")"
64
+ print_item "$prefix" "$label | sfimage=$sf color=$color"
65
+
66
+ local p2="${prefix}--"
67
+ print_item "$p2" "Status: $server_status"
68
+ print_item "$p2" "Internal: http://127.0.0.1:${port}"
69
+ if [[ -n "$server_pid" ]]; then
70
+ if [[ -n "$server_metrics" ]]; then
71
+ local cpu mem etime
72
+ cpu="$(echo "$server_metrics" | cut -d'|' -f1)"
73
+ mem="$(echo "$server_metrics" | cut -d'|' -f2)"
74
+ etime="$(echo "$server_metrics" | cut -d'|' -f3)"
75
+ print_item "$p2" "PID: ${server_pid}, CPU: ${cpu}%, RAM: ${mem}MB, Uptime: ${etime}"
76
+ else
77
+ print_item "$p2" "PID: ${server_pid}"
78
+ fi
79
+ fi
80
+ print_item "$p2" "Open UI (local) | href=http://localhost:${port}/"
81
+ print_item "$p2" "Open Health | href=http://127.0.0.1:${port}/health"
82
+ if [[ -n "$tailscale_url" ]]; then
83
+ print_item "$p2" "Open UI (Tailscale) | href=$tailscale_url"
84
+ fi
85
+
86
+ # Start/stop shortcuts (so you can control from the Server submenu too).
87
+ if [[ -n "$PNPM_BIN" ]]; then
88
+ local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
89
+ local plist=""
90
+ local svc_installed="0"
91
+ if [[ -n "$launch_label" ]]; then
92
+ plist="$HOME/Library/LaunchAgents/${launch_label}.plist"
93
+ [[ -f "$plist" ]] && svc_installed="1"
94
+ fi
95
+
96
+ print_sep "$p2"
97
+ if [[ "$stack_name" == "main" ]]; then
98
+ if [[ "$svc_installed" == "1" ]]; then
99
+ if [[ "$server_status" == "running" ]]; then
100
+ print_item "$p2" "Stop stack (service) | bash=$PNPM_BIN param1=service:stop dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
101
+ else
102
+ print_item "$p2" "Start stack (service) | bash=$PNPM_BIN param1=service:start dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
103
+ fi
104
+ print_item "$p2" "Restart stack (service) | bash=$PNPM_BIN param1=service:restart dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
105
+ else
106
+ if [[ "$server_status" == "running" ]]; then
107
+ print_item "$p2" "Stop (kill port listeners) | bash=$PNPM_TERM param1=stack:fix dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
108
+ else
109
+ print_item "$p2" "Start stack (foreground) | bash=$PNPM_TERM param1=start dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
110
+ fi
111
+ fi
112
+ else
113
+ if [[ "$svc_installed" == "1" ]]; then
114
+ if [[ "$server_status" == "running" ]]; then
115
+ print_item "$p2" "Stop stack (service) | bash=$PNPM_BIN param1=stack param2=service:stop param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
116
+ else
117
+ print_item "$p2" "Start stack (service) | bash=$PNPM_BIN param1=stack param2=service:start param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
118
+ fi
119
+ print_item "$p2" "Restart stack (service) | bash=$PNPM_BIN param1=stack param2=service:restart param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
120
+ else
121
+ if [[ "$server_status" == "running" ]]; then
122
+ print_item "$p2" "Stop (kill port listeners) | bash=$PNPM_TERM param1=stack param2=fix param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
123
+ else
124
+ print_item "$p2" "Start stack (foreground) | bash=$PNPM_TERM param1=stack param2=start param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
125
+ fi
126
+ fi
127
+ fi
128
+ fi
129
+
130
+ # Flavor switching (status-aware: only show switching to the other option).
131
+ local helper="$HAPPY_LOCAL_DIR/extras/swiftbar/set-server-flavor.sh"
132
+ if [[ -n "$PNPM_BIN" ]]; then
133
+ local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
134
+ print_sep "$p2"
135
+ if [[ "$server_component" == "happy-server" ]]; then
136
+ print_item "$p2" "Switch to happy-server-light (restart if service installed) | bash=$helper param1=$stack_name param2=happy-server-light dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
137
+ else
138
+ print_item "$p2" "Switch to happy-server (restart if service installed) | bash=$helper param1=$stack_name param2=happy-server dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
139
+ fi
140
+ if [[ "$stack_name" == "main" ]]; then
141
+ print_item "$p2" "Show flavor status | bash=$PNPM_TERM param1=srv param2=-- param3=status dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
142
+ else
143
+ print_item "$p2" "Show flavor status | bash=$PNPM_TERM param1=stack param2=srv param3=$stack_name param4=-- param5=status dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
144
+ fi
145
+ fi
146
+ }
147
+
148
+ render_component_daemon() {
149
+ local prefix="$1"
150
+ local daemon_status="$2" # running|stale|stopped|unknown|running-no-http|auth_required|starting
151
+ local daemon_pid="$3"
152
+ local daemon_metrics="$4"
153
+ local daemon_uptime="$5"
154
+ local last_heartbeat="$6"
155
+ local state_file="$7"
156
+ local stack_name="$8"
157
+
158
+ local level="red"
159
+ if [[ "$daemon_status" == "running" ]]; then level="green"; fi
160
+ if [[ "$daemon_status" == "running-no-http" || "$daemon_status" == "stale" || "$daemon_status" == "auth_required" || "$daemon_status" == "starting" ]]; then level="orange"; fi
161
+
162
+ local sf="$(sf_for_level "$level")"
163
+ local color="$(color_for_level "$level")"
164
+ print_item "$prefix" "Daemon | sfimage=$sf color=$color"
165
+
166
+ local p2="${prefix}--"
167
+ print_item "$p2" "Status: $daemon_status"
168
+ if [[ -n "$daemon_pid" ]]; then
169
+ if [[ -n "$daemon_metrics" ]]; then
170
+ local cpu mem etime
171
+ cpu="$(echo "$daemon_metrics" | cut -d'|' -f1)"
172
+ mem="$(echo "$daemon_metrics" | cut -d'|' -f2)"
173
+ etime="$(echo "$daemon_metrics" | cut -d'|' -f3)"
174
+ print_item "$p2" "PID: ${daemon_pid}, CPU: ${cpu}%, RAM: ${mem}MB, Uptime: ${etime}"
175
+ else
176
+ print_item "$p2" "PID: ${daemon_pid}"
177
+ fi
178
+ fi
179
+ [[ -n "$daemon_uptime" ]] && print_item "$p2" "Started: $(shorten_text "$daemon_uptime" 52)"
180
+ [[ -n "$last_heartbeat" ]] && print_item "$p2" "Last heartbeat: $(shorten_text "$last_heartbeat" 52)"
181
+ # State file may not exist yet (e.g. daemon is waiting for auth).
182
+ print_item "$p2" "State file: $(shorten_path "$state_file" 52)"
183
+
184
+ if [[ -n "$PNPM_BIN" ]]; then
185
+ print_sep "$p2"
186
+ if [[ "$daemon_status" == "auth_required" ]]; then
187
+ # Provide a direct "fix" action for the common first-run problem under launchd.
188
+ local auth_helper="$HAPPY_LOCAL_DIR/extras/swiftbar/auth-login.sh"
189
+ local server_url="http://127.0.0.1:$(resolve_main_port)"
190
+ local webapp_url
191
+ webapp_url="$(get_tailscale_url)"
192
+ [[ -z "$webapp_url" ]] && webapp_url="http://localhost:$(resolve_main_port)"
193
+ if [[ "$stack_name" == "main" ]]; then
194
+ print_item "$p2" "Auth login (opens browser) | bash=$auth_helper param1=main param2=$server_url param3=$webapp_url dir=$HAPPY_LOCAL_DIR terminal=false refresh=false"
195
+ else
196
+ # For stacks, best-effort use the stack's configured port if available (fallback to main port).
197
+ local env_file="$HOME/.happy/stacks/$stack_name/env"
198
+ local port
199
+ port="$(dotenv_get "$env_file" "HAPPY_LOCAL_SERVER_PORT")"
200
+ [[ -z "$port" ]] && port="$(resolve_main_port)"
201
+ server_url="http://127.0.0.1:${port}"
202
+ webapp_url="$(get_tailscale_url)"
203
+ [[ -z "$webapp_url" ]] && webapp_url="http://localhost:${port}"
204
+ print_item "$p2" "Auth login (opens browser) | bash=$auth_helper param1=$stack_name param2=$server_url param3=$webapp_url param4=$HOME/.happy/stacks/$stack_name/cli dir=$HAPPY_LOCAL_DIR terminal=false refresh=false"
205
+ fi
206
+ print_sep "$p2"
207
+ fi
208
+ if [[ "$stack_name" == "main" ]]; then
209
+ print_item "$p2" "Restart stack (service) | bash=$PNPM_BIN param1=service:restart dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
210
+ else
211
+ print_item "$p2" "Restart stack (service) | bash=$PNPM_BIN param1=stack param2=service:restart param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
212
+ fi
213
+ fi
214
+ }
215
+
216
+ render_component_autostart() {
217
+ local prefix="$1"
218
+ local stack_name="$2"
219
+ local label="$3"
220
+ local launchagent_status="$4" # loaded|unloaded|not_installed
221
+ local autostart_pid="$5"
222
+ local autostart_metrics="$6"
223
+ local logs_dir="$7"
224
+
225
+ local level="red"
226
+ if [[ "$launchagent_status" == "loaded" ]]; then level="green"; fi
227
+ if [[ "$launchagent_status" == "unloaded" ]]; then level="orange"; fi
228
+
229
+ local sf="$(sf_for_level "$level")"
230
+ local color="$(color_for_level "$level")"
231
+ print_item "$prefix" "Autostart | sfimage=$sf color=$color"
232
+
233
+ local p2="${prefix}--"
234
+ print_item "$p2" "Status: $launchagent_status"
235
+ print_item "$p2" "Plist: $(shorten_path "$HOME/Library/LaunchAgents/${label}.plist" 52)"
236
+ if [[ -n "$autostart_pid" ]]; then
237
+ if [[ -n "$autostart_metrics" ]]; then
238
+ local cpu mem etime
239
+ cpu="$(echo "$autostart_metrics" | cut -d'|' -f1)"
240
+ mem="$(echo "$autostart_metrics" | cut -d'|' -f2)"
241
+ etime="$(echo "$autostart_metrics" | cut -d'|' -f3)"
242
+ print_item "$p2" "PID: ${autostart_pid}, CPU: ${cpu}%, RAM: ${mem}MB, Uptime: ${etime}"
243
+ else
244
+ print_item "$p2" "PID: ${autostart_pid}"
245
+ fi
246
+ fi
247
+ local stdout_file="happy-stacks.out.log"
248
+ local stderr_file="happy-stacks.err.log"
249
+ if [[ -f "${logs_dir}/happy-local.out.log" && ! -f "${logs_dir}/happy-stacks.out.log" ]]; then stdout_file="happy-local.out.log"; fi
250
+ if [[ -f "${logs_dir}/happy-local.err.log" && ! -f "${logs_dir}/happy-stacks.err.log" ]]; then stderr_file="happy-local.err.log"; fi
251
+ print_item "$p2" "Open logs (stdout) | bash=/usr/bin/open param1=-a param2=Console param3='${logs_dir}/${stdout_file}' terminal=false"
252
+ print_item "$p2" "Open logs (stderr) | bash=/usr/bin/open param1=-a param2=Console param3='${logs_dir}/${stderr_file}' terminal=false"
253
+
254
+ if [[ -z "$PNPM_BIN" ]]; then
255
+ return
256
+ fi
257
+ print_sep "$p2"
258
+ if [[ "$stack_name" == "main" ]]; then
259
+ if [[ "$launchagent_status" == "not_installed" ]]; then
260
+ print_item "$p2" "Install Autostart | bash=$PNPM_BIN param1=service:install dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
261
+ return
262
+ fi
263
+ # Status-aware: show only the relevant toggle (enable vs disable).
264
+ if [[ "$launchagent_status" == "loaded" ]]; then
265
+ print_item "$p2" "Disable Autostart | bash=$PNPM_BIN param1=service:disable dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
266
+ else
267
+ print_item "$p2" "Enable Autostart | bash=$PNPM_BIN param1=service:enable dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
268
+ fi
269
+ print_item "$p2" "Uninstall Autostart | bash=$PNPM_BIN param1=service:uninstall dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
270
+ return
271
+ fi
272
+
273
+ if [[ "$launchagent_status" == "not_installed" ]]; then
274
+ print_item "$p2" "Install Autostart | bash=$PNPM_BIN param1=stack param2=service:install param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
275
+ return
276
+ fi
277
+ # Status-aware: show only the relevant toggle (enable vs disable).
278
+ if [[ "$launchagent_status" == "loaded" ]]; then
279
+ print_item "$p2" "Disable Autostart | bash=$PNPM_BIN param1=stack param2=service:disable param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
280
+ else
281
+ print_item "$p2" "Enable Autostart | bash=$PNPM_BIN param1=stack param2=service:enable param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
282
+ fi
283
+ print_item "$p2" "Uninstall Autostart | bash=$PNPM_BIN param1=stack param2=service:uninstall param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
284
+ }
285
+
286
+ render_component_tailscale() {
287
+ local prefix="$1"
288
+ local stack_name="$2"
289
+ local tailscale_url="$3"
290
+
291
+ local level="red"
292
+ [[ -n "$tailscale_url" ]] && level="green"
293
+
294
+ local sf="$(sf_for_level "$level")"
295
+ local color="$(color_for_level "$level")"
296
+ print_item "$prefix" "Tailscale | sfimage=$sf color=$color"
297
+
298
+ local p2="${prefix}--"
299
+ if [[ -n "$tailscale_url" ]]; then
300
+ local display="$tailscale_url"
301
+ [[ ${#display} -gt 48 ]] && display="${display:0:48}..."
302
+ print_item "$p2" "URL: $display"
303
+ print_item "$p2" "Copy URL | bash=/bin/bash param1=-c param2='echo -n \"$tailscale_url\" | pbcopy' terminal=false"
304
+ print_item "$p2" "Open URL | href=$tailscale_url"
305
+ else
306
+ print_item "$p2" "Status: not configured / unknown"
307
+ fi
308
+
309
+ if [[ -z "$PNPM_BIN" ]]; then
310
+ return
311
+ fi
312
+ print_sep "$p2"
313
+
314
+ if [[ "$stack_name" == "main" ]]; then
315
+ local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
316
+ print_item "$p2" "Tailscale status | bash=$PNPM_TERM param1=tailscale:status dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
317
+ if [[ -n "$tailscale_url" ]]; then
318
+ print_item "$p2" "Disable Tailscale Serve | bash=$PNPM_BIN param1=tailscale:disable dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
319
+ else
320
+ print_item "$p2" "Enable Tailscale Serve | bash=$PNPM_TERM param1=tailscale:enable dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
321
+ fi
322
+ print_item "$p2" "Print URL | bash=$PNPM_TERM param1=tailscale:url dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
323
+ return
324
+ fi
325
+
326
+ local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
327
+ print_item "$p2" "Tailscale status | bash=$PNPM_TERM param1=stack param2=tailscale:status param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
328
+ if [[ -n "$tailscale_url" ]]; then
329
+ print_item "$p2" "Disable Tailscale Serve | bash=$PNPM_BIN param1=stack param2=tailscale:disable param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
330
+ else
331
+ print_item "$p2" "Enable Tailscale Serve | bash=$PNPM_TERM param1=stack param2=tailscale:enable param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
332
+ fi
333
+ print_item "$p2" "Print URL | bash=$PNPM_TERM param1=stack param2=tailscale:url param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
334
+ }
335
+
336
+ render_component_repo() {
337
+ # Git/worktree component view (unified UI).
338
+ # Usage:
339
+ # render_component_repo <prefix> <component> <context> <stack_name> <env_file>
340
+ # context: main|stack
341
+ local prefix="$1"
342
+ local component="$2"
343
+ local context="$3"
344
+ local stack_name="$4"
345
+ local env_file="$5"
346
+
347
+ local active_dir=""
348
+ if [[ "$context" == "stack" && -n "$env_file" ]]; then
349
+ active_dir="$(resolve_component_dir_from_env_file "$env_file" "$component")"
350
+ else
351
+ active_dir="$(resolve_component_dir_from_env "$component")"
352
+ fi
353
+
354
+ local level="red"
355
+ local detail="missing"
356
+ if is_git_repo "$active_dir"; then
357
+ local dirty
358
+ dirty="$(git_dirty_flag "$active_dir")"
359
+ local ab
360
+ ab="$(git_ahead_behind "$active_dir")"
361
+ local ahead="" behind=""
362
+ if [[ -n "$ab" ]]; then
363
+ ahead="$(echo "$ab" | cut -d'|' -f1)"
364
+ behind="$(echo "$ab" | cut -d'|' -f2)"
365
+ fi
366
+
367
+ if [[ "$dirty" == "dirty" ]] || [[ -n "$behind" && "$behind" != "0" ]]; then
368
+ level="orange"
369
+ else
370
+ level="green"
371
+ fi
372
+ detail="ok"
373
+ fi
374
+
375
+ local sf color
376
+ sf="$(sf_for_level "$level")"
377
+ color="$(color_for_level "$level")"
378
+ print_item "$prefix" "${component} | sfimage=$sf color=$color"
379
+
380
+ local p2="${prefix}--"
381
+ print_item "$p2" "Dir: $(shorten_path "$active_dir" 52)"
382
+ if [[ "$detail" != "ok" ]]; then
383
+ print_item "$p2" "Status: not a git repo / missing"
384
+ if [[ -n "$PNPM_BIN" ]]; then
385
+ print_sep "$p2"
386
+ local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
387
+ print_item "$p2" "Bootstrap (clone missing components) | bash=$PNPM_TERM param1=bootstrap dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
388
+ fi
389
+ return
390
+ fi
391
+
392
+ local branch head upstream
393
+ branch="$(git_head_branch "$active_dir")"
394
+ head="$(git_head_short "$active_dir")"
395
+ upstream="$(git_upstream_short "$active_dir")"
396
+
397
+ local dirty
398
+ dirty="$(git_dirty_flag "$active_dir")"
399
+ local ab ahead behind
400
+ ab="$(git_ahead_behind "$active_dir")"
401
+ ahead=""
402
+ behind=""
403
+ if [[ -n "$ab" ]]; then
404
+ ahead="$(echo "$ab" | cut -d'|' -f1)"
405
+ behind="$(echo "$ab" | cut -d'|' -f2)"
406
+ fi
407
+
408
+ print_item "$p2" "HEAD: ${branch:-"(unknown)"} ${head:+($head)}"
409
+ print_item "$p2" "Upstream: ${upstream:-"(none)"}"
410
+ if [[ -n "$ahead" && -n "$behind" ]]; then
411
+ print_item "$p2" "Ahead/Behind: ${ahead}/${behind}"
412
+ fi
413
+ print_item "$p2" "Working tree: ${dirty}"
414
+
415
+ local main_branch main_upstream main_ab
416
+ main_branch="$(git_main_branch_name "$active_dir")"
417
+ if [[ -n "$main_branch" ]]; then
418
+ main_upstream="$(git_branch_upstream_short "$active_dir" "$main_branch")"
419
+ main_ab="$(git_branch_ahead_behind "$active_dir" "$main_branch")"
420
+ if [[ -n "$main_upstream" ]]; then
421
+ print_item "$p2" "Main: ${main_branch} → ${main_upstream}"
422
+ else
423
+ print_item "$p2" "Main: ${main_branch} → (no upstream)"
424
+ fi
425
+ if [[ -n "$main_ab" ]]; then
426
+ print_item "$p2" "Main ahead/behind: $(echo "$main_ab" | cut -d'|' -f1)/$(echo "$main_ab" | cut -d'|' -f2)"
427
+ fi
428
+
429
+ # Always show comparisons against origin/* and upstream/* when those remote refs exist.
430
+ # (These reflect your last fetch; we do not auto-fetch in the menu.)
431
+ local oref uref
432
+ oref="$(git_remote_main_ref "$active_dir" "origin")"
433
+ uref="$(git_remote_main_ref "$active_dir" "upstream")"
434
+ if [[ -n "$oref" ]]; then
435
+ local oref_short="${oref#refs/remotes/}"
436
+ local oab
437
+ oab="$(git_ahead_behind_refs "$active_dir" "$oref" "$main_branch")"
438
+ if [[ -n "$oab" ]]; then
439
+ print_item "$p2" "Origin: ${oref_short} ahead/behind: $(echo "$oab" | cut -d'|' -f1)/$(echo "$oab" | cut -d'|' -f2)"
440
+ else
441
+ print_item "$p2" "Origin: ${oref_short}"
442
+ fi
443
+ else
444
+ print_item "$p2" "Origin: (no origin/main|master ref)"
445
+ fi
446
+ if [[ -n "$uref" ]]; then
447
+ local uref_short="${uref#refs/remotes/}"
448
+ local uab
449
+ uab="$(git_ahead_behind_refs "$active_dir" "$uref" "$main_branch")"
450
+ if [[ -n "$uab" ]]; then
451
+ print_item "$p2" "Upstream: ${uref_short} ahead/behind: $(echo "$uab" | cut -d'|' -f1)/$(echo "$uab" | cut -d'|' -f2)"
452
+ else
453
+ print_item "$p2" "Upstream: ${uref_short}"
454
+ fi
455
+ else
456
+ print_item "$p2" "Upstream: (no upstream/main|master ref)"
457
+ fi
458
+ fi
459
+
460
+ local wt_count
461
+ wt_count="$(git_worktree_count "$active_dir")"
462
+
463
+ # Quick actions
464
+ print_sep "$p2"
465
+ print_item "$p2" "Open folder | bash=/usr/bin/open param1='$active_dir' terminal=false"
466
+
467
+ if [[ -n "$PNPM_BIN" ]]; then
468
+ local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
469
+ # Run via stack wrappers when in a stack context so env-file stays authoritative.
470
+ if [[ "$context" == "stack" && -n "$stack_name" ]]; then
471
+ print_item "$p2" "Status (active) | bash=$PNPM_TERM param1=stack param2=wt param3=$stack_name param4=-- param5=status param6=$component dir=$HAPPY_LOCAL_DIR terminal=false"
472
+ print_item "$p2" "Sync mirror (upstream/main) | bash=$PNPM_BIN param1=stack param2=wt param3=$stack_name param4=-- param5=sync param6=$component dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
473
+ print_item "$p2" "Update (dry-run) | bash=$PNPM_TERM param1=stack param2=wt param3=$stack_name param4=-- param5=update param6=$component param7=active param8=--dry-run dir=$HAPPY_LOCAL_DIR terminal=false"
474
+ print_item "$p2" "Update (apply) | bash=$PNPM_BIN param1=stack param2=wt param3=$stack_name param4=-- param5=update param6=$component param7=active dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
475
+ print_item "$p2" "Update (apply + stash) | bash=$PNPM_BIN param1=stack param2=wt param3=$stack_name param4=-- param5=update param6=$component param7=active param8=--stash dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
476
+ else
477
+ print_item "$p2" "Status (active) | bash=$PNPM_TERM param1=wt param2=status param3=$component dir=$HAPPY_LOCAL_DIR terminal=false"
478
+ print_item "$p2" "Sync mirror (upstream/main) | bash=$PNPM_BIN param1=wt param2=sync param3=$component dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
479
+ print_item "$p2" "Update (dry-run) | bash=$PNPM_TERM param1=wt param2=update param3=$component param4=active param5=--dry-run dir=$HAPPY_LOCAL_DIR terminal=false"
480
+ print_item "$p2" "Update (apply) | bash=$PNPM_BIN param1=wt param2=update param3=$component param4=active dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
481
+ print_item "$p2" "Update (apply + stash) | bash=$PNPM_BIN param1=wt param2=update param3=$component param4=active param5=--stash dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
482
+ fi
483
+
484
+ print_sep "$p2"
485
+ if [[ "$context" == "stack" && -n "$stack_name" ]]; then
486
+ print_item "$p2" "Switch stack worktree (interactive) | bash=$PNPM_TERM param1=stack param2=wt param3=$stack_name param4=-- param5=use param6=$component param7=--interactive dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
487
+ print_item "$p2" "New worktree (interactive) | bash=$PNPM_TERM param1=stack param2=wt param3=$stack_name param4=-- param5=new param6=--interactive dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
488
+ print_item "$p2" "List worktrees (terminal) | bash=$PNPM_TERM param1=stack param2=wt param3=$stack_name param4=-- param5=list param6=$component dir=$HAPPY_LOCAL_DIR terminal=false"
489
+ else
490
+ print_item "$p2" "Use worktree (interactive) | bash=$PNPM_TERM param1=wt param2=use param3=--interactive dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
491
+ print_item "$p2" "New worktree (interactive) | bash=$PNPM_TERM param1=wt param2=new param3=--interactive dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
492
+ print_item "$p2" "List worktrees (terminal) | bash=$PNPM_TERM param1=wt param2=list param3=$component dir=$HAPPY_LOCAL_DIR terminal=false"
493
+ fi
494
+
495
+ # PR worktree (prompt)
496
+ local pr_helper="$HAPPY_LOCAL_DIR/extras/swiftbar/wt-pr.sh"
497
+ if [[ -x "$pr_helper" ]]; then
498
+ if [[ "$context" == "stack" && -n "$stack_name" ]]; then
499
+ print_item "$p2" "PR worktree (prompt) | bash=$pr_helper param1=$component param2=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
500
+ else
501
+ print_item "$p2" "PR worktree (prompt) | bash=$pr_helper param1=$component dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
502
+ fi
503
+ fi
504
+
505
+ print_sep "$p2"
506
+ if [[ "$context" == "stack" && -n "$stack_name" ]]; then
507
+ print_item "$p2" "Shell (active, new window) | bash=$PNPM_TERM param1=stack param2=wt param3=$stack_name param4=-- param5=shell param6=$component param7=active param8=--new-window dir=$HAPPY_LOCAL_DIR terminal=false"
508
+ print_item "$p2" "Open in VS Code (active) | bash=$PNPM_BIN param1=stack param2=wt param3=$stack_name param4=-- param5=code param6=$component param7=active dir=$HAPPY_LOCAL_DIR terminal=false"
509
+ print_item "$p2" "Open in Cursor (active) | bash=$PNPM_BIN param1=stack param2=wt param3=$stack_name param4=-- param5=cursor param6=$component param7=active dir=$HAPPY_LOCAL_DIR terminal=false"
510
+ else
511
+ print_item "$p2" "Shell (active, new window) | bash=$PNPM_TERM param1=wt param2=shell param3=$component param4=active param5=--new-window dir=$HAPPY_LOCAL_DIR terminal=false"
512
+ print_item "$p2" "Open in VS Code (active) | bash=$PNPM_BIN param1=wt param2=code param3=$component param4=active dir=$HAPPY_LOCAL_DIR terminal=false"
513
+ print_item "$p2" "Open in Cursor (active) | bash=$PNPM_BIN param1=wt param2=cursor param3=$component param4=active dir=$HAPPY_LOCAL_DIR terminal=false"
514
+ fi
515
+
516
+ # Worktrees listing (inline in SwiftBar, plus stack-aware switch).
517
+ local wt_label="Worktrees: ${wt_count:-0} | sfimage=arrow.triangle.branch"
518
+ print_item "$p2" "$wt_label"
519
+ local p3="${p2}--"
520
+ local tsv
521
+ tsv="$(git_worktrees_tsv "$active_dir" 2>/dev/null || true)"
522
+ if [[ -z "$tsv" ]]; then
523
+ print_item "$p3" "No worktrees found | color=$GRAY"
524
+ else
525
+ local root="$(resolve_components_dir)/.worktrees/$component/"
526
+ local shown=0
527
+ while IFS=$'\t' read -r wt_path wt_branchref; do
528
+ [[ -n "$wt_path" ]] || continue
529
+ shown=$((shown + 1))
530
+ if [[ $shown -gt 30 ]]; then
531
+ print_item "$p3" "More… (open folder) | bash=/usr/bin/open param1='$root' terminal=false"
532
+ break
533
+ fi
534
+
535
+ local label=""
536
+ local spec=""
537
+ if [[ "$wt_path" == "$root"* ]]; then
538
+ spec="${wt_path#"$root"}"
539
+ label="$spec"
540
+ else
541
+ spec="$wt_path"
542
+ label="$(shorten_path "$wt_path" 52)"
543
+ fi
544
+
545
+ if [[ -n "$wt_branchref" && "$wt_branchref" == refs/heads/* ]]; then
546
+ label="$label ($(basename "$wt_branchref"))"
547
+ fi
548
+ if [[ "$wt_path" == "$active_dir" ]]; then
549
+ label="(active) $label"
550
+ fi
551
+
552
+ if [[ "$context" == "stack" && -n "$stack_name" ]]; then
553
+ print_item "$p3" "$label"
554
+ print_item "${p3}--" "Use in stack | bash=$PNPM_BIN param1=stack param2=wt param3=$stack_name param4=-- param5=use param6=$component param7=$spec dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
555
+ print_item "${p3}--" "Shell (new window) | bash=$PNPM_TERM param1=stack param2=wt param3=$stack_name param4=-- param5=shell param6=$component param7=$spec param8=--new-window dir=$HAPPY_LOCAL_DIR terminal=false"
556
+ print_item "${p3}--" "Open in VS Code | bash=$PNPM_BIN param1=stack param2=wt param3=$stack_name param4=-- param5=code param6=$component param7=$spec dir=$HAPPY_LOCAL_DIR terminal=false"
557
+ print_item "${p3}--" "Open in Cursor | bash=$PNPM_BIN param1=stack param2=wt param3=$stack_name param4=-- param5=cursor param6=$component param7=$spec dir=$HAPPY_LOCAL_DIR terminal=false"
558
+ else
559
+ print_item "$p3" "$label"
560
+ print_item "${p3}--" "Use (main) | bash=$PNPM_BIN param1=wt param2=use param3=$component param4=$spec dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
561
+ print_item "${p3}--" "Shell (new window) | bash=$PNPM_TERM param1=wt param2=shell param3=$component param4=$spec param5=--new-window dir=$HAPPY_LOCAL_DIR terminal=false"
562
+ print_item "${p3}--" "Open in VS Code | bash=$PNPM_BIN param1=wt param2=code param3=$component param4=$spec dir=$HAPPY_LOCAL_DIR terminal=false"
563
+ print_item "${p3}--" "Open in Cursor | bash=$PNPM_BIN param1=wt param2=cursor param3=$component param4=$spec dir=$HAPPY_LOCAL_DIR terminal=false"
564
+ fi
565
+ done <<<"$tsv"
566
+ fi
567
+ fi
568
+ }
569
+
570
+ render_components_menu() {
571
+ # Usage: render_components_menu <prefix> <context> <stack_name> <env_file>
572
+ local prefix="$1" # "" for main menu, "--" for inside a stack
573
+ local context="$2" # main|stack
574
+ local stack_name="$3"
575
+ local env_file="$4"
576
+
577
+ print_item "$prefix" "Components | sfimage=cube"
578
+ local p2="${prefix}--"
579
+ local components_dir
580
+ components_dir="$(resolve_components_dir)"
581
+ if [[ ! -d "$components_dir" ]]; then
582
+ print_item "$p2" "Missing components dir: $(shorten_path "$components_dir" 52) | color=$GRAY"
583
+ return
584
+ fi
585
+
586
+ local any="0"
587
+ for c in happy happy-cli happy-server-light happy-server; do
588
+ if [[ -d "$components_dir/$c" ]]; then
589
+ any="1"
590
+ render_component_repo "$p2" "$c" "$context" "$stack_name" "$env_file"
591
+ print_sep "$p2"
592
+ fi
593
+ done
594
+ if [[ "$any" == "0" ]]; then
595
+ print_item "$p2" "No components found under components/ | color=$GRAY"
596
+ fi
597
+ }
598
+
599
+ render_stack_overview_item() {
600
+ local title="$1"
601
+ local level="$2" # green|orange|red
602
+ local prefix="$3"
603
+
604
+ local icon_b64
605
+ icon_b64="$(status_icon_b64 "$level" 14)"
606
+ if [[ -n "$icon_b64" ]]; then
607
+ print_item "$prefix" "$title | image=$icon_b64"
608
+ else
609
+ print_item "$prefix" "$title"
610
+ fi
611
+ }
612
+
613
+ collect_stack_status() {
614
+ # Output (tab-separated):
615
+ # level server_status server_pid server_metrics daemon_status daemon_pid daemon_metrics daemon_uptime last_heartbeat launchagent_status autostart_pid autostart_metrics
616
+ local port="$1"
617
+ local cli_home_dir="$2"
618
+ local label="$3"
619
+ local base_dir="$4"
620
+
621
+ local server_status server_pid server_metrics
622
+ server_status="$(check_server_health "$port")"
623
+ server_pid=""
624
+ server_metrics=""
625
+ if [[ "$server_status" == "running" ]]; then
626
+ server_pid="$(get_port_listener_pid "$port")"
627
+ server_metrics="$(get_process_metrics "$server_pid")"
628
+ fi
629
+
630
+ local daemon_raw daemon_status daemon_pid daemon_metrics
631
+ daemon_raw="$(check_daemon_status "$cli_home_dir")"
632
+ daemon_status="$daemon_raw"
633
+ daemon_pid=""
634
+ if [[ "$daemon_raw" == running:* ]] || [[ "$daemon_raw" == running-no-http:* ]]; then
635
+ daemon_pid="${daemon_raw#*:}"
636
+ daemon_status="${daemon_raw%%:*}"
637
+ fi
638
+ daemon_metrics=""
639
+ if [[ -n "$daemon_pid" ]]; then
640
+ daemon_metrics="$(get_process_metrics "$daemon_pid")"
641
+ fi
642
+
643
+ local daemon_uptime last_heartbeat
644
+ daemon_uptime="$(get_daemon_uptime "$cli_home_dir")"
645
+ last_heartbeat="$(get_last_heartbeat "$cli_home_dir")"
646
+
647
+ local plist_path="$HOME/Library/LaunchAgents/${label}.plist"
648
+ local launchagent_status autostart_pid autostart_metrics
649
+ launchagent_status="$(check_launchagent_status "$label" "$plist_path")"
650
+ autostart_pid=""
651
+ autostart_metrics=""
652
+ if [[ "$launchagent_status" != "not_installed" ]]; then
653
+ autostart_pid="$(launchagent_pid_for_label "$label")"
654
+ autostart_metrics="$(get_process_metrics "$autostart_pid")"
655
+ fi
656
+
657
+ local level
658
+ level="$(level_from_server_daemon "$server_status" "$daemon_status")"
659
+
660
+ # Important: callers use `read` with IFS=$'\t' which collapses consecutive delimiters.
661
+ # Emit "-" placeholders for optional/empty fields so parsing stays stable.
662
+ local spid="${server_pid:-"-"}"
663
+ local smet="${server_metrics:-"-"}"
664
+ local dpid="${daemon_pid:-"-"}"
665
+ local dmet="${daemon_metrics:-"-"}"
666
+ local dupt="${daemon_uptime:-"-"}"
667
+ local dhb="${last_heartbeat:-"-"}"
668
+ local apid="${autostart_pid:-"-"}"
669
+ local amet="${autostart_metrics:-"-"}"
670
+
671
+ printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
672
+ "$level" \
673
+ "$server_status" "$spid" "$smet" \
674
+ "$daemon_status" "$dpid" "$dmet" "$dupt" "$dhb" \
675
+ "$launchagent_status" "$apid" "$amet"
676
+ }
677
+
678
+ render_stack_info() {
679
+ # Renders a single "Info" item (with actions) at the given prefix.
680
+ local prefix="$1" # "" or "--"
681
+ local stack_name="$2"
682
+ local port="$3"
683
+ local server_component="$4"
684
+ local base_dir="$5"
685
+ local cli_home_dir="$6"
686
+ local label="$7"
687
+ local env_file="$8" # optional
688
+ local tailscale_url="$9" # optional
689
+
690
+ # Avoid low-contrast gray in the main list; keep it readable in both light/dark.
691
+ print_item "$prefix" "Stack details | sfimage=info.circle"
692
+ local p2="${prefix}--"
693
+ print_item "$p2" "Server component: ${server_component}"
694
+ print_item "$p2" "Port: ${port}"
695
+ print_item "$p2" "Label: ${label}"
696
+ [[ -n "$env_file" ]] && print_item "$p2" "Env: $(shorten_path "$env_file" 52)"
697
+ [[ -n "$tailscale_url" ]] && print_item "$p2" "Tailscale: $(shorten_text "$tailscale_url" 52)"
698
+
699
+ print_sep "$p2"
700
+ print_item "$p2" "Open repo | bash=/usr/bin/open param1='$HAPPY_LOCAL_DIR' terminal=false"
701
+ print_item "$p2" "Open data dir | bash=/usr/bin/open param1='$base_dir' terminal=false"
702
+ print_item "$p2" "Open logs dir | bash=/usr/bin/open param1='${base_dir}/logs' terminal=false"
703
+ print_item "$p2" "Open CLI home | bash=/usr/bin/open param1='$cli_home_dir' terminal=false"
704
+ if [[ "$stack_name" == "main" ]]; then
705
+ local main_env
706
+ main_env="$(resolve_main_env_file)"
707
+ if [[ -n "$main_env" ]]; then
708
+ print_item "$p2" "Edit main env | bash=/usr/bin/open param1=-a param2=TextEdit param3='$main_env' terminal=false"
709
+ else
710
+ print_item "$p2" "Edit env.local | bash=/usr/bin/open param1=-a param2=TextEdit param3='$HAPPY_LOCAL_DIR/env.local' terminal=false"
711
+ fi
712
+ else
713
+ print_item "$p2" "Open stack env | bash=/usr/bin/open param1='$env_file' terminal=false"
714
+ fi
715
+
716
+ if [[ -z "$PNPM_BIN" ]]; then
717
+ return
718
+ fi
719
+ print_sep "$p2"
720
+
721
+ local plist="$HOME/Library/LaunchAgents/${label}.plist"
722
+ local svc_installed="0"
723
+ [[ -f "$plist" ]] && svc_installed="1"
724
+
725
+ if [[ "$stack_name" == "main" ]]; then
726
+ local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
727
+ if [[ "$svc_installed" == "1" ]]; then
728
+ # Status-aware: only show start/stop based on whether the stack is running.
729
+ if [[ "${MAIN_LEVEL:-}" == "red" ]]; then
730
+ print_item "$p2" "Start (service) | bash=$PNPM_BIN param1=service:start dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
731
+ else
732
+ print_item "$p2" "Stop (service) | bash=$PNPM_BIN param1=service:stop dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
733
+ fi
734
+ print_item "$p2" "Restart (service) | bash=$PNPM_BIN param1=service:restart dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
735
+ else
736
+ if [[ "${MAIN_LEVEL:-}" == "red" ]]; then
737
+ print_item "$p2" "Start (foreground) | bash=$PNPM_TERM param1=start dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
738
+ else
739
+ print_item "$p2" "Stop (kill port listeners) | bash=$PNPM_TERM param1=stack:fix dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
740
+ fi
741
+ fi
742
+ print_item "$p2" "Dev mode | bash=$PNPM_TERM param1=dev dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
743
+ print_item "$p2" "Build UI | bash=$PNPM_TERM param1=build dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
744
+ print_item "$p2" "Doctor | bash=$PNPM_TERM param1=stack:doctor dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
745
+ return
746
+ fi
747
+
748
+ local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
749
+ if [[ "$svc_installed" == "1" ]]; then
750
+ # Status-aware: only show start/stop based on whether the stack is running.
751
+ if [[ "$STACK_LEVEL" == "red" ]]; then
752
+ print_item "$p2" "Start (service) | bash=$PNPM_BIN param1=stack param2=service:start param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
753
+ else
754
+ print_item "$p2" "Stop (service) | bash=$PNPM_BIN param1=stack param2=service:stop param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
755
+ fi
756
+ print_item "$p2" "Restart (service) | bash=$PNPM_BIN param1=stack param2=service:restart param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
757
+ else
758
+ if [[ "$STACK_LEVEL" == "red" ]]; then
759
+ print_item "$p2" "Start (foreground) | bash=$PNPM_TERM param1=stack param2=start param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
760
+ else
761
+ print_item "$p2" "Stop (kill port listeners) | bash=$PNPM_TERM param1=stack param2=fix param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
762
+ fi
763
+ fi
764
+ print_item "$p2" "Dev mode | bash=$PNPM_TERM param1=stack param2=dev param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
765
+ print_item "$p2" "Build UI | bash=$PNPM_TERM param1=stack param2=build param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
766
+ print_item "$p2" "Doctor | bash=$PNPM_TERM param1=stack param2=doctor param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
767
+ print_item "$p2" "Edit stack (interactive) | bash=$PNPM_TERM param1=stack param2=edit param3=$stack_name param4=--interactive dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
768
+ print_item "$p2" "Select worktrees (interactive) | bash=$PNPM_TERM param1=stack param2=wt param3=$stack_name param4=-- param5=use param6=--interactive dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
769
+
770
+ local pr_helper="$HAPPY_LOCAL_DIR/extras/swiftbar/wt-pr.sh"
771
+ if [[ -x "$pr_helper" ]]; then
772
+ print_item "$p2" "PR worktree into this stack (prompt) | bash=$pr_helper param1=_prompt_ param2=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
773
+ fi
774
+ }