happy-stacks 0.1.2 → 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 (91) hide show
  1. package/README.md +121 -83
  2. package/bin/happys.mjs +70 -10
  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/stacks.md +39 -0
  8. package/extras/swiftbar/auth-login.sh +5 -2
  9. package/extras/swiftbar/git-cache-refresh.sh +130 -0
  10. package/extras/swiftbar/happy-stacks.5s.sh +131 -81
  11. package/extras/swiftbar/happys-term.sh +15 -38
  12. package/extras/swiftbar/happys.sh +15 -32
  13. package/extras/swiftbar/install.sh +99 -13
  14. package/extras/swiftbar/lib/git.sh +309 -1
  15. package/extras/swiftbar/lib/icons.sh +2 -2
  16. package/extras/swiftbar/lib/render.sh +209 -80
  17. package/extras/swiftbar/lib/system.sh +27 -4
  18. package/extras/swiftbar/lib/utils.sh +311 -28
  19. package/extras/swiftbar/pnpm.sh +2 -1
  20. package/extras/swiftbar/set-interval.sh +10 -5
  21. package/extras/swiftbar/set-server-flavor.sh +11 -2
  22. package/extras/swiftbar/wt-pr.sh +9 -2
  23. package/package.json +2 -1
  24. package/scripts/auth.mjs +560 -112
  25. package/scripts/build.mjs +24 -4
  26. package/scripts/cli-link.mjs +3 -3
  27. package/scripts/completion.mjs +15 -8
  28. package/scripts/daemon.mjs +130 -20
  29. package/scripts/dev.mjs +201 -133
  30. package/scripts/doctor.mjs +26 -21
  31. package/scripts/edison.mjs +1828 -0
  32. package/scripts/happy.mjs +3 -7
  33. package/scripts/init.mjs +43 -20
  34. package/scripts/install.mjs +14 -8
  35. package/scripts/lint.mjs +145 -0
  36. package/scripts/menubar.mjs +81 -8
  37. package/scripts/migrate.mjs +25 -15
  38. package/scripts/mobile.mjs +13 -7
  39. package/scripts/run.mjs +114 -27
  40. package/scripts/self.mjs +3 -7
  41. package/scripts/server_flavor.mjs +3 -3
  42. package/scripts/service.mjs +15 -2
  43. package/scripts/setup.mjs +790 -0
  44. package/scripts/setup_pr.mjs +182 -0
  45. package/scripts/stack.mjs +1792 -254
  46. package/scripts/stop.mjs +6 -3
  47. package/scripts/tailscale.mjs +17 -2
  48. package/scripts/test.mjs +144 -0
  49. package/scripts/tui.mjs +556 -0
  50. package/scripts/typecheck.mjs +2 -2
  51. package/scripts/ui_gateway.mjs +2 -2
  52. package/scripts/uninstall.mjs +18 -10
  53. package/scripts/utils/auth_files.mjs +58 -0
  54. package/scripts/utils/auth_login_ux.mjs +76 -0
  55. package/scripts/utils/auth_sources.mjs +12 -0
  56. package/scripts/utils/browser.mjs +22 -0
  57. package/scripts/utils/canonical_home.mjs +20 -0
  58. package/scripts/utils/{cli_registry.mjs → cli/cli_registry.mjs} +48 -0
  59. package/scripts/utils/{wizard.mjs → cli/wizard.mjs} +1 -1
  60. package/scripts/utils/config.mjs +6 -2
  61. package/scripts/utils/dev_auth_key.mjs +169 -0
  62. package/scripts/utils/dev_daemon.mjs +104 -0
  63. package/scripts/utils/dev_expo_web.mjs +112 -0
  64. package/scripts/utils/dev_server.mjs +183 -0
  65. package/scripts/utils/env.mjs +60 -11
  66. package/scripts/utils/env_file.mjs +36 -0
  67. package/scripts/utils/expo.mjs +4 -2
  68. package/scripts/utils/handy_master_secret.mjs +94 -0
  69. package/scripts/utils/happy_server_infra.mjs +100 -46
  70. package/scripts/utils/localhost_host.mjs +17 -0
  71. package/scripts/utils/ownership.mjs +135 -0
  72. package/scripts/utils/paths.mjs +5 -2
  73. package/scripts/utils/pm.mjs +121 -20
  74. package/scripts/utils/proc.mjs +29 -2
  75. package/scripts/utils/runtime.mjs +1 -3
  76. package/scripts/utils/sandbox.mjs +14 -0
  77. package/scripts/utils/server.mjs +24 -0
  78. package/scripts/utils/server_port.mjs +9 -0
  79. package/scripts/utils/server_urls.mjs +54 -0
  80. package/scripts/utils/stack_context.mjs +23 -0
  81. package/scripts/utils/stack_runtime_state.mjs +104 -0
  82. package/scripts/utils/stack_startup.mjs +208 -0
  83. package/scripts/utils/stack_stop.mjs +79 -30
  84. package/scripts/utils/stacks.mjs +38 -0
  85. package/scripts/utils/watch.mjs +63 -0
  86. package/scripts/utils/worktrees.mjs +57 -1
  87. package/scripts/where.mjs +14 -7
  88. package/scripts/worktrees.mjs +82 -8
  89. /package/scripts/utils/{args.mjs → cli/args.mjs} +0 -0
  90. /package/scripts/utils/{cli.mjs → cli/cli.mjs} +0 -0
  91. /package/scripts/utils/{smoke_help.mjs → cli/smoke_help.mjs} +0 -0
@@ -79,7 +79,11 @@ render_component_server() {
79
79
 
80
80
  local p2="${prefix}--"
81
81
  print_item "$p2" "Status: $server_status"
82
- 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
83
87
  if [[ -n "$server_pid" ]]; then
84
88
  if [[ -n "$server_metrics" ]]; then
85
89
  local cpu mem etime
@@ -91,8 +95,10 @@ render_component_server() {
91
95
  print_item "$p2" "PID: ${server_pid}"
92
96
  fi
93
97
  fi
94
- print_item "$p2" "Open UI (local) | href=http://localhost:${port}/"
95
- 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
96
102
  if [[ -n "$tailscale_url" ]]; then
97
103
  print_item "$p2" "Open UI (Tailscale) | href=$tailscale_url"
98
104
  fi
@@ -102,9 +108,11 @@ render_component_server() {
102
108
  local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/happys-term.sh"
103
109
  local plist=""
104
110
  local svc_installed="0"
105
- if [[ -n "$launch_label" ]]; then
106
- plist="$HOME/Library/LaunchAgents/${launch_label}.plist"
107
- [[ -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
108
116
  fi
109
117
 
110
118
  print_sep "$p2"
@@ -118,7 +126,7 @@ render_component_server() {
118
126
  print_item "$p2" "Restart stack (service) | bash=$PNPM_BIN param1=service:restart dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
119
127
  else
120
128
  if [[ "$server_status" == "running" ]]; then
121
- 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"
122
130
  else
123
131
  print_item "$p2" "Start stack (foreground) | bash=$PNPM_TERM param1=start dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
124
132
  fi
@@ -133,7 +141,7 @@ render_component_server() {
133
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"
134
142
  else
135
143
  if [[ "$server_status" == "running" ]]; then
136
- 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"
137
145
  else
138
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"
139
147
  fi
@@ -202,11 +210,9 @@ render_component_daemon() {
202
210
  # Provide a direct "fix" action for the common first-run problem under launchd.
203
211
  local auth_helper="$HAPPY_LOCAL_DIR/extras/swiftbar/auth-login.sh"
204
212
  local server_url="http://127.0.0.1:$(resolve_main_port)"
205
- local webapp_url
206
- webapp_url="$(get_tailscale_url)"
207
- [[ -z "$webapp_url" ]] && webapp_url="http://localhost:$(resolve_main_port)"
213
+ local webapp_url="http://localhost:$(resolve_main_port)"
208
214
  if [[ "$stack_name" == "main" ]]; then
209
- 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"
210
216
  else
211
217
  # For stacks, best-effort use the stack's configured port if available (fallback to main port).
212
218
  local env_file
@@ -216,16 +222,17 @@ render_component_daemon() {
216
222
  [[ -z "$port" ]] && port="$(dotenv_get "$env_file" "HAPPY_LOCAL_SERVER_PORT")"
217
223
  [[ -z "$port" ]] && port="$(resolve_main_port)"
218
224
  server_url="http://127.0.0.1:${port}"
219
- webapp_url="$(get_tailscale_url)"
220
- [[ -z "$webapp_url" ]] && webapp_url="http://localhost:${port}"
221
- print_item "$p2" "Auth login (opens browser) | bash=$auth_helper param1=$stack_name param2=$server_url param3=$webapp_url 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"
222
227
  fi
223
228
  print_sep "$p2"
224
229
  fi
225
- if [[ "$stack_name" == "main" ]]; then
226
- print_item "$p2" "Restart stack (service) | bash=$PNPM_BIN param1=service:restart dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
227
- else
228
- 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
229
236
  fi
230
237
  fi
231
238
  }
@@ -239,6 +246,13 @@ render_component_autostart() {
239
246
  local autostart_metrics="$6"
240
247
  local logs_dir="$7"
241
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
+
242
256
  local level="red"
243
257
  if [[ "$launchagent_status" == "loaded" ]]; then level="green"; fi
244
258
  if [[ "$launchagent_status" == "unloaded" ]]; then level="orange"; fi
@@ -323,6 +337,11 @@ render_component_tailscale() {
323
337
  print_item "$p2" "Status: not configured / unknown"
324
338
  fi
325
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
+
326
345
  if [[ -z "$PNPM_BIN" ]]; then
327
346
  return
328
347
  fi
@@ -361,6 +380,9 @@ render_component_repo() {
361
380
  local stack_name="$4"
362
381
  local env_file="$5"
363
382
 
383
+ local t0 t1
384
+ t0="$(swiftbar_now_ms 2>/dev/null || echo 0)"
385
+
364
386
  local active_dir=""
365
387
  # If we have an env file for the current context, prefer it (stack env is authoritative).
366
388
  if [[ -n "$env_file" && -f "$env_file" ]]; then
@@ -371,25 +393,51 @@ render_component_repo() {
371
393
 
372
394
  local level="red"
373
395
  local detail="missing"
374
- if is_git_repo "$active_dir"; then
375
- local dirty
376
- dirty="$(git_dirty_flag "$active_dir")"
377
- local ab
378
- ab="$(git_ahead_behind "$active_dir")"
379
- local ahead="" behind=""
380
- if [[ -n "$ab" ]]; then
381
- ahead="$(echo "$ab" | cut -d'|' -f1)"
382
- 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
383
426
  fi
427
+ fi
384
428
 
429
+ if [[ "$status" == "ok" ]]; then
430
+ detail="ok"
385
431
  if [[ "$dirty" == "dirty" ]] || [[ -n "$behind" && "$behind" != "0" ]]; then
386
432
  level="orange"
387
433
  else
388
434
  level="green"
389
435
  fi
390
- detail="ok"
391
436
  fi
392
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
+
393
441
  local sf color
394
442
  sf="$(sf_for_level "$level")"
395
443
  color="$(color_for_level "$level")"
@@ -398,7 +446,20 @@ render_component_repo() {
398
446
  local p2="${prefix}--"
399
447
  print_item "$p2" "Dir: $(shorten_path "$active_dir" 52)"
400
448
  if [[ "$detail" != "ok" ]]; then
401
- 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
402
463
  if [[ -n "$PNPM_BIN" ]]; then
403
464
  print_sep "$p2"
404
465
  local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/happys-term.sh"
@@ -407,22 +468,28 @@ render_component_repo() {
407
468
  return
408
469
  fi
409
470
 
410
- local branch head upstream
411
- branch="$(git_head_branch "$active_dir")"
412
- head="$(git_head_short "$active_dir")"
413
- upstream="$(git_upstream_short "$active_dir")"
414
-
415
- local dirty
416
- dirty="$(git_dirty_flag "$active_dir")"
417
- local ab ahead behind
418
- ab="$(git_ahead_behind "$active_dir")"
419
- ahead=""
420
- behind=""
421
- if [[ -n "$ab" ]]; then
422
- ahead="$(echo "$ab" | cut -d'|' -f1)"
423
- 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
424
490
  fi
425
491
 
492
+ print_sep "$p2"
426
493
  print_item "$p2" "HEAD: ${branch:-"(unknown)"} ${head:+($head)}"
427
494
  print_item "$p2" "Upstream: ${upstream:-"(none)"}"
428
495
  if [[ -n "$ahead" && -n "$behind" ]]; then
@@ -430,31 +497,22 @@ render_component_repo() {
430
497
  fi
431
498
  print_item "$p2" "Working tree: ${dirty}"
432
499
 
433
- local main_branch main_upstream main_ab
434
- main_branch="$(git_main_branch_name "$active_dir")"
435
500
  if [[ -n "$main_branch" ]]; then
436
- main_upstream="$(git_branch_upstream_short "$active_dir" "$main_branch")"
437
- main_ab="$(git_branch_ahead_behind "$active_dir" "$main_branch")"
438
501
  if [[ -n "$main_upstream" ]]; then
439
502
  print_item "$p2" "Main: ${main_branch} → ${main_upstream}"
440
503
  else
441
504
  print_item "$p2" "Main: ${main_branch} → (no upstream)"
442
505
  fi
443
- if [[ -n "$main_ab" ]]; then
444
- 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}"
445
508
  fi
446
509
 
447
510
  # Always show comparisons against origin/* and upstream/* when those remote refs exist.
448
511
  # (These reflect your last fetch; we do not auto-fetch in the menu.)
449
- local oref uref
450
- oref="$(git_remote_main_ref "$active_dir" "origin")"
451
- uref="$(git_remote_main_ref "$active_dir" "upstream")"
452
512
  if [[ -n "$oref" ]]; then
453
513
  local oref_short="${oref#refs/remotes/}"
454
- local oab
455
- oab="$(git_ahead_behind_refs "$active_dir" "$oref" "$main_branch")"
456
- if [[ -n "$oab" ]]; then
457
- 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}"
458
516
  else
459
517
  print_item "$p2" "Origin: ${oref_short}"
460
518
  fi
@@ -463,10 +521,8 @@ render_component_repo() {
463
521
  fi
464
522
  if [[ -n "$uref" ]]; then
465
523
  local uref_short="${uref#refs/remotes/}"
466
- local uab
467
- uab="$(git_ahead_behind_refs "$active_dir" "$uref" "$main_branch")"
468
- if [[ -n "$uab" ]]; then
469
- 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}"
470
526
  else
471
527
  print_item "$p2" "Upstream: ${uref_short}"
472
528
  fi
@@ -476,7 +532,8 @@ render_component_repo() {
476
532
  fi
477
533
 
478
534
  local wt_count
479
- wt_count="$(git_worktree_count "$active_dir")"
535
+ # If cache didn't populate wt_count, fall back to empty string.
536
+ wt_count="${wt_count:-}"
480
537
 
481
538
  # Quick actions
482
539
  print_sep "$p2"
@@ -536,7 +593,11 @@ render_component_repo() {
536
593
  print_item "$p2" "$wt_label"
537
594
  local p3="${p2}--"
538
595
  local tsv
539
- 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
540
601
  if [[ -z "$tsv" ]]; then
541
602
  print_item "$p3" "No worktrees found | color=$GRAY"
542
603
  else
@@ -604,14 +665,55 @@ render_components_menu() {
604
665
  local stack_name="$3"
605
666
  local env_file="$4"
606
667
 
668
+ local t0 t1
669
+ t0="$(swiftbar_now_ms 2>/dev/null || echo 0)"
670
+
607
671
  print_item "$prefix" "Components | sfimage=cube"
608
672
  local p2="${prefix}--"
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
686
+ fi
687
+
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"
704
+ fi
705
+ print_sep "$p2"
706
+ fi
707
+
609
708
  # Always render the known components using the resolved component dirs (env file → env.local/.env → fallback),
610
709
  # instead of assuming they live under `~/.happy-stacks/workspace/components`.
611
710
  for c in happy happy-cli happy-server-light happy-server; do
612
711
  render_component_repo "$p2" "$c" "$context" "$stack_name" "$env_file"
613
712
  print_sep "$p2"
614
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))"
615
717
  }
616
718
 
617
719
  render_stack_overview_item() {
@@ -662,14 +764,20 @@ collect_stack_status() {
662
764
  daemon_uptime="$(get_daemon_uptime "$cli_home_dir")"
663
765
  last_heartbeat="$(get_last_heartbeat "$cli_home_dir")"
664
766
 
665
- local plist_path="$HOME/Library/LaunchAgents/${label}.plist"
666
767
  local launchagent_status autostart_pid autostart_metrics
667
- launchagent_status="$(check_launchagent_status "$label" "$plist_path")"
668
- autostart_pid=""
669
- autostart_metrics=""
670
- if [[ "$launchagent_status" != "not_installed" ]]; then
671
- autostart_pid="$(launchagent_pid_for_label "$label")"
672
- 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
673
781
  fi
674
782
 
675
783
  local level
@@ -709,7 +817,18 @@ render_stack_info() {
709
817
  print_item "$prefix" "Stack details | sfimage=server.rack"
710
818
  local p2="${prefix}--"
711
819
  print_item "$p2" "Server component: ${server_component}"
712
- 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}"
713
832
  print_item "$p2" "Label: ${label}"
714
833
  [[ -n "$env_file" ]] && print_item "$p2" "Env: $(shorten_path "$env_file" 52)"
715
834
  [[ -n "$tailscale_url" ]] && print_item "$p2" "Tailscale: $(shorten_text "$tailscale_url" 52)"
@@ -736,9 +855,13 @@ render_stack_info() {
736
855
  fi
737
856
  print_sep "$p2"
738
857
 
739
- local plist="$HOME/Library/LaunchAgents/${label}.plist"
740
858
  local svc_installed="0"
741
- [[ -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)"
742
865
 
743
866
  if [[ "$stack_name" == "main" ]]; then
744
867
  local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/happys-term.sh"
@@ -754,10 +877,12 @@ render_stack_info() {
754
877
  if [[ "${MAIN_LEVEL:-}" == "red" ]]; then
755
878
  print_item "$p2" "Start (foreground) | bash=$PNPM_TERM param1=start dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
756
879
  else
757
- 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"
758
881
  fi
759
882
  fi
760
- 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
761
886
  print_item "$p2" "Build UI | bash=$PNPM_TERM param1=build dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
762
887
  print_item "$p2" "Doctor | bash=$PNPM_TERM param1=stack:doctor dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
763
888
  return
@@ -776,17 +901,21 @@ render_stack_info() {
776
901
  if [[ "$STACK_LEVEL" == "red" ]]; then
777
902
  print_item "$p2" "Start (foreground) | bash=$PNPM_TERM param1=stack param2=start param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
778
903
  else
779
- 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"
780
905
  fi
781
906
  fi
782
- 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
783
910
  print_item "$p2" "Build UI | bash=$PNPM_TERM param1=stack param2=build param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
784
911
  print_item "$p2" "Doctor | bash=$PNPM_TERM param1=stack param2=doctor param3=$stack_name dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
785
- 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"
786
- 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
787
916
 
788
917
  local pr_helper="$HAPPY_LOCAL_DIR/extras/swiftbar/wt-pr.sh"
789
- if [[ -x "$pr_helper" ]]; then
918
+ if [[ "$menu_mode" != "selfhost" && -x "$pr_helper" ]]; then
790
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"
791
920
  fi
792
921
  }
@@ -14,9 +14,7 @@ get_process_metrics() {
14
14
  return
15
15
  fi
16
16
  local cpu rss etime
17
- cpu="$(echo "$line" | awk '{print $1}')"
18
- rss="$(echo "$line" | awk '{print $2}')" # KB
19
- etime="$(echo "$line" | awk '{print $3}')"
17
+ IFS=' ' read -r cpu rss etime <<<"$line"
20
18
  local mem_mb
21
19
  mem_mb="$(awk -v rss="$rss" 'BEGIN { printf "%.0f", (rss/1024.0) }')"
22
20
  echo "$cpu|$mem_mb|$etime"
@@ -43,7 +41,11 @@ ensure_launchctl_cache() {
43
41
  return
44
42
  fi
45
43
  if command -v launchctl >/dev/null 2>&1; then
44
+ local t0 t1
45
+ t0="$(swiftbar_now_ms 2>/dev/null || echo 0)"
46
46
  LAUNCHCTL_LIST_CACHE="$(launchctl list 2>/dev/null || true)"
47
+ t1="$(swiftbar_now_ms 2>/dev/null || echo 0)"
48
+ swiftbar_profile_log "time" "label=launchctl_list" "ms=$((t1 - t0))"
47
49
  fi
48
50
  }
49
51
 
@@ -56,7 +58,8 @@ check_launchagent_status() {
56
58
  fi
57
59
 
58
60
  ensure_launchctl_cache
59
- if echo "$LAUNCHCTL_LIST_CACHE" | grep -q "$label"; then
61
+ # Match the label column exactly (avoid substring false positives).
62
+ if echo "$LAUNCHCTL_LIST_CACHE" | awk -v lbl="$label" '$3==lbl{found=1} END{exit found?0:1}'; then
60
63
  echo "loaded"
61
64
  return
62
65
  fi
@@ -88,7 +91,11 @@ check_server_health() {
88
91
  fi
89
92
  local response
90
93
  # Tight timeouts to keep menus snappy even with many stacks.
94
+ local t0 t1
95
+ t0="$(swiftbar_now_ms 2>/dev/null || echo 0)"
91
96
  response="$(curl -s --connect-timeout 0.2 --max-time 0.6 "http://127.0.0.1:${port}/health" 2>/dev/null || true)"
97
+ t1="$(swiftbar_now_ms 2>/dev/null || echo 0)"
98
+ swiftbar_profile_log "time" "label=curl_health" "port=${port}" "ms=$((t1 - t0))" "bytes=${#response}"
92
99
  if [[ "$response" == *"ok"* ]] || [[ "$response" == *"Welcome"* ]]; then
93
100
  echo "running"
94
101
  return
@@ -99,6 +106,8 @@ check_server_health() {
99
106
  check_daemon_status() {
100
107
  local cli_home_dir="$1"
101
108
  local state_file="$cli_home_dir/daemon.state.json"
109
+ local t0 t1
110
+ t0="$(swiftbar_now_ms 2>/dev/null || echo 0)"
102
111
  if [[ -z "$cli_home_dir" ]] || [[ ! -f "$state_file" ]]; then
103
112
  # If the daemon is starting but hasn't written daemon.state.json yet, we can still detect it
104
113
  # via the lock file PID.
@@ -153,13 +162,19 @@ check_daemon_status() {
153
162
  # Best-effort: confirm the control server is responding (tight timeouts).
154
163
  if [[ -n "$httpPort" ]] && [[ "$httpPort" =~ ^[0-9]+$ ]]; then
155
164
  if curl -s --connect-timeout 0.2 --max-time 0.5 -X POST -H 'content-type: application/json' -d '{}' "http://127.0.0.1:${httpPort}/list" >/dev/null 2>&1; then
165
+ t1="$(swiftbar_now_ms 2>/dev/null || echo 0)"
166
+ swiftbar_profile_log "time" "label=daemon_status" "ms=$((t1 - t0))" "httpProbe=ok"
156
167
  echo "running:$pid"
157
168
  return
158
169
  fi
170
+ t1="$(swiftbar_now_ms 2>/dev/null || echo 0)"
171
+ swiftbar_profile_log "time" "label=daemon_status" "ms=$((t1 - t0))" "httpProbe=fail"
159
172
  echo "running-no-http:$pid"
160
173
  return
161
174
  fi
162
175
 
176
+ t1="$(swiftbar_now_ms 2>/dev/null || echo 0)"
177
+ swiftbar_profile_log "time" "label=daemon_status" "ms=$((t1 - t0))" "httpProbe=skip"
163
178
  echo "running:$pid"
164
179
  }
165
180
 
@@ -199,7 +214,11 @@ get_tailscale_url() {
199
214
  local happys_sh="$HAPPY_LOCAL_DIR/extras/swiftbar/happys.sh"
200
215
  if [[ -x "$happys_sh" ]]; then
201
216
  # Keep SwiftBar responsive: use a tight timeout for this periodic probe.
217
+ local t0 t1
218
+ t0="$(swiftbar_now_ms 2>/dev/null || echo 0)"
202
219
  url="$("$happys_sh" tailscale:url --timeout-ms=2500 2>/dev/null | head -1 | tr -d '[:space:]' || true)"
220
+ t1="$(swiftbar_now_ms 2>/dev/null || echo 0)"
221
+ swiftbar_profile_log "time" "label=tailscale_url_happys" "ms=$((t1 - t0))" "ok=$([[ "$url" == https://* ]] && echo 1 || echo 0)"
203
222
  if [[ "$url" == https://* ]]; then
204
223
  echo "$url"
205
224
  return
@@ -208,7 +227,11 @@ get_tailscale_url() {
208
227
  fi
209
228
 
210
229
  if command -v tailscale &>/dev/null; then
230
+ local t0 t1
231
+ t0="$(swiftbar_now_ms 2>/dev/null || echo 0)"
211
232
  url="$(tailscale serve status 2>/dev/null | grep -oE 'https://[^ ]+' | head -1 || true)"
233
+ t1="$(swiftbar_now_ms 2>/dev/null || echo 0)"
234
+ swiftbar_profile_log "time" "label=tailscale_url_cli" "ms=$((t1 - t0))" "ok=$([[ -n "$url" ]] && echo 1 || echo 0)"
212
235
  fi
213
236
  if [[ -z "$url" ]] && [[ -x "/Applications/Tailscale.app/Contents/MacOS/tailscale" ]]; then
214
237
  url="$(/Applications/Tailscale.app/Contents/MacOS/tailscale serve status 2>/dev/null | grep -oE 'https://[^ ]+' | head -1 || true)"