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
@@ -17,6 +17,146 @@ shorten_path() {
17
17
  shorten_text "$pretty" "$max"
18
18
  }
19
19
 
20
+ swiftbar_is_sandboxed() {
21
+ [[ -n "${HAPPY_STACKS_SANDBOX_DIR:-}" ]]
22
+ }
23
+
24
+ swiftbar_profile_enabled() {
25
+ [[ "${HAPPY_STACKS_SWIFTBAR_PROFILE:-}" == "1" || "${HAPPY_LOCAL_SWIFTBAR_PROFILE:-}" == "1" ]]
26
+ }
27
+
28
+ swiftbar_now_ms() {
29
+ # macOS `date` doesn't support %N, so use Time::HiRes when available.
30
+ if command -v perl >/dev/null 2>&1; then
31
+ perl -MTime::HiRes=time -e 'printf("%.0f\n", time()*1000)'
32
+ return
33
+ fi
34
+ if command -v python3 >/dev/null 2>&1; then
35
+ python3 -c 'import time; print(int(time.time()*1000))'
36
+ return
37
+ fi
38
+ # Fallback: seconds granularity.
39
+ echo $(( $(date +%s 2>/dev/null || echo 0) * 1000 ))
40
+ }
41
+
42
+ swiftbar_profile_log_file() {
43
+ # Keep logs in the home install by default (stable across repos/worktrees).
44
+ local canonical="${HAPPY_STACKS_CANONICAL_HOME_DIR:-${HAPPY_LOCAL_CANONICAL_HOME_DIR:-$HOME/.happy-stacks}}"
45
+ local home="${HAPPY_STACKS_HOME_DIR:-${HAPPY_LOCAL_DIR:-$canonical}}"
46
+ echo "${home}/cache/swiftbar/profile.log"
47
+ }
48
+
49
+ swiftbar_profile_log() {
50
+ # Usage: swiftbar_profile_log "event" "k=v" "k2=v2" ...
51
+ swiftbar_profile_enabled || return 0
52
+
53
+ local log_file
54
+ log_file="$(swiftbar_profile_log_file)"
55
+ mkdir -p "$(dirname "$log_file")" 2>/dev/null || true
56
+
57
+ local ts
58
+ ts="$(swiftbar_now_ms)"
59
+ {
60
+ printf '%s\t%s' "$ts" "$1"
61
+ shift || true
62
+ for kv in "$@"; do
63
+ printf '\t%s' "$kv"
64
+ done
65
+ printf '\n'
66
+ } >>"$log_file" 2>/dev/null || true
67
+ }
68
+
69
+ swiftbar_profile_time() {
70
+ # Usage: swiftbar_profile_time <label> -- <command...>
71
+ swiftbar_profile_enabled || { shift; [[ "${1:-}" == "--" ]] && shift; "$@"; return $?; }
72
+ local label="$1"
73
+ shift
74
+ [[ "${1:-}" == "--" ]] && shift
75
+
76
+ local t0 t1 rc
77
+ t0="$(swiftbar_now_ms)"
78
+ "$@"
79
+ rc=$?
80
+ t1="$(swiftbar_now_ms)"
81
+ swiftbar_profile_log "time" "label=${label}" "ms=$((t1 - t0))" "rc=${rc}"
82
+ return $rc
83
+ }
84
+
85
+ swiftbar_cache_hash12() {
86
+ # Usage: swiftbar_cache_hash12 "string"
87
+ local s="$1"
88
+ if command -v md5 >/dev/null 2>&1; then
89
+ md5 -q -s "$s" 2>/dev/null | head -c 12
90
+ return
91
+ fi
92
+ if command -v shasum >/dev/null 2>&1; then
93
+ printf '%s' "$s" | shasum -a 256 2>/dev/null | awk '{print substr($1,1,12)}'
94
+ return
95
+ fi
96
+ # Last resort (not cryptographic): length + a sanitized prefix.
97
+ printf '%s' "${#s}-$(echo "$s" | tr -cd '[:alnum:]' | head -c 10)"
98
+ }
99
+
100
+ swiftbar_hash() {
101
+ # Usage: swiftbar_hash "string"
102
+ local s="$1"
103
+ if command -v md5 >/dev/null 2>&1; then
104
+ md5 -q -s "$s" 2>/dev/null || true
105
+ return
106
+ fi
107
+ if command -v shasum >/dev/null 2>&1; then
108
+ printf '%s' "$s" | shasum -a 256 2>/dev/null | awk '{print $1}'
109
+ return
110
+ fi
111
+ printf '%s' "$(swiftbar_cache_hash12 "$s")"
112
+ }
113
+
114
+ swiftbar_run_cache_dir() {
115
+ # Per-process (per-refresh) cache. Avoid persisting across SwiftBar refreshes.
116
+ local base="${TMPDIR:-/tmp}"
117
+ local dir="${base%/}/happy-stacks-swiftbar-cache-${UID:-0}-$$"
118
+ mkdir -p "$dir" 2>/dev/null || true
119
+ echo "$dir"
120
+ }
121
+
122
+ swiftbar_cache_file_for_key() {
123
+ local key="$1"
124
+ local dir
125
+ dir="$(swiftbar_run_cache_dir)"
126
+ echo "${dir}/$(swiftbar_cache_hash12 "$key").cache"
127
+ }
128
+
129
+ swiftbar_cache_get() {
130
+ # Usage: swiftbar_cache_get <key>
131
+ # Output: cached stdout (may be empty). Exit status: cached rc.
132
+ local key="$1"
133
+ local f
134
+ f="$(swiftbar_cache_file_for_key "$key")"
135
+ # Important: return a distinct code on cache-miss so callers can distinguish from cached rc=1.
136
+ [[ -f "$f" ]] || return 111
137
+ local rc_line rc
138
+ rc_line="$(head -n 1 "$f" 2>/dev/null || true)"
139
+ rc="${rc_line#rc:}"
140
+ tail -n +2 "$f" 2>/dev/null || true
141
+ [[ "$rc" =~ ^[0-9]+$ ]] || rc=0
142
+ return "$rc"
143
+ }
144
+
145
+ swiftbar_cache_set() {
146
+ # Usage: swiftbar_cache_set <key> <rc> <stdout>
147
+ local key="$1"
148
+ local rc="$2"
149
+ local out="${3:-}"
150
+ local f
151
+ f="$(swiftbar_cache_file_for_key "$key")"
152
+ {
153
+ printf 'rc:%s\n' "${rc:-0}"
154
+ printf '%s' "$out"
155
+ # Keep files line-friendly.
156
+ [[ "$out" == *$'\n' ]] || printf '\n'
157
+ } >"$f" 2>/dev/null || true
158
+ }
159
+
20
160
  dotenv_get() {
21
161
  # Usage: dotenv_get /path/to/env KEY
22
162
  # Notes:
@@ -60,7 +200,8 @@ expand_home_path() {
60
200
  }
61
201
 
62
202
  resolve_happy_local_dir() {
63
- local home="${HAPPY_STACKS_HOME_DIR:-$HOME/.happy-stacks}"
203
+ local canonical="${HAPPY_STACKS_CANONICAL_HOME_DIR:-${HAPPY_LOCAL_CANONICAL_HOME_DIR:-$HOME/.happy-stacks}}"
204
+ local home="${HAPPY_STACKS_HOME_DIR:-${HAPPY_LOCAL_DIR:-$canonical}}"
64
205
 
65
206
  # If user provided a valid directory, keep it.
66
207
  if [[ -n "${HAPPY_LOCAL_DIR:-}" ]] && [[ -f "$HAPPY_LOCAL_DIR/extras/swiftbar/lib/utils.sh" ]]; then
@@ -103,6 +244,12 @@ resolve_stacks_storage_root() {
103
244
  return
104
245
  fi
105
246
 
247
+ # In sandbox mode, avoid falling back to the user's real ~/.happy/stacks.
248
+ if swiftbar_is_sandboxed; then
249
+ echo "${HAPPY_STACKS_STORAGE_DIR:-${HAPPY_STACKS_SANDBOX_DIR%/}/storage}"
250
+ return
251
+ fi
252
+
106
253
  echo "$HOME/.happy/stacks"
107
254
  }
108
255
 
@@ -117,18 +264,22 @@ resolve_stack_env_file() {
117
264
  return
118
265
  fi
119
266
 
120
- local legacy="$HOME/.happy/local/stacks/${stack_name}/env"
121
- if [[ -f "$legacy" ]]; then
122
- echo "$legacy"
123
- return
267
+ if ! swiftbar_is_sandboxed; then
268
+ local legacy="$HOME/.happy/local/stacks/${stack_name}/env"
269
+ if [[ -f "$legacy" ]]; then
270
+ echo "$legacy"
271
+ return
272
+ fi
124
273
  fi
125
274
 
126
275
  # 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
276
+ if ! swiftbar_is_sandboxed; then
277
+ if [[ "$stack_name" == "main" ]]; then
278
+ local legacy_single="$HOME/.happy/local/env"
279
+ if [[ -f "$legacy_single" ]]; then
280
+ echo "$legacy_single"
281
+ return
282
+ fi
132
283
  fi
133
284
  fi
134
285
 
@@ -179,6 +330,11 @@ resolve_stack_label() {
179
330
  primary="com.happy.stacks.${stack_name}"
180
331
  legacy="com.happy.local.${stack_name}"
181
332
  fi
333
+ if swiftbar_is_sandboxed; then
334
+ # Never inspect global LaunchAgents in sandbox mode.
335
+ echo "$primary"
336
+ return
337
+ fi
182
338
  local primary_plist="$HOME/Library/LaunchAgents/${primary}.plist"
183
339
  local legacy_plist="$HOME/Library/LaunchAgents/${legacy}.plist"
184
340
  if [[ -f "$primary_plist" ]]; then
@@ -208,10 +364,12 @@ resolve_pnpm_bin() {
208
364
  fi
209
365
 
210
366
  local global_happys
211
- global_happys="$(command -v happys 2>/dev/null || true)"
212
- if [[ -n "$global_happys" ]]; then
213
- echo "$global_happys"
214
- return
367
+ if ! swiftbar_is_sandboxed; then
368
+ global_happys="$(command -v happys 2>/dev/null || true)"
369
+ if [[ -n "$global_happys" ]]; then
370
+ echo "$global_happys"
371
+ return
372
+ fi
215
373
  fi
216
374
 
217
375
  echo ""
@@ -228,8 +386,9 @@ resolve_node_bin() {
228
386
  return
229
387
  fi
230
388
 
231
- # Fall back to reading ~/.happy-stacks/.env (written by `happys init`).
232
- local home="${HAPPY_STACKS_HOME_DIR:-$HOME/.happy-stacks}"
389
+ # Fall back to reading the canonical pointer env (written by `happys init`).
390
+ local canonical="${HAPPY_STACKS_CANONICAL_HOME_DIR:-${HAPPY_LOCAL_CANONICAL_HOME_DIR:-$HOME/.happy-stacks}}"
391
+ local home="${HAPPY_STACKS_HOME_DIR:-${HAPPY_LOCAL_DIR:-$canonical}}"
233
392
  local env_file="$home/.env"
234
393
  if [[ -f "$env_file" ]]; then
235
394
  local v
@@ -281,17 +440,19 @@ resolve_main_env_file() {
281
440
  echo "$main"
282
441
  return
283
442
  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
443
+ if ! swiftbar_is_sandboxed; then
444
+ # Legacy stacks location (pre-migration).
445
+ local legacy="$HOME/.happy/local/stacks/main/env"
446
+ if [[ -f "$legacy" ]]; then
447
+ echo "$legacy"
448
+ return
449
+ fi
450
+ # Very old single-stack location (best-effort).
451
+ local legacy_single="$HOME/.happy/local/env"
452
+ if [[ -f "$legacy_single" ]]; then
453
+ echo "$legacy_single"
454
+ return
455
+ fi
295
456
  fi
296
457
  echo ""
297
458
  }
@@ -302,7 +463,8 @@ resolve_main_port() {
302
463
  # 2) main stack env
303
464
  # 3) home env.local
304
465
  # 4) home .env
305
- # 4) fallback to HAPPY_LOCAL_PORT / 3005
466
+ # 5) runtime state (ephemeral stacks)
467
+ # 6) fallback to HAPPY_LOCAL_PORT / 3005
306
468
  if [[ -n "${HAPPY_LOCAL_SERVER_PORT:-}" ]]; then
307
469
  echo "$HAPPY_LOCAL_SERVER_PORT"
308
470
  return
@@ -338,9 +500,98 @@ resolve_main_port() {
338
500
  return
339
501
  fi
340
502
 
503
+ # Runtime-only port overlay (ephemeral stacks): best-effort.
504
+ local base_dir state_file
505
+ base_dir="$(resolve_stack_base_dir main "$env_file")"
506
+ state_file="${base_dir}/stack.runtime.json"
507
+ p="$(resolve_runtime_server_port_from_state_file "$state_file")"
508
+ if [[ -n "$p" ]]; then
509
+ echo "$p"
510
+ return
511
+ fi
512
+
341
513
  echo "${HAPPY_LOCAL_PORT:-3005}"
342
514
  }
343
515
 
516
+ resolve_runtime_server_port_from_state_file() {
517
+ # Reads stack.runtime.json and returns ports.server, but only if ownerPid is alive.
518
+ # Output: port number or empty.
519
+ local state_file="$1"
520
+ [[ -n "$state_file" && -f "$state_file" ]] || return 0
521
+
522
+ local owner="" port=""
523
+
524
+ # Fast-path: parse our own JSON shape without spawning node (best-effort).
525
+ if command -v grep >/dev/null 2>&1; then
526
+ owner="$(grep -oE '"ownerPid"[[:space:]]*:[[:space:]]*[0-9]+' "$state_file" 2>/dev/null | head -1 | grep -oE '[0-9]+' || true)"
527
+ port="$(grep -oE '"server"[[:space:]]*:[[:space:]]*[0-9]+' "$state_file" 2>/dev/null | head -1 | grep -oE '[0-9]+' || true)"
528
+ fi
529
+
530
+ if [[ -z "$owner" || -z "$port" ]]; then
531
+ local node_bin
532
+ node_bin="$(resolve_node_bin)"
533
+ if [[ -n "$node_bin" && -x "$node_bin" ]]; then
534
+ local out
535
+ out="$(
536
+ "$node_bin" -e '
537
+ const fs=require("fs");
538
+ try {
539
+ const s=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));
540
+ const owner=String(s?.ownerPid ?? "");
541
+ const port=String(s?.ports?.server ?? "");
542
+ process.stdout.write(owner + "\t" + port);
543
+ } catch { process.stdout.write("\t"); }
544
+ ' "$state_file" 2>/dev/null || true
545
+ )"
546
+ IFS=$'\t' read -r owner port <<<"$out"
547
+ elif command -v python3 >/dev/null 2>&1; then
548
+ local out
549
+ out="$(
550
+ python3 -c 'import json,sys;
551
+ try:
552
+ s=json.load(open(sys.argv[1],"r"))
553
+ owner=str(s.get("ownerPid",""))
554
+ port=str((s.get("ports") or {}).get("server",""))
555
+ print(owner+"\\t"+port,end="")
556
+ except Exception:
557
+ print("\\t",end="")' "$state_file" 2>/dev/null || true
558
+ )"
559
+ IFS=$'\t' read -r owner port <<<"$out"
560
+ fi
561
+ fi
562
+
563
+ [[ "$owner" =~ ^[0-9]+$ ]] || owner=""
564
+ [[ "$port" =~ ^[0-9]+$ ]] || port=""
565
+ if [[ -n "$owner" ]] && kill -0 "$owner" 2>/dev/null; then
566
+ echo "$port"
567
+ fi
568
+ }
569
+
570
+ resolve_stack_server_port() {
571
+ # Usage: resolve_stack_server_port <stack_name> <env_file>
572
+ # Priority:
573
+ # - pinned port in env file
574
+ # - runtime port in stack.runtime.json (only if ownerPid alive)
575
+ local stack_name="${1:-main}"
576
+ local env_file="${2:-}"
577
+
578
+ local p=""
579
+ if [[ -n "$env_file" && -f "$env_file" ]]; then
580
+ p="$(dotenv_get "$env_file" "HAPPY_STACKS_SERVER_PORT")"
581
+ [[ -z "$p" ]] && p="$(dotenv_get "$env_file" "HAPPY_LOCAL_SERVER_PORT")"
582
+ fi
583
+ if [[ -n "$p" ]]; then
584
+ echo "$p"
585
+ return
586
+ fi
587
+
588
+ local base_dir state_file
589
+ base_dir="$(resolve_stack_base_dir "$stack_name" "$env_file")"
590
+ state_file="${base_dir}/stack.runtime.json"
591
+ p="$(resolve_runtime_server_port_from_state_file "$state_file")"
592
+ echo "$p"
593
+ }
594
+
344
595
  resolve_main_server_component() {
345
596
  if [[ -n "${HAPPY_LOCAL_SERVER_COMPONENT:-}" ]]; then
346
597
  echo "$HAPPY_LOCAL_SERVER_COMPONENT"
@@ -379,3 +630,35 @@ resolve_main_server_component() {
379
630
 
380
631
  echo "happy-server-light"
381
632
  }
633
+
634
+ resolve_menubar_mode() {
635
+ # selfhost | dev (default: dev)
636
+ local raw=""
637
+ if [[ -n "${HAPPY_LOCAL_MENUBAR_MODE:-}" ]]; then
638
+ raw="$HAPPY_LOCAL_MENUBAR_MODE"
639
+ elif [[ -n "${HAPPY_STACKS_MENUBAR_MODE:-}" ]]; then
640
+ raw="$HAPPY_STACKS_MENUBAR_MODE"
641
+ fi
642
+
643
+ local env_file
644
+ env_file="$(resolve_main_env_file)"
645
+ if [[ -z "$raw" && -n "$env_file" ]]; then
646
+ raw="$(dotenv_get "$env_file" "HAPPY_LOCAL_MENUBAR_MODE")"
647
+ [[ -z "$raw" ]] && raw="$(dotenv_get "$env_file" "HAPPY_STACKS_MENUBAR_MODE")"
648
+ fi
649
+
650
+ if [[ -z "$raw" ]]; then
651
+ raw="$(dotenv_get "$HAPPY_LOCAL_DIR/env.local" "HAPPY_LOCAL_MENUBAR_MODE")"
652
+ [[ -z "$raw" ]] && raw="$(dotenv_get "$HAPPY_LOCAL_DIR/env.local" "HAPPY_STACKS_MENUBAR_MODE")"
653
+ fi
654
+ if [[ -z "$raw" ]]; then
655
+ raw="$(dotenv_get "$HAPPY_LOCAL_DIR/.env" "HAPPY_LOCAL_MENUBAR_MODE")"
656
+ [[ -z "$raw" ]] && raw="$(dotenv_get "$HAPPY_LOCAL_DIR/.env" "HAPPY_STACKS_MENUBAR_MODE")"
657
+ fi
658
+
659
+ raw="$(echo "${raw:-}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
660
+ case "$raw" in
661
+ selfhost|self-host|self_host|host) echo "selfhost" ;;
662
+ *) echo "dev" ;;
663
+ esac
664
+ }
@@ -4,7 +4,8 @@ set -euo pipefail
4
4
  # Back-compat wrapper for SwiftBar menu actions.
5
5
  # Historically this executed `pnpm`; now it delegates to `happys.sh`.
6
6
 
7
- HAPPY_STACKS_HOME_DIR="${HAPPY_STACKS_HOME_DIR:-$HOME/.happy-stacks}"
7
+ CANONICAL_HOME_DIR="${HAPPY_STACKS_CANONICAL_HOME_DIR:-${HAPPY_LOCAL_CANONICAL_HOME_DIR:-$HOME/.happy-stacks}}"
8
+ HAPPY_STACKS_HOME_DIR="${HAPPY_STACKS_HOME_DIR:-$CANONICAL_HOME_DIR}"
8
9
  HAPPY_LOCAL_DIR="${HAPPY_LOCAL_DIR:-$HAPPY_STACKS_HOME_DIR}"
9
10
 
10
11
  exec "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/happys.sh" "$@"
@@ -26,13 +26,18 @@ if [[ -z "$PLUGIN_DIR" ]]; then
26
26
  fi
27
27
  mkdir -p "$PLUGIN_DIR"
28
28
 
29
- TARGET="$PLUGIN_DIR/happy-stacks.${INTERVAL}.sh"
30
- HAPPY_STACKS_HOME_DIR="${HAPPY_STACKS_HOME_DIR:-$HOME/.happy-stacks}"
31
- HAPPY_LOCAL_DIR="${HAPPY_LOCAL_DIR:-$HAPPY_STACKS_HOME_DIR}"
29
+ PLUGIN_BASENAME="${HAPPY_STACKS_SWIFTBAR_PLUGIN_BASENAME:-${HAPPY_LOCAL_SWIFTBAR_PLUGIN_BASENAME:-happy-stacks}}"
30
+ TARGET="$PLUGIN_DIR/${PLUGIN_BASENAME}.${INTERVAL}.sh"
31
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
32
+ DEFAULT_HOME_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
33
+
34
+ CANONICAL_HOME_DIR="${HAPPY_STACKS_CANONICAL_HOME_DIR:-${HAPPY_LOCAL_CANONICAL_HOME_DIR:-$DEFAULT_HOME_DIR}}"
35
+ HAPPY_LOCAL_DIR="${HAPPY_LOCAL_DIR:-${HAPPY_STACKS_HOME_DIR:-$CANONICAL_HOME_DIR}}"
36
+ HAPPY_STACKS_HOME_DIR="${HAPPY_STACKS_HOME_DIR:-$HAPPY_LOCAL_DIR}"
32
37
  SOURCE="${HAPPY_LOCAL_DIR}/extras/swiftbar/happy-stacks.5s.sh"
33
38
 
34
39
  # If a happy-stacks plugin already exists, rename it into place; otherwise copy from repo source.
35
- EXISTING="$(ls "$PLUGIN_DIR"/happy-stacks.*.sh 2>/dev/null | head -1 || true)"
40
+ EXISTING="$(ls "$PLUGIN_DIR"/"${PLUGIN_BASENAME}".*.sh 2>/dev/null | head -1 || true)"
36
41
  if [[ -n "$EXISTING" ]]; then
37
42
  if [[ "$EXISTING" != "$TARGET" ]]; then
38
43
  rm -f "$TARGET"
@@ -47,7 +52,7 @@ else
47
52
  fi
48
53
 
49
54
  # Remove any other intervals to avoid duplicates in SwiftBar.
50
- for f in "$PLUGIN_DIR"/happy-stacks.*.sh; do
55
+ for f in "$PLUGIN_DIR"/"${PLUGIN_BASENAME}".*.sh; do
51
56
  [[ "$f" == "$TARGET" ]] && continue
52
57
  rm -f "$f" || true
53
58
  done
@@ -24,8 +24,11 @@ if [[ "$FLAVOR" != "happy-server" && "$FLAVOR" != "happy-server-light" ]]; then
24
24
  exit 2
25
25
  fi
26
26
 
27
- HAPPY_STACKS_HOME_DIR="${HAPPY_STACKS_HOME_DIR:-$HOME/.happy-stacks}"
28
- HAPPY_LOCAL_DIR="${HAPPY_LOCAL_DIR:-$HAPPY_STACKS_HOME_DIR}"
27
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
28
+ DEFAULT_HOME_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
29
+
30
+ HAPPY_LOCAL_DIR="${HAPPY_LOCAL_DIR:-${HAPPY_STACKS_HOME_DIR:-$DEFAULT_HOME_DIR}}"
31
+ HAPPY_STACKS_HOME_DIR="${HAPPY_STACKS_HOME_DIR:-$HAPPY_LOCAL_DIR}"
29
32
 
30
33
  HAPPYS_BIN="$HAPPY_LOCAL_DIR/extras/swiftbar/happys.sh"
31
34
  if [[ ! -x "$HAPPYS_BIN" ]]; then
@@ -34,6 +37,9 @@ if [[ ! -x "$HAPPYS_BIN" ]]; then
34
37
  fi
35
38
 
36
39
  restart_main_service_best_effort() {
40
+ if [[ -n "${HAPPY_STACKS_SANDBOX_DIR:-}" ]]; then
41
+ return 0
42
+ fi
37
43
  "$HAPPYS_BIN" service:restart >/dev/null 2>&1 || true
38
44
  # If the installed LaunchAgent is still legacy/baked, reinstall so it persists only env-file pointer.
39
45
  "$HAPPYS_BIN" service:install >/dev/null 2>&1 || true
@@ -41,6 +47,9 @@ restart_main_service_best_effort() {
41
47
 
42
48
  restart_stack_service_best_effort() {
43
49
  local name="$1"
50
+ if [[ -n "${HAPPY_STACKS_SANDBOX_DIR:-}" ]]; then
51
+ return 0
52
+ fi
44
53
  "$HAPPYS_BIN" stack service:restart "$name" >/dev/null 2>&1 || true
45
54
  "$HAPPYS_BIN" stack service:install "$name" >/dev/null 2>&1 || true
46
55
  }
@@ -17,11 +17,18 @@ set -euo pipefail
17
17
  COMPONENT="${1:-}"
18
18
  STACK_NAME="${2:-}"
19
19
 
20
- HAPPY_STACKS_HOME_DIR="${HAPPY_STACKS_HOME_DIR:-$HOME/.happy-stacks}"
21
- HAPPY_LOCAL_DIR="${HAPPY_LOCAL_DIR:-$HAPPY_STACKS_HOME_DIR}"
20
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
+ DEFAULT_HOME_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
22
+
23
+ HAPPY_LOCAL_DIR="${HAPPY_LOCAL_DIR:-${HAPPY_STACKS_HOME_DIR:-$DEFAULT_HOME_DIR}}"
24
+ HAPPY_STACKS_HOME_DIR="${HAPPY_STACKS_HOME_DIR:-$HAPPY_LOCAL_DIR}"
22
25
 
23
26
  HAPPYS="$HAPPY_LOCAL_DIR/extras/swiftbar/happys.sh"
24
27
  if [[ ! -x "$HAPPYS" ]]; then
28
+ if [[ -n "${HAPPY_STACKS_SANDBOX_DIR:-}" ]]; then
29
+ echo "missing happys wrapper in sandbox: $HAPPYS" >&2
30
+ exit 1
31
+ fi
25
32
  HAPPYS="$(command -v happys 2>/dev/null || true)"
26
33
  fi
27
34
  if [[ -z "$HAPPYS" ]]; then
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "happy-stacks",
3
3
  "type": "module",
4
- "version": "0.1.2",
4
+ "version": "0.2.0",
5
5
  "packageManager": "pnpm@10.18.3",
6
6
  "bin": {
7
7
  "happys": "./bin/happys.mjs",
@@ -14,6 +14,7 @@
14
14
  "scripts/"
15
15
  ],
16
16
  "scripts": {
17
+ "setup": "node ./scripts/setup.mjs",
17
18
  "init": "node ./scripts/init.mjs",
18
19
  "uninstall": "node ./scripts/uninstall.mjs",
19
20
  "where": "node ./scripts/where.mjs",