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
@@ -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:
@@ -50,8 +190,18 @@ dotenv_get() {
50
190
  ' "$file" 2>/dev/null
51
191
  }
52
192
 
193
+ expand_home_path() {
194
+ local p="$1"
195
+ if [[ "$p" == "~/"* ]]; then
196
+ echo "$HOME/${p#~/}"
197
+ return
198
+ fi
199
+ echo "$p"
200
+ }
201
+
53
202
  resolve_happy_local_dir() {
54
- 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}}"
55
205
 
56
206
  # If user provided a valid directory, keep it.
57
207
  if [[ -n "${HAPPY_LOCAL_DIR:-}" ]] && [[ -f "$HAPPY_LOCAL_DIR/extras/swiftbar/lib/utils.sh" ]]; then
@@ -69,24 +219,194 @@ resolve_happy_local_dir() {
69
219
  echo "$home"
70
220
  }
71
221
 
222
+ resolve_stacks_storage_root() {
223
+ # Priority:
224
+ # 1) explicit env var
225
+ # 2) home env.local
226
+ # 3) home .env (canonical pointer file, written by `happys init`)
227
+ # 4) default to ~/.happy/stacks
228
+ if [[ -n "${HAPPY_STACKS_STORAGE_DIR:-}" ]]; then
229
+ echo "$(expand_home_path "$HAPPY_STACKS_STORAGE_DIR")"
230
+ return
231
+ fi
232
+ if [[ -n "${HAPPY_LOCAL_STORAGE_DIR:-}" ]]; then
233
+ echo "$(expand_home_path "$HAPPY_LOCAL_STORAGE_DIR")"
234
+ return
235
+ fi
236
+
237
+ local p
238
+ p="$(dotenv_get "$HAPPY_LOCAL_DIR/env.local" "HAPPY_STACKS_STORAGE_DIR")"
239
+ [[ -z "$p" ]] && p="$(dotenv_get "$HAPPY_LOCAL_DIR/env.local" "HAPPY_LOCAL_STORAGE_DIR")"
240
+ [[ -z "$p" ]] && p="$(dotenv_get "$HAPPY_LOCAL_DIR/.env" "HAPPY_STACKS_STORAGE_DIR")"
241
+ [[ -z "$p" ]] && p="$(dotenv_get "$HAPPY_LOCAL_DIR/.env" "HAPPY_LOCAL_STORAGE_DIR")"
242
+ if [[ -n "$p" ]]; then
243
+ echo "$(expand_home_path "$p")"
244
+ return
245
+ fi
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
+
253
+ echo "$HOME/.happy/stacks"
254
+ }
255
+
256
+ resolve_stack_env_file() {
257
+ local stack_name="${1:-main}"
258
+ local storage_root
259
+ storage_root="$(resolve_stacks_storage_root)"
260
+
261
+ local primary="${storage_root}/${stack_name}/env"
262
+ if [[ -f "$primary" ]]; then
263
+ echo "$primary"
264
+ return
265
+ fi
266
+
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
273
+ fi
274
+
275
+ # Very old single-stack location (best-effort).
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
283
+ fi
284
+ fi
285
+
286
+ echo "$primary"
287
+ }
288
+
289
+ resolve_stack_base_dir() {
290
+ local stack_name="${1:-main}"
291
+ local env_file="${2:-}"
292
+ if [[ -z "$env_file" ]]; then
293
+ env_file="$(resolve_stack_env_file "$stack_name")"
294
+ fi
295
+ # If the env file exists, its parent directory is the stack base dir for all supported layouts.
296
+ if [[ -n "$env_file" ]] && [[ -f "$env_file" ]]; then
297
+ dirname "$env_file"
298
+ return
299
+ fi
300
+ local storage_root
301
+ storage_root="$(resolve_stacks_storage_root)"
302
+ echo "${storage_root}/${stack_name}"
303
+ }
304
+
305
+ resolve_stack_cli_home_dir() {
306
+ local stack_name="${1:-main}"
307
+ local env_file="${2:-}"
308
+ if [[ -z "$env_file" ]]; then
309
+ env_file="$(resolve_stack_env_file "$stack_name")"
310
+ fi
311
+ local cli_home=""
312
+ if [[ -n "$env_file" ]] && [[ -f "$env_file" ]]; then
313
+ cli_home="$(dotenv_get "$env_file" "HAPPY_STACKS_CLI_HOME_DIR")"
314
+ [[ -z "$cli_home" ]] && cli_home="$(dotenv_get "$env_file" "HAPPY_LOCAL_CLI_HOME_DIR")"
315
+ fi
316
+ if [[ -n "$cli_home" ]]; then
317
+ echo "$(expand_home_path "$cli_home")"
318
+ return
319
+ fi
320
+ local base_dir
321
+ base_dir="$(resolve_stack_base_dir "$stack_name" "$env_file")"
322
+ echo "${base_dir}/cli"
323
+ }
324
+
325
+ resolve_stack_label() {
326
+ local stack_name="${1:-main}"
327
+ local primary="com.happy.stacks"
328
+ local legacy="com.happy.local"
329
+ if [[ "$stack_name" != "main" ]]; then
330
+ primary="com.happy.stacks.${stack_name}"
331
+ legacy="com.happy.local.${stack_name}"
332
+ fi
333
+ if swiftbar_is_sandboxed; then
334
+ # Never inspect global LaunchAgents in sandbox mode.
335
+ echo "$primary"
336
+ return
337
+ fi
338
+ local primary_plist="$HOME/Library/LaunchAgents/${primary}.plist"
339
+ local legacy_plist="$HOME/Library/LaunchAgents/${legacy}.plist"
340
+ if [[ -f "$primary_plist" ]]; then
341
+ echo "$primary"
342
+ return
343
+ fi
344
+ if [[ -f "$legacy_plist" ]]; then
345
+ echo "$legacy"
346
+ return
347
+ fi
348
+ echo "$primary"
349
+ }
350
+
72
351
  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"
352
+ # Back-compat: historically this was "pnpm", but the plugin now runs `happys` via wrapper scripts.
353
+ local wrapper="$HAPPY_LOCAL_DIR/extras/swiftbar/happys.sh"
75
354
  if [[ -x "$wrapper" ]]; then
76
355
  echo "$wrapper"
77
356
  return
78
357
  fi
79
358
 
80
- local global_happys
81
- global_happys="$(command -v happys 2>/dev/null || true)"
82
- if [[ -n "$global_happys" ]]; then
83
- echo "$global_happys"
359
+ # Older installs.
360
+ wrapper="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm.sh"
361
+ if [[ -x "$wrapper" ]]; then
362
+ echo "$wrapper"
84
363
  return
85
364
  fi
86
365
 
366
+ local global_happys
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
373
+ fi
374
+
87
375
  echo ""
88
376
  }
89
377
 
378
+ resolve_node_bin() {
379
+ # Prefer explicit env vars first.
380
+ if [[ -n "${HAPPY_STACKS_NODE:-}" ]] && [[ -x "${HAPPY_STACKS_NODE:-}" ]]; then
381
+ echo "$HAPPY_STACKS_NODE"
382
+ return
383
+ fi
384
+ if [[ -n "${HAPPY_LOCAL_NODE:-}" ]] && [[ -x "${HAPPY_LOCAL_NODE:-}" ]]; then
385
+ echo "$HAPPY_LOCAL_NODE"
386
+ return
387
+ fi
388
+
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}}"
392
+ local env_file="$home/.env"
393
+ if [[ -f "$env_file" ]]; then
394
+ local v
395
+ v="$(dotenv_get "$env_file" "HAPPY_STACKS_NODE")"
396
+ if [[ -n "$v" ]] && [[ -x "$v" ]]; then
397
+ echo "$v"
398
+ return
399
+ fi
400
+ v="$(dotenv_get "$env_file" "HAPPY_LOCAL_NODE")"
401
+ if [[ -n "$v" ]] && [[ -x "$v" ]]; then
402
+ echo "$v"
403
+ return
404
+ fi
405
+ fi
406
+
407
+ command -v node 2>/dev/null || true
408
+ }
409
+
90
410
  resolve_workspace_dir() {
91
411
  if [[ -n "${HAPPY_STACKS_WORKSPACE_DIR:-}" ]]; then
92
412
  echo "$HAPPY_STACKS_WORKSPACE_DIR"
@@ -112,21 +432,39 @@ resolve_main_env_file() {
112
432
  echo "$explicit"
113
433
  return
114
434
  fi
115
- local main="$HOME/.happy/stacks/main/env"
435
+
436
+ local storage_root
437
+ storage_root="$(resolve_stacks_storage_root)"
438
+ local main="$storage_root/main/env"
116
439
  if [[ -f "$main" ]]; then
117
440
  echo "$main"
118
441
  return
119
442
  fi
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
456
+ fi
120
457
  echo ""
121
458
  }
122
459
 
123
460
  resolve_main_port() {
124
461
  # Priority:
125
462
  # 1) explicit env var
126
- # 2) main stack env (~/.happy/stacks/main/env)
463
+ # 2) main stack env
127
464
  # 3) home env.local
128
465
  # 4) home .env
129
- # 4) fallback to HAPPY_LOCAL_PORT / 3005
466
+ # 5) runtime state (ephemeral stacks)
467
+ # 6) fallback to HAPPY_LOCAL_PORT / 3005
130
468
  if [[ -n "${HAPPY_LOCAL_SERVER_PORT:-}" ]]; then
131
469
  echo "$HAPPY_LOCAL_SERVER_PORT"
132
470
  return
@@ -162,9 +500,98 @@ resolve_main_port() {
162
500
  return
163
501
  fi
164
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
+
165
513
  echo "${HAPPY_LOCAL_PORT:-3005}"
166
514
  }
167
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
+
168
595
  resolve_main_server_component() {
169
596
  if [[ -n "${HAPPY_LOCAL_SERVER_COMPONENT:-}" ]]; then
170
597
  echo "$HAPPY_LOCAL_SERVER_COMPONENT"
@@ -203,3 +630,35 @@ resolve_main_server_component() {
203
630
 
204
631
  echo "happy-server-light"
205
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
+ }
@@ -1,125 +1,5 @@
1
1
  #!/bin/bash
2
2
  set -euo pipefail
3
3
 
4
- # Open preferred terminal and run a happys command.
5
- #
6
- # Preference order follows wt shell semantics:
7
- # - HAPPY_LOCAL_WT_TERMINAL=ghostty|iterm|terminal|current
8
- # (also accepts "auto" which tries ghostty->iterm->terminal->current)
9
- #
10
- # Notes:
11
- # - iTerm / Terminal: we run the command automatically via AppleScript.
12
- # - Ghostty: best-effort; if we can't run the command, we open Ghostty in the dir and copy the command to clipboard.
13
-
14
- HAPPY_STACKS_HOME_DIR="${HAPPY_STACKS_HOME_DIR:-$HOME/.happy-stacks}"
15
- HAPPY_LOCAL_DIR="${HAPPY_LOCAL_DIR:-$HAPPY_STACKS_HOME_DIR}"
16
-
17
- WORKDIR="${HAPPY_STACKS_WORKSPACE_DIR:-$HAPPY_STACKS_HOME_DIR/workspace}"
18
- if [[ ! -d "$WORKDIR" ]]; then
19
- WORKDIR="$HOME"
20
- fi
21
-
22
- PNPM_SH="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm.sh"
23
- if [[ ! -x "$PNPM_SH" ]]; then
24
- echo "missing happys wrapper: $PNPM_SH" >&2
25
- exit 1
26
- fi
27
-
28
- pref_raw="$(echo "${HAPPY_STACKS_WT_TERMINAL:-${HAPPY_LOCAL_WT_TERMINAL:-auto}}" | tr '[:upper:]' '[:lower:]')"
29
- pref="$pref_raw"
30
- if [[ "$pref" == "" ]]; then pref="auto"; fi
31
-
32
- cmd=( "$PNPM_SH" "$@" )
33
-
34
- escape_for_osascript_string() {
35
- # Escape for inclusion inside an AppleScript string literal.
36
- # (We generate: write text "<cmd>")
37
- local s="$1"
38
- s="${s//\\/\\\\}"
39
- s="${s//\"/\\\"}"
40
- echo "$s"
41
- }
42
-
43
- shell_cmd() {
44
- # Build a zsh command that cds and runs happys (via wrapper), leaving the shell open.
45
- local joined=""
46
- local q
47
- joined="cd \"${WORKDIR//\"/\\\"}\"; "
48
- for q in "${cmd[@]}"; do
49
- # Basic shell quoting
50
- if [[ "$q" =~ [[:space:]\\"\'\$\`\!\&\|\;\<\>\(\)\[\]\{\}] ]]; then
51
- joined+="'${q//\'/\'\\\'\'}' "
52
- else
53
- joined+="$q "
54
- fi
55
- done
56
- joined+="; echo; echo \"[happy-stacks] done\"; exec /bin/zsh -i"
57
- echo "$joined"
58
- }
59
-
60
- run_iterm() {
61
- if ! command -v osascript >/dev/null 2>&1; then
62
- return 1
63
- fi
64
- local s
65
- s="$(shell_cmd)"
66
- s="$(escape_for_osascript_string "$s")"
67
- osascript \
68
- -e 'tell application "iTerm" to activate' \
69
- -e 'tell application "iTerm" to create window with default profile' \
70
- -e "tell application \"iTerm\" to tell current session of current window to write text \"${s}\"" >/dev/null
71
- }
72
-
73
- run_terminal_app() {
74
- if ! command -v osascript >/dev/null 2>&1; then
75
- return 1
76
- fi
77
- local s
78
- s="$(shell_cmd)"
79
- # Terminal.app uses do script.
80
- s="$(escape_for_osascript_string "$s")"
81
- osascript \
82
- -e 'tell application "Terminal" to activate' \
83
- -e "tell application \"Terminal\" to do script \"${s}\"" >/dev/null
84
- }
85
-
86
- run_ghostty() {
87
- if ! command -v ghostty >/dev/null 2>&1; then
88
- return 1
89
- fi
90
-
91
- # Best-effort: try to run the command. If ghostty doesn't support -e on this system,
92
- # fall back to opening the dir and copying the command.
93
- local s
94
- s="$(shell_cmd)"
95
- if ghostty --working-directory "$WORKDIR" -e /bin/zsh -lc "$s" >/dev/null 2>&1; then
96
- return 0
97
- fi
98
-
99
- # Fallback: open in dir and copy command for manual paste.
100
- echo -n "$s" | pbcopy 2>/dev/null || true
101
- ghostty --working-directory "$WORKDIR" >/dev/null 2>&1 || true
102
- return 0
103
- }
104
-
105
- try_one() {
106
- local t="$1"
107
- case "$t" in
108
- ghostty) run_ghostty ;;
109
- iterm) run_iterm ;;
110
- terminal) run_terminal_app ;;
111
- current) ( cd "$WORKDIR"; exec "${cmd[@]}" ) ;;
112
- *) return 1 ;;
113
- esac
114
- }
115
-
116
- if [[ "$pref" == "auto" ]]; then
117
- for t in ghostty iterm terminal current; do
118
- if try_one "$t"; then
119
- exit 0
120
- fi
121
- done
122
- exit 1
123
- fi
124
-
125
- try_one "$pref"
4
+ # Back-compat wrapper. Use `happys-term.sh` for new installs.
5
+ exec "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/happys-term.sh" "$@"
@@ -2,20 +2,10 @@
2
2
  set -euo pipefail
3
3
 
4
4
  # Back-compat wrapper for SwiftBar menu actions.
5
- # Historically this executed `pnpm` in the cloned repo; it now executes `happys`.
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
- HAPPYS_BIN="$HAPPY_LOCAL_DIR/bin/happys"
11
- if [[ ! -x "$HAPPYS_BIN" ]]; then
12
- HAPPYS_BIN="$(command -v happys 2>/dev/null || true)"
13
- fi
14
-
15
- if [[ -z "${HAPPYS_BIN:-}" ]]; then
16
- echo "happys not found (run: npx happy-stacks init, or npm i -g happy-stacks)" >&2
17
- exit 1
18
- fi
19
-
20
- exec "$HAPPYS_BIN" "$@"
21
-
11
+ exec "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/happys.sh" "$@"