happy-stacks 0.1.0 → 0.2.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 (95) hide show
  1. package/README.md +130 -74
  2. package/bin/happys.mjs +140 -9
  3. package/docs/edison.md +381 -0
  4. package/docs/happy-development.md +733 -0
  5. package/docs/menubar.md +54 -0
  6. package/docs/paths-and-env.md +141 -0
  7. package/docs/server-flavors.md +61 -2
  8. package/docs/stacks.md +55 -4
  9. package/extras/swiftbar/auth-login.sh +10 -7
  10. package/extras/swiftbar/git-cache-refresh.sh +130 -0
  11. package/extras/swiftbar/happy-stacks.5s.sh +175 -83
  12. package/extras/swiftbar/happys-term.sh +128 -0
  13. package/extras/swiftbar/happys.sh +35 -0
  14. package/extras/swiftbar/install.sh +99 -13
  15. package/extras/swiftbar/lib/git.sh +309 -1
  16. package/extras/swiftbar/lib/icons.sh +2 -2
  17. package/extras/swiftbar/lib/render.sh +279 -132
  18. package/extras/swiftbar/lib/system.sh +64 -10
  19. package/extras/swiftbar/lib/utils.sh +469 -10
  20. package/extras/swiftbar/pnpm-term.sh +2 -122
  21. package/extras/swiftbar/pnpm.sh +4 -14
  22. package/extras/swiftbar/set-interval.sh +10 -5
  23. package/extras/swiftbar/set-server-flavor.sh +19 -10
  24. package/extras/swiftbar/wt-pr.sh +10 -3
  25. package/package.json +2 -1
  26. package/scripts/auth.mjs +833 -14
  27. package/scripts/build.mjs +24 -4
  28. package/scripts/cli-link.mjs +3 -3
  29. package/scripts/completion.mjs +15 -8
  30. package/scripts/daemon.mjs +200 -23
  31. package/scripts/dev.mjs +230 -57
  32. package/scripts/doctor.mjs +26 -21
  33. package/scripts/edison.mjs +1828 -0
  34. package/scripts/happy.mjs +3 -7
  35. package/scripts/init.mjs +275 -46
  36. package/scripts/install.mjs +14 -8
  37. package/scripts/lint.mjs +145 -0
  38. package/scripts/menubar.mjs +81 -8
  39. package/scripts/migrate.mjs +302 -0
  40. package/scripts/mobile.mjs +59 -21
  41. package/scripts/run.mjs +222 -43
  42. package/scripts/self.mjs +3 -7
  43. package/scripts/server_flavor.mjs +3 -3
  44. package/scripts/service.mjs +190 -38
  45. package/scripts/setup.mjs +790 -0
  46. package/scripts/setup_pr.mjs +182 -0
  47. package/scripts/stack.mjs +2273 -92
  48. package/scripts/stop.mjs +160 -0
  49. package/scripts/tailscale.mjs +164 -23
  50. package/scripts/test.mjs +144 -0
  51. package/scripts/tui.mjs +556 -0
  52. package/scripts/typecheck.mjs +145 -0
  53. package/scripts/ui_gateway.mjs +248 -0
  54. package/scripts/uninstall.mjs +21 -13
  55. package/scripts/utils/auth_files.mjs +58 -0
  56. package/scripts/utils/auth_login_ux.mjs +76 -0
  57. package/scripts/utils/auth_sources.mjs +12 -0
  58. package/scripts/utils/browser.mjs +22 -0
  59. package/scripts/utils/canonical_home.mjs +20 -0
  60. package/scripts/utils/{cli_registry.mjs → cli/cli_registry.mjs} +71 -0
  61. package/scripts/utils/{wizard.mjs → cli/wizard.mjs} +1 -1
  62. package/scripts/utils/config.mjs +13 -1
  63. package/scripts/utils/dev_auth_key.mjs +169 -0
  64. package/scripts/utils/dev_daemon.mjs +104 -0
  65. package/scripts/utils/dev_expo_web.mjs +112 -0
  66. package/scripts/utils/dev_server.mjs +183 -0
  67. package/scripts/utils/env.mjs +94 -23
  68. package/scripts/utils/env_file.mjs +36 -0
  69. package/scripts/utils/expo.mjs +96 -0
  70. package/scripts/utils/handy_master_secret.mjs +94 -0
  71. package/scripts/utils/happy_server_infra.mjs +484 -0
  72. package/scripts/utils/localhost_host.mjs +17 -0
  73. package/scripts/utils/ownership.mjs +135 -0
  74. package/scripts/utils/paths.mjs +5 -2
  75. package/scripts/utils/pm.mjs +132 -22
  76. package/scripts/utils/ports.mjs +51 -13
  77. package/scripts/utils/proc.mjs +75 -7
  78. package/scripts/utils/runtime.mjs +1 -3
  79. package/scripts/utils/sandbox.mjs +14 -0
  80. package/scripts/utils/server.mjs +61 -0
  81. package/scripts/utils/server_port.mjs +9 -0
  82. package/scripts/utils/server_urls.mjs +54 -0
  83. package/scripts/utils/stack_context.mjs +23 -0
  84. package/scripts/utils/stack_runtime_state.mjs +104 -0
  85. package/scripts/utils/stack_startup.mjs +208 -0
  86. package/scripts/utils/stack_stop.mjs +255 -0
  87. package/scripts/utils/stacks.mjs +38 -0
  88. package/scripts/utils/validate.mjs +42 -1
  89. package/scripts/utils/watch.mjs +63 -0
  90. package/scripts/utils/worktrees.mjs +57 -1
  91. package/scripts/where.mjs +14 -7
  92. package/scripts/worktrees.mjs +135 -15
  93. /package/scripts/utils/{args.mjs → cli/args.mjs} +0 -0
  94. /package/scripts/utils/{cli.mjs → cli/cli.mjs} +0 -0
  95. /package/scripts/utils/{smoke_help.mjs → cli/smoke_help.mjs} +0 -0
@@ -21,9 +21,16 @@ level_from_server_daemon() {
21
21
 
22
22
  color_for_level() {
23
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"
24
+ if [[ "$level" == "green" ]]; then echo "#16a34a"; return; fi
25
+ if [[ "$level" == "orange" ]]; then echo "#f59e0b"; return; fi
26
+ echo "#e74c3c"
27
+ }
28
+
29
+ sfconfig_for_level() {
30
+ local level="$1"
31
+ local color="$(color_for_level "$level")"
32
+ # sfconfig SFSymbol configuration Configures Rendering Mode for sfimage. Accepts a json encoded as base64, example json {"renderingMode":"Palette", "colors":["red","blue"], "scale": "large", "weight": "bold"}. Original issue #354
33
+ echo "{\"colors\":[\"$color\"], \"scale\": \"small\"}" | base64 -b 0
27
34
  }
28
35
 
29
36
  sf_for_level() {
@@ -33,6 +40,13 @@ sf_for_level() {
33
40
  echo "xmark.circle.fill"
34
41
  }
35
42
 
43
+ sf_suffix_for_level() {
44
+ local level="$1"
45
+ if [[ "$level" == "green" ]]; then echo "badge.checkmark"; return; fi
46
+ if [[ "$level" == "orange" ]]; then echo "trianglebadge.exclamationmark"; return; fi
47
+ echo "badge.xmark"
48
+ }
49
+
36
50
  print_item() {
37
51
  local prefix="$1"
38
52
  shift
@@ -60,12 +74,16 @@ render_component_server() {
60
74
 
61
75
  local label="Server (${server_component})"
62
76
  local sf="$(sf_for_level "$level")"
63
- local color="$(color_for_level "$level")"
64
- print_item "$prefix" "$label | sfimage=$sf color=$color"
77
+ local sfconfig="$(sfconfig_for_level "$level")"
78
+ print_item "$prefix" "$label | sfimage=$sf sfconfig=$sfconfig"
65
79
 
66
80
  local p2="${prefix}--"
67
81
  print_item "$p2" "Status: $server_status"
68
- print_item "$p2" "Internal: http://127.0.0.1:${port}"
82
+ if [[ -n "$port" ]]; then
83
+ print_item "$p2" "Internal: http://127.0.0.1:${port}"
84
+ else
85
+ print_item "$p2" "Port: ephemeral (allocated at start time)"
86
+ fi
69
87
  if [[ -n "$server_pid" ]]; then
70
88
  if [[ -n "$server_metrics" ]]; then
71
89
  local cpu mem etime
@@ -77,20 +95,24 @@ render_component_server() {
77
95
  print_item "$p2" "PID: ${server_pid}"
78
96
  fi
79
97
  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"
98
+ if [[ -n "$port" ]]; then
99
+ print_item "$p2" "Open UI (local) | href=http://localhost:${port}/"
100
+ print_item "$p2" "Open Health | href=http://127.0.0.1:${port}/health"
101
+ fi
82
102
  if [[ -n "$tailscale_url" ]]; then
83
103
  print_item "$p2" "Open UI (Tailscale) | href=$tailscale_url"
84
104
  fi
85
105
 
86
106
  # Start/stop shortcuts (so you can control from the Server submenu too).
87
107
  if [[ -n "$PNPM_BIN" ]]; then
88
- local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
108
+ local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/happys-term.sh"
89
109
  local plist=""
90
110
  local svc_installed="0"
91
- if [[ -n "$launch_label" ]]; then
92
- plist="$HOME/Library/LaunchAgents/${launch_label}.plist"
93
- [[ -f "$plist" ]] && svc_installed="1"
111
+ if ! swiftbar_is_sandboxed; then
112
+ if [[ -n "$launch_label" ]]; then
113
+ plist="$HOME/Library/LaunchAgents/${launch_label}.plist"
114
+ [[ -f "$plist" ]] && svc_installed="1"
115
+ fi
94
116
  fi
95
117
 
96
118
  print_sep "$p2"
@@ -104,7 +126,7 @@ render_component_server() {
104
126
  print_item "$p2" "Restart stack (service) | bash=$PNPM_BIN param1=service:restart dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
105
127
  else
106
128
  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"
129
+ print_item "$p2" "Stop stack | bash=$PNPM_BIN param1=stack param2=stop param3=main dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
108
130
  else
109
131
  print_item "$p2" "Start stack (foreground) | bash=$PNPM_TERM param1=start dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
110
132
  fi
@@ -119,7 +141,7 @@ render_component_server() {
119
141
  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
142
  else
121
143
  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"
144
+ print_item "$p2" "Stop stack | bash=$PNPM_BIN param1=stack param2=stop param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
123
145
  else
124
146
  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
147
  fi
@@ -130,7 +152,7 @@ render_component_server() {
130
152
  # Flavor switching (status-aware: only show switching to the other option).
131
153
  local helper="$HAPPY_LOCAL_DIR/extras/swiftbar/set-server-flavor.sh"
132
154
  if [[ -n "$PNPM_BIN" ]]; then
133
- local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
155
+ local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/happys-term.sh"
134
156
  print_sep "$p2"
135
157
  if [[ "$server_component" == "happy-server" ]]; then
136
158
  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"
@@ -159,9 +181,10 @@ render_component_daemon() {
159
181
  if [[ "$daemon_status" == "running" ]]; then level="green"; fi
160
182
  if [[ "$daemon_status" == "running-no-http" || "$daemon_status" == "stale" || "$daemon_status" == "auth_required" || "$daemon_status" == "starting" ]]; then level="orange"; fi
161
183
 
184
+ local sfconfig="$(sfconfig_for_level "$level")"
185
+
162
186
  local sf="$(sf_for_level "$level")"
163
- local color="$(color_for_level "$level")"
164
- print_item "$prefix" "Daemon | sfimage=$sf color=$color"
187
+ print_item "$prefix" "Daemon | sfimage=$sf sfconfig=$sfconfig"
165
188
 
166
189
  local p2="${prefix}--"
167
190
  print_item "$p2" "Status: $daemon_status"
@@ -187,28 +210,29 @@ render_component_daemon() {
187
210
  # Provide a direct "fix" action for the common first-run problem under launchd.
188
211
  local auth_helper="$HAPPY_LOCAL_DIR/extras/swiftbar/auth-login.sh"
189
212
  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)"
213
+ local webapp_url="http://localhost:$(resolve_main_port)"
193
214
  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"
215
+ print_item "$p2" "Auth login (opens browser) | bash=$auth_helper param1=main dir=$HAPPY_LOCAL_DIR terminal=false refresh=false"
195
216
  else
196
217
  # 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"
218
+ local env_file
219
+ env_file="$(resolve_stack_env_file "$stack_name")"
198
220
  local port
199
- port="$(dotenv_get "$env_file" "HAPPY_LOCAL_SERVER_PORT")"
221
+ port="$(dotenv_get "$env_file" "HAPPY_STACKS_SERVER_PORT")"
222
+ [[ -z "$port" ]] && port="$(dotenv_get "$env_file" "HAPPY_LOCAL_SERVER_PORT")"
200
223
  [[ -z "$port" ]] && port="$(resolve_main_port)"
201
224
  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"
225
+ webapp_url="http://localhost:${port}"
226
+ print_item "$p2" "Auth login (opens browser) | bash=$auth_helper param1=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=false"
205
227
  fi
206
228
  print_sep "$p2"
207
229
  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"
230
+ if ! swiftbar_is_sandboxed; then
231
+ if [[ "$stack_name" == "main" ]]; then
232
+ print_item "$p2" "Restart stack (service) | bash=$PNPM_BIN param1=service:restart dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
233
+ else
234
+ 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"
235
+ fi
212
236
  fi
213
237
  fi
214
238
  }
@@ -222,13 +246,20 @@ render_component_autostart() {
222
246
  local autostart_metrics="$6"
223
247
  local logs_dir="$7"
224
248
 
249
+ if swiftbar_is_sandboxed; then
250
+ print_item "$prefix" "Autostart | sfimage=exclamationmark.triangle sfconfig=light"
251
+ local p2="${prefix}--"
252
+ print_item "$p2" "Status: disabled in sandbox"
253
+ return
254
+ fi
255
+
225
256
  local level="red"
226
257
  if [[ "$launchagent_status" == "loaded" ]]; then level="green"; fi
227
258
  if [[ "$launchagent_status" == "unloaded" ]]; then level="orange"; fi
228
259
 
229
260
  local sf="$(sf_for_level "$level")"
230
- local color="$(color_for_level "$level")"
231
- print_item "$prefix" "Autostart | sfimage=$sf color=$color"
261
+ local sfconfig="$(sfconfig_for_level "$level")"
262
+ print_item "$prefix" "Autostart | sfimage=$sf sfconfig=$sfconfig"
232
263
 
233
264
  local p2="${prefix}--"
234
265
  print_item "$p2" "Status: $launchagent_status"
@@ -292,8 +323,8 @@ render_component_tailscale() {
292
323
  [[ -n "$tailscale_url" ]] && level="green"
293
324
 
294
325
  local sf="$(sf_for_level "$level")"
295
- local color="$(color_for_level "$level")"
296
- print_item "$prefix" "Tailscale | sfimage=$sf color=$color"
326
+ local sfconfig="$(sfconfig_for_level "$level")"
327
+ print_item "$prefix" "Tailscale | sfimage=$sf sfconfig=$sfconfig"
297
328
 
298
329
  local p2="${prefix}--"
299
330
  if [[ -n "$tailscale_url" ]]; then
@@ -306,13 +337,18 @@ render_component_tailscale() {
306
337
  print_item "$p2" "Status: not configured / unknown"
307
338
  fi
308
339
 
340
+ # Tailscale Serve is global machine state; never offer enable/disable actions in sandbox mode.
341
+ if swiftbar_is_sandboxed; then
342
+ return
343
+ fi
344
+
309
345
  if [[ -z "$PNPM_BIN" ]]; then
310
346
  return
311
347
  fi
312
348
  print_sep "$p2"
313
349
 
314
350
  if [[ "$stack_name" == "main" ]]; then
315
- local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
351
+ local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/happys-term.sh"
316
352
  print_item "$p2" "Tailscale status | bash=$PNPM_TERM param1=tailscale:status dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
317
353
  if [[ -n "$tailscale_url" ]]; then
318
354
  print_item "$p2" "Disable Tailscale Serve | bash=$PNPM_BIN param1=tailscale:disable dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
@@ -323,7 +359,7 @@ render_component_tailscale() {
323
359
  return
324
360
  fi
325
361
 
326
- local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
362
+ local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/happys-term.sh"
327
363
  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
364
  if [[ -n "$tailscale_url" ]]; then
329
365
  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"
@@ -344,8 +380,12 @@ render_component_repo() {
344
380
  local stack_name="$4"
345
381
  local env_file="$5"
346
382
 
383
+ local t0 t1
384
+ t0="$(swiftbar_now_ms 2>/dev/null || echo 0)"
385
+
347
386
  local active_dir=""
348
- if [[ "$context" == "stack" && -n "$env_file" ]]; then
387
+ # If we have an env file for the current context, prefer it (stack env is authoritative).
388
+ if [[ -n "$env_file" && -f "$env_file" ]]; then
349
389
  active_dir="$(resolve_component_dir_from_env_file "$env_file" "$component")"
350
390
  else
351
391
  active_dir="$(resolve_component_dir_from_env "$component")"
@@ -353,25 +393,51 @@ render_component_repo() {
353
393
 
354
394
  local level="red"
355
395
  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)"
396
+
397
+ local git_mode
398
+ git_mode="$(git_cache_mode)"
399
+
400
+ local stale="0"
401
+ local meta="" info="" wts=""
402
+ if [[ "$git_mode" == "cached" ]]; then
403
+ # Never refresh synchronously during menu render.
404
+ IFS=$'\t' read -r meta info wts stale <<<"$(git_cache_load_or_refresh "$context" "$stack_name" "$component" "$active_dir" "0")"
405
+ fi
406
+
407
+ local status="missing"
408
+ local dirty="" ahead="" behind="" wt_count=""
409
+ local branch="" head="" upstream=""
410
+ local main_branch="" main_upstream="" main_ahead="" main_behind=""
411
+ local oref="" o_ahead="" o_behind="" uref="" u_ahead="" u_behind=""
412
+
413
+ if [[ "$git_mode" == "cached" && -f "$info" ]]; then
414
+ IFS=$'\t' read -r status _ad branch head upstream dirty ahead behind main_branch main_upstream main_ahead main_behind oref o_ahead o_behind uref u_ahead u_behind wt_count <"$info" || true
415
+ elif [[ "$git_mode" == "live" ]]; then
416
+ # live mode only: do git work on every refresh
417
+ if is_git_repo "$active_dir"; then
418
+ status="ok"
419
+ dirty="$(git_dirty_flag "$active_dir")"
420
+ local ab
421
+ ab="$(git_ahead_behind "$active_dir")"
422
+ if [[ -n "$ab" ]]; then
423
+ ahead="$(echo "$ab" | cut -d'|' -f1)"
424
+ behind="$(echo "$ab" | cut -d'|' -f2)"
425
+ fi
365
426
  fi
427
+ fi
366
428
 
429
+ if [[ "$status" == "ok" ]]; then
430
+ detail="ok"
367
431
  if [[ "$dirty" == "dirty" ]] || [[ -n "$behind" && "$behind" != "0" ]]; then
368
432
  level="orange"
369
433
  else
370
434
  level="green"
371
435
  fi
372
- detail="ok"
373
436
  fi
374
437
 
438
+ t1="$(swiftbar_now_ms 2>/dev/null || echo 0)"
439
+ swiftbar_profile_log "time" "label=render_component_repo" "component=${component}" "context=${context}" "ms=$((t1 - t0))" "detail=${detail}"
440
+
375
441
  local sf color
376
442
  sf="$(sf_for_level "$level")"
377
443
  color="$(color_for_level "$level")"
@@ -380,31 +446,50 @@ render_component_repo() {
380
446
  local p2="${prefix}--"
381
447
  print_item "$p2" "Dir: $(shorten_path "$active_dir" 52)"
382
448
  if [[ "$detail" != "ok" ]]; then
383
- print_item "$p2" "Status: not a git repo / missing"
449
+ if [[ "$git_mode" == "cached" ]]; then
450
+ print_item "$p2" "Status: git cache missing (or not a git repo)"
451
+ local refresh="$HAPPY_LOCAL_DIR/extras/swiftbar/git-cache-refresh.sh"
452
+ if [[ -x "$refresh" ]]; then
453
+ print_sep "$p2"
454
+ if [[ "$context" == "stack" && -n "$stack_name" ]]; then
455
+ print_item "$p2" "Refresh Git cache (this stack) | bash=$refresh param1=stack param2=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
456
+ else
457
+ print_item "$p2" "Refresh Git cache (main) | bash=$refresh param1=main dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
458
+ fi
459
+ fi
460
+ else
461
+ print_item "$p2" "Status: not a git repo / missing"
462
+ fi
384
463
  if [[ -n "$PNPM_BIN" ]]; then
385
464
  print_sep "$p2"
386
- local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
465
+ local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/happys-term.sh"
387
466
  print_item "$p2" "Bootstrap (clone missing components) | bash=$PNPM_TERM param1=bootstrap dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
388
467
  fi
389
468
  return
390
469
  fi
391
470
 
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)"
471
+ # Cache status + refresh actions
472
+ if [[ "$git_mode" == "cached" ]]; then
473
+ local age=""
474
+ age="$(git_cache_age_sec "$meta")"
475
+ if [[ -n "$age" ]]; then
476
+ if [[ "$stale" == "1" ]]; then
477
+ print_item "$p2" "Git cache: stale (${age}s old) | color=$YELLOW"
478
+ else
479
+ print_item "$p2" "Git cache: fresh (${age}s old) | color=$GRAY"
480
+ fi
481
+ fi
482
+ local refresh="$HAPPY_LOCAL_DIR/extras/swiftbar/git-cache-refresh.sh"
483
+ if [[ -x "$refresh" ]]; then
484
+ print_sep "$p2"
485
+ print_item "$p2" "Refresh Git cache (this component) | bash=$refresh param1=component param2=$context param3=$stack_name param4=$component dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
486
+ if [[ "$context" == "stack" && -n "$stack_name" ]]; then
487
+ print_item "$p2" "Refresh Git cache (this stack) | bash=$refresh param1=stack param2=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
488
+ fi
489
+ fi
406
490
  fi
407
491
 
492
+ print_sep "$p2"
408
493
  print_item "$p2" "HEAD: ${branch:-"(unknown)"} ${head:+($head)}"
409
494
  print_item "$p2" "Upstream: ${upstream:-"(none)"}"
410
495
  if [[ -n "$ahead" && -n "$behind" ]]; then
@@ -412,31 +497,22 @@ render_component_repo() {
412
497
  fi
413
498
  print_item "$p2" "Working tree: ${dirty}"
414
499
 
415
- local main_branch main_upstream main_ab
416
- main_branch="$(git_main_branch_name "$active_dir")"
417
500
  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
501
  if [[ -n "$main_upstream" ]]; then
421
502
  print_item "$p2" "Main: ${main_branch} → ${main_upstream}"
422
503
  else
423
504
  print_item "$p2" "Main: ${main_branch} → (no upstream)"
424
505
  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)"
506
+ if [[ -n "$main_ahead" && -n "$main_behind" ]]; then
507
+ print_item "$p2" "Main ahead/behind: ${main_ahead}/${main_behind}"
427
508
  fi
428
509
 
429
510
  # Always show comparisons against origin/* and upstream/* when those remote refs exist.
430
511
  # (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
512
  if [[ -n "$oref" ]]; then
435
513
  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)"
514
+ if [[ -n "$o_ahead" && -n "$o_behind" ]]; then
515
+ print_item "$p2" "Origin: ${oref_short} ahead/behind: ${o_ahead}/${o_behind}"
440
516
  else
441
517
  print_item "$p2" "Origin: ${oref_short}"
442
518
  fi
@@ -445,10 +521,8 @@ render_component_repo() {
445
521
  fi
446
522
  if [[ -n "$uref" ]]; then
447
523
  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)"
524
+ if [[ -n "$u_ahead" && -n "$u_behind" ]]; then
525
+ print_item "$p2" "Upstream: ${uref_short} ahead/behind: ${u_ahead}/${u_behind}"
452
526
  else
453
527
  print_item "$p2" "Upstream: ${uref_short}"
454
528
  fi
@@ -457,15 +531,16 @@ render_component_repo() {
457
531
  fi
458
532
  fi
459
533
 
460
- local wt_count
461
- wt_count="$(git_worktree_count "$active_dir")"
534
+ local wt_count
535
+ # If cache didn't populate wt_count, fall back to empty string.
536
+ wt_count="${wt_count:-}"
462
537
 
463
538
  # Quick actions
464
539
  print_sep "$p2"
465
540
  print_item "$p2" "Open folder | bash=/usr/bin/open param1='$active_dir' terminal=false"
466
541
 
467
542
  if [[ -n "$PNPM_BIN" ]]; then
468
- local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
543
+ local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/happys-term.sh"
469
544
  # Run via stack wrappers when in a stack context so env-file stays authoritative.
470
545
  if [[ "$context" == "stack" && -n "$stack_name" ]]; then
471
546
  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"
@@ -518,11 +593,19 @@ render_component_repo() {
518
593
  print_item "$p2" "$wt_label"
519
594
  local p3="${p2}--"
520
595
  local tsv
521
- tsv="$(git_worktrees_tsv "$active_dir" 2>/dev/null || true)"
596
+ if [[ "$git_mode" == "cached" && -f "$wts" ]]; then
597
+ tsv="$(cat "$wts" 2>/dev/null || true)"
598
+ else
599
+ tsv="$(git_worktrees_tsv "$active_dir" 2>/dev/null || true)"
600
+ fi
522
601
  if [[ -z "$tsv" ]]; then
523
602
  print_item "$p3" "No worktrees found | color=$GRAY"
524
603
  else
525
- local root="$(resolve_components_dir)/.worktrees/$component/"
604
+ # Worktrees live alongside the component checkout at: <componentsRoot>/.worktrees/<component>/...
605
+ local components_root default_path root
606
+ components_root="$(dirname "$active_dir")"
607
+ default_path="$components_root/$component"
608
+ root="$components_root/.worktrees/$component/"
526
609
  local shown=0
527
610
  while IFS=$'\t' read -r wt_path wt_branchref; do
528
611
  [[ -n "$wt_path" ]] || continue
@@ -534,11 +617,13 @@ render_component_repo() {
534
617
 
535
618
  local label=""
536
619
  local spec=""
537
- if [[ "$wt_path" == "$root"* ]]; then
620
+ if [[ "$wt_path" == "$default_path" ]]; then
621
+ spec="default"
622
+ label="default"
623
+ elif [[ "$wt_path" == "$root"* ]]; then
538
624
  spec="${wt_path#"$root"}"
539
625
  label="$spec"
540
626
  else
541
- spec="$wt_path"
542
627
  label="$(shorten_path "$wt_path" 52)"
543
628
  fi
544
629
 
@@ -549,18 +634,24 @@ render_component_repo() {
549
634
  label="(active) $label"
550
635
  fi
551
636
 
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"
637
+ print_item "$p3" "$label"
638
+
639
+ # Only show "use" actions when we can express the worktree as a spec (default or under .worktrees).
640
+ # Some git worktrees can exist outside our managed tree; for those we only offer open/shell actions.
641
+ if [[ -n "$spec" ]]; then
642
+ if [[ "$context" == "stack" && -n "$stack_name" ]]; then
643
+ 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"
644
+ 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"
645
+ 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"
646
+ 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"
647
+ else
648
+ print_item "${p3}--" "Use (main) | bash=$PNPM_BIN param1=wt param2=use param3=$component param4=$spec dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
649
+ 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"
650
+ print_item "${p3}--" "Open in VS Code | bash=$PNPM_BIN param1=wt param2=code param3=$component param4=$spec dir=$HAPPY_LOCAL_DIR terminal=false"
651
+ print_item "${p3}--" "Open in Cursor | bash=$PNPM_BIN param1=wt param2=cursor param3=$component param4=$spec dir=$HAPPY_LOCAL_DIR terminal=false"
652
+ fi
558
653
  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"
654
+ print_item "${p3}--" "Open folder | bash=/usr/bin/open param1='$wt_path' terminal=false"
564
655
  fi
565
656
  done <<<"$tsv"
566
657
  fi
@@ -574,26 +665,55 @@ render_components_menu() {
574
665
  local stack_name="$3"
575
666
  local env_file="$4"
576
667
 
668
+ local t0 t1
669
+ t0="$(swiftbar_now_ms 2>/dev/null || echo 0)"
670
+
577
671
  print_item "$prefix" "Components | sfimage=cube"
578
672
  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
673
+
674
+ # Background auto-refresh: keep menu refresh snappy but update git cache when TTL expires.
675
+ if [[ "$(git_cache_mode)" == "cached" ]]; then
676
+ local scope
677
+ scope="$(git_cache_auto_refresh_scope)"
678
+ local refresh="$HAPPY_LOCAL_DIR/extras/swiftbar/git-cache-refresh.sh"
679
+ if [[ -x "$refresh" ]]; then
680
+ if [[ "$scope" == "all" ]]; then
681
+ git_cache_maybe_refresh_async "all" "$refresh" all
682
+ elif [[ "$scope" == "main" && "$context" == "main" ]]; then
683
+ git_cache_maybe_refresh_async "main" "$refresh" main
684
+ fi
685
+ fi
584
686
  fi
585
687
 
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"
688
+ # Git cache controls (to keep the menu refresh fast while retaining rich inline worktrees UI).
689
+ local refresh="$HAPPY_LOCAL_DIR/extras/swiftbar/git-cache-refresh.sh"
690
+ if [[ -f "$refresh" ]]; then
691
+ local mode ttl
692
+ mode="$(git_cache_mode)"
693
+ ttl="$(git_cache_ttl_sec)"
694
+ print_item "$p2" "Git cache | sfimage=arrow.triangle.2.circlepath"
695
+ local p3="${p2}--"
696
+ print_item "$p3" "Mode: ${mode} (default: cached)"
697
+ print_item "$p3" "TTL: ${ttl}s (set HAPPY_STACKS_SWIFTBAR_GIT_TTL_SEC)"
698
+ print_sep "$p3"
699
+ if [[ "$context" == "main" ]]; then
700
+ print_item "$p3" "Refresh now (main components) | bash=$refresh param1=main dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
701
+ print_item "$p3" "Refresh now (all stacks/components) | bash=$refresh param1=all dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
702
+ else
703
+ print_item "$p3" "Refresh now (this stack) | bash=$refresh param1=stack param2=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
592
704
  fi
593
- done
594
- if [[ "$any" == "0" ]]; then
595
- print_item "$p2" "No components found under components/ | color=$GRAY"
705
+ print_sep "$p2"
596
706
  fi
707
+
708
+ # Always render the known components using the resolved component dirs (env file → env.local/.env → fallback),
709
+ # instead of assuming they live under `~/.happy-stacks/workspace/components`.
710
+ for c in happy happy-cli happy-server-light happy-server; do
711
+ render_component_repo "$p2" "$c" "$context" "$stack_name" "$env_file"
712
+ print_sep "$p2"
713
+ done
714
+
715
+ t1="$(swiftbar_now_ms 2>/dev/null || echo 0)"
716
+ swiftbar_profile_log "time" "label=render_components_menu" "context=${context}" "stack=${stack_name}" "ms=$((t1 - t0))"
597
717
  }
598
718
 
599
719
  render_stack_overview_item() {
@@ -644,14 +764,20 @@ collect_stack_status() {
644
764
  daemon_uptime="$(get_daemon_uptime "$cli_home_dir")"
645
765
  last_heartbeat="$(get_last_heartbeat "$cli_home_dir")"
646
766
 
647
- local plist_path="$HOME/Library/LaunchAgents/${label}.plist"
648
767
  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")"
768
+ if swiftbar_is_sandboxed; then
769
+ launchagent_status="sandbox_disabled"
770
+ autostart_pid=""
771
+ autostart_metrics=""
772
+ else
773
+ local plist_path="$HOME/Library/LaunchAgents/${label}.plist"
774
+ launchagent_status="$(check_launchagent_status "$label" "$plist_path")"
775
+ autostart_pid=""
776
+ autostart_metrics=""
777
+ if [[ "$launchagent_status" != "not_installed" ]]; then
778
+ autostart_pid="$(launchagent_pid_for_label "$label")"
779
+ autostart_metrics="$(get_process_metrics "$autostart_pid")"
780
+ fi
655
781
  fi
656
782
 
657
783
  local level
@@ -688,10 +814,21 @@ render_stack_info() {
688
814
  local tailscale_url="$9" # optional
689
815
 
690
816
  # Avoid low-contrast gray in the main list; keep it readable in both light/dark.
691
- print_item "$prefix" "Stack details | sfimage=info.circle"
817
+ print_item "$prefix" "Stack details | sfimage=server.rack"
692
818
  local p2="${prefix}--"
693
819
  print_item "$p2" "Server component: ${server_component}"
694
- print_item "$p2" "Port: ${port}"
820
+ local pinned_port=""
821
+ if [[ -n "$env_file" && -f "$env_file" ]]; then
822
+ pinned_port="$(dotenv_get "$env_file" "HAPPY_STACKS_SERVER_PORT")"
823
+ [[ -z "$pinned_port" ]] && pinned_port="$(dotenv_get "$env_file" "HAPPY_LOCAL_SERVER_PORT")"
824
+ fi
825
+ local port_display="$port"
826
+ if [[ -z "$port_display" ]]; then
827
+ port_display="ephemeral (not running)"
828
+ elif [[ -z "$pinned_port" ]]; then
829
+ port_display="${port_display} (ephemeral)"
830
+ fi
831
+ print_item "$p2" "Port: ${port_display}"
695
832
  print_item "$p2" "Label: ${label}"
696
833
  [[ -n "$env_file" ]] && print_item "$p2" "Env: $(shorten_path "$env_file" 52)"
697
834
  [[ -n "$tailscale_url" ]] && print_item "$p2" "Tailscale: $(shorten_text "$tailscale_url" 52)"
@@ -718,12 +855,16 @@ render_stack_info() {
718
855
  fi
719
856
  print_sep "$p2"
720
857
 
721
- local plist="$HOME/Library/LaunchAgents/${label}.plist"
722
858
  local svc_installed="0"
723
- [[ -f "$plist" ]] && svc_installed="1"
859
+ if ! swiftbar_is_sandboxed; then
860
+ local plist="$HOME/Library/LaunchAgents/${label}.plist"
861
+ [[ -f "$plist" ]] && svc_installed="1"
862
+ fi
863
+ local menu_mode
864
+ menu_mode="$(resolve_menubar_mode)"
724
865
 
725
866
  if [[ "$stack_name" == "main" ]]; then
726
- local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
867
+ local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/happys-term.sh"
727
868
  if [[ "$svc_installed" == "1" ]]; then
728
869
  # Status-aware: only show start/stop based on whether the stack is running.
729
870
  if [[ "${MAIN_LEVEL:-}" == "red" ]]; then
@@ -736,16 +877,18 @@ render_stack_info() {
736
877
  if [[ "${MAIN_LEVEL:-}" == "red" ]]; then
737
878
  print_item "$p2" "Start (foreground) | bash=$PNPM_TERM param1=start dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
738
879
  else
739
- print_item "$p2" "Stop (kill port listeners) | bash=$PNPM_TERM param1=stack:fix dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
880
+ print_item "$p2" "Stop stack | bash=$PNPM_BIN param1=stack param2=stop param3=main dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
740
881
  fi
741
882
  fi
742
- print_item "$p2" "Dev mode | bash=$PNPM_TERM param1=dev dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
883
+ if [[ "$menu_mode" != "selfhost" ]]; then
884
+ print_item "$p2" "Dev mode | bash=$PNPM_TERM param1=dev dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
885
+ fi
743
886
  print_item "$p2" "Build UI | bash=$PNPM_TERM param1=build dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
744
887
  print_item "$p2" "Doctor | bash=$PNPM_TERM param1=stack:doctor dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
745
888
  return
746
889
  fi
747
890
 
748
- local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
891
+ local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/happys-term.sh"
749
892
  if [[ "$svc_installed" == "1" ]]; then
750
893
  # Status-aware: only show start/stop based on whether the stack is running.
751
894
  if [[ "$STACK_LEVEL" == "red" ]]; then
@@ -758,17 +901,21 @@ render_stack_info() {
758
901
  if [[ "$STACK_LEVEL" == "red" ]]; then
759
902
  print_item "$p2" "Start (foreground) | bash=$PNPM_TERM param1=stack param2=start param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
760
903
  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"
904
+ print_item "$p2" "Stop stack | bash=$PNPM_BIN param1=stack param2=stop param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
762
905
  fi
763
906
  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"
907
+ if [[ "$menu_mode" != "selfhost" ]]; then
908
+ print_item "$p2" "Dev mode | bash=$PNPM_TERM param1=stack param2=dev param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
909
+ fi
765
910
  print_item "$p2" "Build UI | bash=$PNPM_TERM param1=stack param2=build param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
766
911
  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"
912
+ if [[ "$menu_mode" != "selfhost" ]]; then
913
+ 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"
914
+ 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"
915
+ fi
769
916
 
770
917
  local pr_helper="$HAPPY_LOCAL_DIR/extras/swiftbar/wt-pr.sh"
771
- if [[ -x "$pr_helper" ]]; then
918
+ if [[ "$menu_mode" != "selfhost" && -x "$pr_helper" ]]; then
772
919
  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
920
  fi
774
921
  }