happy-stacks 0.0.0 → 0.1.2

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 (42) hide show
  1. package/README.md +22 -4
  2. package/bin/happys.mjs +76 -5
  3. package/docs/server-flavors.md +61 -2
  4. package/docs/stacks.md +16 -4
  5. package/extras/swiftbar/auth-login.sh +5 -5
  6. package/extras/swiftbar/happy-stacks.5s.sh +83 -41
  7. package/extras/swiftbar/happys-term.sh +151 -0
  8. package/extras/swiftbar/happys.sh +52 -0
  9. package/extras/swiftbar/lib/render.sh +74 -56
  10. package/extras/swiftbar/lib/system.sh +37 -6
  11. package/extras/swiftbar/lib/utils.sh +180 -4
  12. package/extras/swiftbar/pnpm-term.sh +2 -122
  13. package/extras/swiftbar/pnpm.sh +2 -13
  14. package/extras/swiftbar/set-server-flavor.sh +8 -8
  15. package/extras/swiftbar/wt-pr.sh +1 -1
  16. package/package.json +1 -1
  17. package/scripts/auth.mjs +374 -3
  18. package/scripts/daemon.mjs +78 -11
  19. package/scripts/dev.mjs +122 -17
  20. package/scripts/init.mjs +238 -32
  21. package/scripts/migrate.mjs +292 -0
  22. package/scripts/mobile.mjs +51 -19
  23. package/scripts/run.mjs +118 -26
  24. package/scripts/service.mjs +176 -37
  25. package/scripts/stack.mjs +665 -22
  26. package/scripts/stop.mjs +157 -0
  27. package/scripts/tailscale.mjs +147 -21
  28. package/scripts/typecheck.mjs +145 -0
  29. package/scripts/ui_gateway.mjs +248 -0
  30. package/scripts/uninstall.mjs +3 -3
  31. package/scripts/utils/cli_registry.mjs +23 -0
  32. package/scripts/utils/config.mjs +9 -1
  33. package/scripts/utils/env.mjs +37 -15
  34. package/scripts/utils/expo.mjs +94 -0
  35. package/scripts/utils/happy_server_infra.mjs +430 -0
  36. package/scripts/utils/pm.mjs +11 -2
  37. package/scripts/utils/ports.mjs +51 -13
  38. package/scripts/utils/proc.mjs +46 -5
  39. package/scripts/utils/server.mjs +37 -0
  40. package/scripts/utils/stack_stop.mjs +206 -0
  41. package/scripts/utils/validate.mjs +42 -1
  42. package/scripts/worktrees.mjs +53 -7
@@ -0,0 +1,52 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # SwiftBar menu action wrapper.
5
+ # Runs `happys` using the stable shim installed under ~/.happy-stacks/bin.
6
+
7
+ CANONICAL_ENV_FILE="$HOME/.happy-stacks/.env"
8
+
9
+ dotenv_get_quick() {
10
+ local file="$1"
11
+ local key="$2"
12
+ [[ -n "$file" && -n "$key" && -f "$file" ]] || return 0
13
+ local line
14
+ line="$(grep -E "^${key}=" "$file" 2>/dev/null | head -n 1 || true)"
15
+ [[ -n "$line" ]] || return 0
16
+ local v="${line#*=}"
17
+ v="${v%$'\r'}"
18
+ if [[ "$v" == \"*\" && "$v" == *\" ]]; then v="${v#\"}"; v="${v%\"}"; fi
19
+ if [[ "$v" == \'*\' && "$v" == *\' ]]; then v="${v#\'}"; v="${v%\'}"; fi
20
+ echo "$v"
21
+ }
22
+
23
+ expand_home_quick() {
24
+ local p="$1"
25
+ if [[ "$p" == "~/"* ]]; then
26
+ echo "$HOME/${p#~/}"
27
+ else
28
+ echo "$p"
29
+ fi
30
+ }
31
+
32
+ home_from_canonical=""
33
+ if [[ -f "$CANONICAL_ENV_FILE" ]]; then
34
+ home_from_canonical="$(dotenv_get_quick "$CANONICAL_ENV_FILE" "HAPPY_STACKS_HOME_DIR")"
35
+ [[ -z "$home_from_canonical" ]] && home_from_canonical="$(dotenv_get_quick "$CANONICAL_ENV_FILE" "HAPPY_LOCAL_HOME_DIR")"
36
+ fi
37
+ home_from_canonical="$(expand_home_quick "${home_from_canonical:-}")"
38
+
39
+ HAPPY_STACKS_HOME_DIR="${HAPPY_STACKS_HOME_DIR:-${home_from_canonical:-$HOME/.happy-stacks}}"
40
+ HAPPY_LOCAL_DIR="${HAPPY_LOCAL_DIR:-$HAPPY_STACKS_HOME_DIR}"
41
+
42
+ HAPPYS_BIN="$HAPPY_LOCAL_DIR/bin/happys"
43
+ if [[ ! -x "$HAPPYS_BIN" ]]; then
44
+ HAPPYS_BIN="$(command -v happys 2>/dev/null || true)"
45
+ fi
46
+
47
+ if [[ -z "${HAPPYS_BIN:-}" ]]; then
48
+ echo "happys not found (run: npx happy-stacks init, or npm i -g happy-stacks)" >&2
49
+ exit 1
50
+ fi
51
+
52
+ exec "$HAPPYS_BIN" "$@"
@@ -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,8 +74,8 @@ 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"
@@ -85,7 +99,7 @@ render_component_server() {
85
99
 
86
100
  # Start/stop shortcuts (so you can control from the Server submenu too).
87
101
  if [[ -n "$PNPM_BIN" ]]; then
88
- local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
102
+ local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/happys-term.sh"
89
103
  local plist=""
90
104
  local svc_installed="0"
91
105
  if [[ -n "$launch_label" ]]; then
@@ -130,7 +144,7 @@ render_component_server() {
130
144
  # Flavor switching (status-aware: only show switching to the other option).
131
145
  local helper="$HAPPY_LOCAL_DIR/extras/swiftbar/set-server-flavor.sh"
132
146
  if [[ -n "$PNPM_BIN" ]]; then
133
- local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
147
+ local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/happys-term.sh"
134
148
  print_sep "$p2"
135
149
  if [[ "$server_component" == "happy-server" ]]; then
136
150
  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 +173,10 @@ render_component_daemon() {
159
173
  if [[ "$daemon_status" == "running" ]]; then level="green"; fi
160
174
  if [[ "$daemon_status" == "running-no-http" || "$daemon_status" == "stale" || "$daemon_status" == "auth_required" || "$daemon_status" == "starting" ]]; then level="orange"; fi
161
175
 
176
+ local sfconfig="$(sfconfig_for_level "$level")"
177
+
162
178
  local sf="$(sf_for_level "$level")"
163
- local color="$(color_for_level "$level")"
164
- print_item "$prefix" "Daemon | sfimage=$sf color=$color"
179
+ print_item "$prefix" "Daemon | sfimage=$sf sfconfig=$sfconfig"
165
180
 
166
181
  local p2="${prefix}--"
167
182
  print_item "$p2" "Status: $daemon_status"
@@ -194,14 +209,16 @@ render_component_daemon() {
194
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"
195
210
  else
196
211
  # 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"
212
+ local env_file
213
+ env_file="$(resolve_stack_env_file "$stack_name")"
198
214
  local port
199
- port="$(dotenv_get "$env_file" "HAPPY_LOCAL_SERVER_PORT")"
215
+ port="$(dotenv_get "$env_file" "HAPPY_STACKS_SERVER_PORT")"
216
+ [[ -z "$port" ]] && port="$(dotenv_get "$env_file" "HAPPY_LOCAL_SERVER_PORT")"
200
217
  [[ -z "$port" ]] && port="$(resolve_main_port)"
201
218
  server_url="http://127.0.0.1:${port}"
202
219
  webapp_url="$(get_tailscale_url)"
203
220
  [[ -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"
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"
205
222
  fi
206
223
  print_sep "$p2"
207
224
  fi
@@ -227,8 +244,8 @@ render_component_autostart() {
227
244
  if [[ "$launchagent_status" == "unloaded" ]]; then level="orange"; fi
228
245
 
229
246
  local sf="$(sf_for_level "$level")"
230
- local color="$(color_for_level "$level")"
231
- print_item "$prefix" "Autostart | sfimage=$sf color=$color"
247
+ local sfconfig="$(sfconfig_for_level "$level")"
248
+ print_item "$prefix" "Autostart | sfimage=$sf sfconfig=$sfconfig"
232
249
 
233
250
  local p2="${prefix}--"
234
251
  print_item "$p2" "Status: $launchagent_status"
@@ -292,8 +309,8 @@ render_component_tailscale() {
292
309
  [[ -n "$tailscale_url" ]] && level="green"
293
310
 
294
311
  local sf="$(sf_for_level "$level")"
295
- local color="$(color_for_level "$level")"
296
- print_item "$prefix" "Tailscale | sfimage=$sf color=$color"
312
+ local sfconfig="$(sfconfig_for_level "$level")"
313
+ print_item "$prefix" "Tailscale | sfimage=$sf sfconfig=$sfconfig"
297
314
 
298
315
  local p2="${prefix}--"
299
316
  if [[ -n "$tailscale_url" ]]; then
@@ -312,7 +329,7 @@ render_component_tailscale() {
312
329
  print_sep "$p2"
313
330
 
314
331
  if [[ "$stack_name" == "main" ]]; then
315
- local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
332
+ local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/happys-term.sh"
316
333
  print_item "$p2" "Tailscale status | bash=$PNPM_TERM param1=tailscale:status dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
317
334
  if [[ -n "$tailscale_url" ]]; then
318
335
  print_item "$p2" "Disable Tailscale Serve | bash=$PNPM_BIN param1=tailscale:disable dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
@@ -323,7 +340,7 @@ render_component_tailscale() {
323
340
  return
324
341
  fi
325
342
 
326
- local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
343
+ local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/happys-term.sh"
327
344
  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
345
  if [[ -n "$tailscale_url" ]]; then
329
346
  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"
@@ -345,7 +362,8 @@ render_component_repo() {
345
362
  local env_file="$5"
346
363
 
347
364
  local active_dir=""
348
- if [[ "$context" == "stack" && -n "$env_file" ]]; then
365
+ # If we have an env file for the current context, prefer it (stack env is authoritative).
366
+ if [[ -n "$env_file" && -f "$env_file" ]]; then
349
367
  active_dir="$(resolve_component_dir_from_env_file "$env_file" "$component")"
350
368
  else
351
369
  active_dir="$(resolve_component_dir_from_env "$component")"
@@ -383,7 +401,7 @@ render_component_repo() {
383
401
  print_item "$p2" "Status: not a git repo / missing"
384
402
  if [[ -n "$PNPM_BIN" ]]; then
385
403
  print_sep "$p2"
386
- local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
404
+ local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/happys-term.sh"
387
405
  print_item "$p2" "Bootstrap (clone missing components) | bash=$PNPM_TERM param1=bootstrap dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
388
406
  fi
389
407
  return
@@ -457,15 +475,15 @@ render_component_repo() {
457
475
  fi
458
476
  fi
459
477
 
460
- local wt_count
461
- wt_count="$(git_worktree_count "$active_dir")"
478
+ local wt_count
479
+ wt_count="$(git_worktree_count "$active_dir")"
462
480
 
463
481
  # Quick actions
464
482
  print_sep "$p2"
465
483
  print_item "$p2" "Open folder | bash=/usr/bin/open param1='$active_dir' terminal=false"
466
484
 
467
485
  if [[ -n "$PNPM_BIN" ]]; then
468
- local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
486
+ local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/happys-term.sh"
469
487
  # Run via stack wrappers when in a stack context so env-file stays authoritative.
470
488
  if [[ "$context" == "stack" && -n "$stack_name" ]]; then
471
489
  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"
@@ -522,7 +540,11 @@ render_component_repo() {
522
540
  if [[ -z "$tsv" ]]; then
523
541
  print_item "$p3" "No worktrees found | color=$GRAY"
524
542
  else
525
- local root="$(resolve_components_dir)/.worktrees/$component/"
543
+ # Worktrees live alongside the component checkout at: <componentsRoot>/.worktrees/<component>/...
544
+ local components_root default_path root
545
+ components_root="$(dirname "$active_dir")"
546
+ default_path="$components_root/$component"
547
+ root="$components_root/.worktrees/$component/"
526
548
  local shown=0
527
549
  while IFS=$'\t' read -r wt_path wt_branchref; do
528
550
  [[ -n "$wt_path" ]] || continue
@@ -534,11 +556,13 @@ render_component_repo() {
534
556
 
535
557
  local label=""
536
558
  local spec=""
537
- if [[ "$wt_path" == "$root"* ]]; then
559
+ if [[ "$wt_path" == "$default_path" ]]; then
560
+ spec="default"
561
+ label="default"
562
+ elif [[ "$wt_path" == "$root"* ]]; then
538
563
  spec="${wt_path#"$root"}"
539
564
  label="$spec"
540
565
  else
541
- spec="$wt_path"
542
566
  label="$(shorten_path "$wt_path" 52)"
543
567
  fi
544
568
 
@@ -549,18 +573,24 @@ render_component_repo() {
549
573
  label="(active) $label"
550
574
  fi
551
575
 
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"
576
+ print_item "$p3" "$label"
577
+
578
+ # Only show "use" actions when we can express the worktree as a spec (default or under .worktrees).
579
+ # Some git worktrees can exist outside our managed tree; for those we only offer open/shell actions.
580
+ if [[ -n "$spec" ]]; then
581
+ if [[ "$context" == "stack" && -n "$stack_name" ]]; then
582
+ 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"
583
+ 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"
584
+ 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"
585
+ 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"
586
+ else
587
+ print_item "${p3}--" "Use (main) | bash=$PNPM_BIN param1=wt param2=use param3=$component param4=$spec dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
588
+ 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"
589
+ print_item "${p3}--" "Open in VS Code | bash=$PNPM_BIN param1=wt param2=code param3=$component param4=$spec dir=$HAPPY_LOCAL_DIR terminal=false"
590
+ print_item "${p3}--" "Open in Cursor | bash=$PNPM_BIN param1=wt param2=cursor param3=$component param4=$spec dir=$HAPPY_LOCAL_DIR terminal=false"
591
+ fi
558
592
  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"
593
+ print_item "${p3}--" "Open folder | bash=/usr/bin/open param1='$wt_path' terminal=false"
564
594
  fi
565
595
  done <<<"$tsv"
566
596
  fi
@@ -576,24 +606,12 @@ render_components_menu() {
576
606
 
577
607
  print_item "$prefix" "Components | sfimage=cube"
578
608
  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"
609
+ # Always render the known components using the resolved component dirs (env file → env.local/.env → fallback),
610
+ # instead of assuming they live under `~/.happy-stacks/workspace/components`.
587
611
  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
612
+ render_component_repo "$p2" "$c" "$context" "$stack_name" "$env_file"
613
+ print_sep "$p2"
593
614
  done
594
- if [[ "$any" == "0" ]]; then
595
- print_item "$p2" "No components found under components/ | color=$GRAY"
596
- fi
597
615
  }
598
616
 
599
617
  render_stack_overview_item() {
@@ -688,7 +706,7 @@ render_stack_info() {
688
706
  local tailscale_url="$9" # optional
689
707
 
690
708
  # Avoid low-contrast gray in the main list; keep it readable in both light/dark.
691
- print_item "$prefix" "Stack details | sfimage=info.circle"
709
+ print_item "$prefix" "Stack details | sfimage=server.rack"
692
710
  local p2="${prefix}--"
693
711
  print_item "$p2" "Server component: ${server_component}"
694
712
  print_item "$p2" "Port: ${port}"
@@ -723,7 +741,7 @@ render_stack_info() {
723
741
  [[ -f "$plist" ]] && svc_installed="1"
724
742
 
725
743
  if [[ "$stack_name" == "main" ]]; then
726
- local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
744
+ local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/happys-term.sh"
727
745
  if [[ "$svc_installed" == "1" ]]; then
728
746
  # Status-aware: only show start/stop based on whether the stack is running.
729
747
  if [[ "${MAIN_LEVEL:-}" == "red" ]]; then
@@ -745,7 +763,7 @@ render_stack_info() {
745
763
  return
746
764
  fi
747
765
 
748
- local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
766
+ local PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/happys-term.sh"
749
767
  if [[ "$svc_installed" == "1" ]]; then
750
768
  # Status-aware: only show start/stop based on whether the stack is running.
751
769
  if [[ "$STACK_LEVEL" == "red" ]]; then
@@ -112,7 +112,7 @@ check_daemon_status() {
112
112
  local latest_log
113
113
  latest_log="$(ls -1t "$cli_home_dir"/logs/*-daemon.log 2>/dev/null | head -1 || true)"
114
114
  if [[ -n "$latest_log" ]]; then
115
- if tail -n 120 "$latest_log" 2>/dev/null | rg -q "No credentials found|starting authentication flow|Waiting for credentials"; then
115
+ if tail -n 120 "$latest_log" 2>/dev/null | grep -Eq "No credentials found|starting authentication flow|Waiting for credentials"; then
116
116
  echo "auth_required:$lock_pid"
117
117
  return
118
118
  fi
@@ -129,9 +129,16 @@ check_daemon_status() {
129
129
  return
130
130
  fi
131
131
 
132
+ local node_bin
133
+ node_bin="$(resolve_node_bin)"
134
+ if [[ -z "$node_bin" ]] || [[ ! -x "$node_bin" ]]; then
135
+ echo "unknown"
136
+ return
137
+ fi
138
+
132
139
  local pid httpPort
133
- pid="$(node -e 'const fs=require("fs"); const s=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); process.stdout.write(String(s.pid ?? ""));' "$state_file" 2>/dev/null || true)"
134
- httpPort="$(node -e 'const fs=require("fs"); const s=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); process.stdout.write(String(s.httpPort ?? ""));' "$state_file" 2>/dev/null || true)"
140
+ pid="$("$node_bin" -e 'const fs=require("fs"); const s=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); process.stdout.write(String(s.pid ?? ""));' "$state_file" 2>/dev/null || true)"
141
+ httpPort="$("$node_bin" -e 'const fs=require("fs"); const s=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); process.stdout.write(String(s.httpPort ?? ""));' "$state_file" 2>/dev/null || true)"
135
142
 
136
143
  if [[ -z "$pid" ]] || ! [[ "$pid" =~ ^[0-9]+$ ]]; then
137
144
  echo "unknown"
@@ -162,7 +169,12 @@ get_daemon_uptime() {
162
169
  if [[ -z "$cli_home_dir" ]] || [[ ! -f "$state_file" ]]; then
163
170
  return
164
171
  fi
165
- node -e 'const fs=require("fs"); const s=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); if (s.startTime) process.stdout.write(String(s.startTime));' "$state_file" 2>/dev/null || true
172
+ local node_bin
173
+ node_bin="$(resolve_node_bin)"
174
+ if [[ -z "$node_bin" ]] || [[ ! -x "$node_bin" ]]; then
175
+ return
176
+ fi
177
+ "$node_bin" -e 'const fs=require("fs"); const s=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); if (s.startTime) process.stdout.write(String(s.startTime));' "$state_file" 2>/dev/null || true
166
178
  }
167
179
 
168
180
  get_last_heartbeat() {
@@ -171,20 +183,39 @@ get_last_heartbeat() {
171
183
  if [[ -z "$cli_home_dir" ]] || [[ ! -f "$state_file" ]]; then
172
184
  return
173
185
  fi
174
- node -e 'const fs=require("fs"); const s=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); if (s.lastHeartbeat) process.stdout.write(String(s.lastHeartbeat));' "$state_file" 2>/dev/null || true
186
+ local node_bin
187
+ node_bin="$(resolve_node_bin)"
188
+ if [[ -z "$node_bin" ]] || [[ ! -x "$node_bin" ]]; then
189
+ return
190
+ fi
191
+ "$node_bin" -e 'const fs=require("fs"); const s=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); if (s.lastHeartbeat) process.stdout.write(String(s.lastHeartbeat));' "$state_file" 2>/dev/null || true
175
192
  }
176
193
 
177
194
  get_tailscale_url() {
178
195
  # Try multiple methods to get the Tailscale URL (best-effort).
179
196
  local url=""
180
197
 
198
+ # Preferred: use happys (respects our own timeouts/env handling).
199
+ local happys_sh="$HAPPY_LOCAL_DIR/extras/swiftbar/happys.sh"
200
+ if [[ -x "$happys_sh" ]]; then
201
+ # Keep SwiftBar responsive: use a tight timeout for this periodic probe.
202
+ url="$("$happys_sh" tailscale:url --timeout-ms=2500 2>/dev/null | head -1 | tr -d '[:space:]' || true)"
203
+ if [[ "$url" == https://* ]]; then
204
+ echo "$url"
205
+ return
206
+ fi
207
+ url=""
208
+ fi
209
+
181
210
  if command -v tailscale &>/dev/null; then
182
211
  url="$(tailscale serve status 2>/dev/null | grep -oE 'https://[^ ]+' | head -1 || true)"
183
212
  fi
213
+ if [[ -z "$url" ]] && [[ -x "/Applications/Tailscale.app/Contents/MacOS/tailscale" ]]; then
214
+ url="$(/Applications/Tailscale.app/Contents/MacOS/tailscale serve status 2>/dev/null | grep -oE 'https://[^ ]+' | head -1 || true)"
215
+ fi
184
216
  if [[ -z "$url" ]] && [[ -x "/Applications/Tailscale.app/Contents/MacOS/Tailscale" ]]; then
185
217
  url="$(/Applications/Tailscale.app/Contents/MacOS/Tailscale serve status 2>/dev/null | grep -oE 'https://[^ ]+' | head -1 || true)"
186
218
  fi
187
219
 
188
220
  echo "$url"
189
221
  }
190
-
@@ -50,6 +50,15 @@ dotenv_get() {
50
50
  ' "$file" 2>/dev/null
51
51
  }
52
52
 
53
+ expand_home_path() {
54
+ local p="$1"
55
+ if [[ "$p" == "~/"* ]]; then
56
+ echo "$HOME/${p#~/}"
57
+ return
58
+ fi
59
+ echo "$p"
60
+ }
61
+
53
62
  resolve_happy_local_dir() {
54
63
  local home="${HAPPY_STACKS_HOME_DIR:-$HOME/.happy-stacks}"
55
64
 
@@ -69,9 +78,130 @@ resolve_happy_local_dir() {
69
78
  echo "$home"
70
79
  }
71
80
 
81
+ resolve_stacks_storage_root() {
82
+ # Priority:
83
+ # 1) explicit env var
84
+ # 2) home env.local
85
+ # 3) home .env (canonical pointer file, written by `happys init`)
86
+ # 4) default to ~/.happy/stacks
87
+ if [[ -n "${HAPPY_STACKS_STORAGE_DIR:-}" ]]; then
88
+ echo "$(expand_home_path "$HAPPY_STACKS_STORAGE_DIR")"
89
+ return
90
+ fi
91
+ if [[ -n "${HAPPY_LOCAL_STORAGE_DIR:-}" ]]; then
92
+ echo "$(expand_home_path "$HAPPY_LOCAL_STORAGE_DIR")"
93
+ return
94
+ fi
95
+
96
+ local p
97
+ p="$(dotenv_get "$HAPPY_LOCAL_DIR/env.local" "HAPPY_STACKS_STORAGE_DIR")"
98
+ [[ -z "$p" ]] && p="$(dotenv_get "$HAPPY_LOCAL_DIR/env.local" "HAPPY_LOCAL_STORAGE_DIR")"
99
+ [[ -z "$p" ]] && p="$(dotenv_get "$HAPPY_LOCAL_DIR/.env" "HAPPY_STACKS_STORAGE_DIR")"
100
+ [[ -z "$p" ]] && p="$(dotenv_get "$HAPPY_LOCAL_DIR/.env" "HAPPY_LOCAL_STORAGE_DIR")"
101
+ if [[ -n "$p" ]]; then
102
+ echo "$(expand_home_path "$p")"
103
+ return
104
+ fi
105
+
106
+ echo "$HOME/.happy/stacks"
107
+ }
108
+
109
+ resolve_stack_env_file() {
110
+ local stack_name="${1:-main}"
111
+ local storage_root
112
+ storage_root="$(resolve_stacks_storage_root)"
113
+
114
+ local primary="${storage_root}/${stack_name}/env"
115
+ if [[ -f "$primary" ]]; then
116
+ echo "$primary"
117
+ return
118
+ fi
119
+
120
+ local legacy="$HOME/.happy/local/stacks/${stack_name}/env"
121
+ if [[ -f "$legacy" ]]; then
122
+ echo "$legacy"
123
+ return
124
+ fi
125
+
126
+ # Very old single-stack location (best-effort).
127
+ if [[ "$stack_name" == "main" ]]; then
128
+ local legacy_single="$HOME/.happy/local/env"
129
+ if [[ -f "$legacy_single" ]]; then
130
+ echo "$legacy_single"
131
+ return
132
+ fi
133
+ fi
134
+
135
+ echo "$primary"
136
+ }
137
+
138
+ resolve_stack_base_dir() {
139
+ local stack_name="${1:-main}"
140
+ local env_file="${2:-}"
141
+ if [[ -z "$env_file" ]]; then
142
+ env_file="$(resolve_stack_env_file "$stack_name")"
143
+ fi
144
+ # If the env file exists, its parent directory is the stack base dir for all supported layouts.
145
+ if [[ -n "$env_file" ]] && [[ -f "$env_file" ]]; then
146
+ dirname "$env_file"
147
+ return
148
+ fi
149
+ local storage_root
150
+ storage_root="$(resolve_stacks_storage_root)"
151
+ echo "${storage_root}/${stack_name}"
152
+ }
153
+
154
+ resolve_stack_cli_home_dir() {
155
+ local stack_name="${1:-main}"
156
+ local env_file="${2:-}"
157
+ if [[ -z "$env_file" ]]; then
158
+ env_file="$(resolve_stack_env_file "$stack_name")"
159
+ fi
160
+ local cli_home=""
161
+ if [[ -n "$env_file" ]] && [[ -f "$env_file" ]]; then
162
+ cli_home="$(dotenv_get "$env_file" "HAPPY_STACKS_CLI_HOME_DIR")"
163
+ [[ -z "$cli_home" ]] && cli_home="$(dotenv_get "$env_file" "HAPPY_LOCAL_CLI_HOME_DIR")"
164
+ fi
165
+ if [[ -n "$cli_home" ]]; then
166
+ echo "$(expand_home_path "$cli_home")"
167
+ return
168
+ fi
169
+ local base_dir
170
+ base_dir="$(resolve_stack_base_dir "$stack_name" "$env_file")"
171
+ echo "${base_dir}/cli"
172
+ }
173
+
174
+ resolve_stack_label() {
175
+ local stack_name="${1:-main}"
176
+ local primary="com.happy.stacks"
177
+ local legacy="com.happy.local"
178
+ if [[ "$stack_name" != "main" ]]; then
179
+ primary="com.happy.stacks.${stack_name}"
180
+ legacy="com.happy.local.${stack_name}"
181
+ fi
182
+ local primary_plist="$HOME/Library/LaunchAgents/${primary}.plist"
183
+ local legacy_plist="$HOME/Library/LaunchAgents/${legacy}.plist"
184
+ if [[ -f "$primary_plist" ]]; then
185
+ echo "$primary"
186
+ return
187
+ fi
188
+ if [[ -f "$legacy_plist" ]]; then
189
+ echo "$legacy"
190
+ return
191
+ fi
192
+ echo "$primary"
193
+ }
194
+
72
195
  resolve_pnpm_bin() {
73
- # Back-compat: historically this was "pnpm", but the plugin now runs `happys` via a wrapper script.
74
- local wrapper="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm.sh"
196
+ # Back-compat: historically this was "pnpm", but the plugin now runs `happys` via wrapper scripts.
197
+ local wrapper="$HAPPY_LOCAL_DIR/extras/swiftbar/happys.sh"
198
+ if [[ -x "$wrapper" ]]; then
199
+ echo "$wrapper"
200
+ return
201
+ fi
202
+
203
+ # Older installs.
204
+ wrapper="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm.sh"
75
205
  if [[ -x "$wrapper" ]]; then
76
206
  echo "$wrapper"
77
207
  return
@@ -87,6 +217,37 @@ resolve_pnpm_bin() {
87
217
  echo ""
88
218
  }
89
219
 
220
+ resolve_node_bin() {
221
+ # Prefer explicit env vars first.
222
+ if [[ -n "${HAPPY_STACKS_NODE:-}" ]] && [[ -x "${HAPPY_STACKS_NODE:-}" ]]; then
223
+ echo "$HAPPY_STACKS_NODE"
224
+ return
225
+ fi
226
+ if [[ -n "${HAPPY_LOCAL_NODE:-}" ]] && [[ -x "${HAPPY_LOCAL_NODE:-}" ]]; then
227
+ echo "$HAPPY_LOCAL_NODE"
228
+ return
229
+ fi
230
+
231
+ # Fall back to reading ~/.happy-stacks/.env (written by `happys init`).
232
+ local home="${HAPPY_STACKS_HOME_DIR:-$HOME/.happy-stacks}"
233
+ local env_file="$home/.env"
234
+ if [[ -f "$env_file" ]]; then
235
+ local v
236
+ v="$(dotenv_get "$env_file" "HAPPY_STACKS_NODE")"
237
+ if [[ -n "$v" ]] && [[ -x "$v" ]]; then
238
+ echo "$v"
239
+ return
240
+ fi
241
+ v="$(dotenv_get "$env_file" "HAPPY_LOCAL_NODE")"
242
+ if [[ -n "$v" ]] && [[ -x "$v" ]]; then
243
+ echo "$v"
244
+ return
245
+ fi
246
+ fi
247
+
248
+ command -v node 2>/dev/null || true
249
+ }
250
+
90
251
  resolve_workspace_dir() {
91
252
  if [[ -n "${HAPPY_STACKS_WORKSPACE_DIR:-}" ]]; then
92
253
  echo "$HAPPY_STACKS_WORKSPACE_DIR"
@@ -112,18 +273,33 @@ resolve_main_env_file() {
112
273
  echo "$explicit"
113
274
  return
114
275
  fi
115
- local main="$HOME/.happy/stacks/main/env"
276
+
277
+ local storage_root
278
+ storage_root="$(resolve_stacks_storage_root)"
279
+ local main="$storage_root/main/env"
116
280
  if [[ -f "$main" ]]; then
117
281
  echo "$main"
118
282
  return
119
283
  fi
284
+ # Legacy stacks location (pre-migration).
285
+ local legacy="$HOME/.happy/local/stacks/main/env"
286
+ if [[ -f "$legacy" ]]; then
287
+ echo "$legacy"
288
+ return
289
+ fi
290
+ # Very old single-stack location (best-effort).
291
+ local legacy_single="$HOME/.happy/local/env"
292
+ if [[ -f "$legacy_single" ]]; then
293
+ echo "$legacy_single"
294
+ return
295
+ fi
120
296
  echo ""
121
297
  }
122
298
 
123
299
  resolve_main_port() {
124
300
  # Priority:
125
301
  # 1) explicit env var
126
- # 2) main stack env (~/.happy/stacks/main/env)
302
+ # 2) main stack env
127
303
  # 3) home env.local
128
304
  # 4) home .env
129
305
  # 4) fallback to HAPPY_LOCAL_PORT / 3005