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.
- package/README.md +22 -4
- package/bin/happys.mjs +76 -5
- package/docs/server-flavors.md +61 -2
- package/docs/stacks.md +16 -4
- package/extras/swiftbar/auth-login.sh +5 -5
- package/extras/swiftbar/happy-stacks.5s.sh +83 -41
- package/extras/swiftbar/happys-term.sh +151 -0
- package/extras/swiftbar/happys.sh +52 -0
- package/extras/swiftbar/lib/render.sh +74 -56
- package/extras/swiftbar/lib/system.sh +37 -6
- package/extras/swiftbar/lib/utils.sh +180 -4
- package/extras/swiftbar/pnpm-term.sh +2 -122
- package/extras/swiftbar/pnpm.sh +2 -13
- package/extras/swiftbar/set-server-flavor.sh +8 -8
- package/extras/swiftbar/wt-pr.sh +1 -1
- package/package.json +1 -1
- package/scripts/auth.mjs +374 -3
- package/scripts/daemon.mjs +78 -11
- package/scripts/dev.mjs +122 -17
- package/scripts/init.mjs +238 -32
- package/scripts/migrate.mjs +292 -0
- package/scripts/mobile.mjs +51 -19
- package/scripts/run.mjs +118 -26
- package/scripts/service.mjs +176 -37
- package/scripts/stack.mjs +665 -22
- package/scripts/stop.mjs +157 -0
- package/scripts/tailscale.mjs +147 -21
- package/scripts/typecheck.mjs +145 -0
- package/scripts/ui_gateway.mjs +248 -0
- package/scripts/uninstall.mjs +3 -3
- package/scripts/utils/cli_registry.mjs +23 -0
- package/scripts/utils/config.mjs +9 -1
- package/scripts/utils/env.mjs +37 -15
- package/scripts/utils/expo.mjs +94 -0
- package/scripts/utils/happy_server_infra.mjs +430 -0
- package/scripts/utils/pm.mjs +11 -2
- package/scripts/utils/ports.mjs +51 -13
- package/scripts/utils/proc.mjs +46 -5
- package/scripts/utils/server.mjs +37 -0
- package/scripts/utils/stack_stop.mjs +206 -0
- package/scripts/utils/validate.mjs +42 -1
- 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 "
|
|
25
|
-
if [[ "$level" == "orange" ]]; then echo "
|
|
26
|
-
echo "
|
|
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
|
|
64
|
-
print_item "$prefix" "$label | sfimage=$sf
|
|
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/
|
|
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/
|
|
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
|
-
|
|
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
|
|
212
|
+
local env_file
|
|
213
|
+
env_file="$(resolve_stack_env_file "$stack_name")"
|
|
198
214
|
local port
|
|
199
|
-
port="$(dotenv_get "$env_file" "
|
|
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
|
|
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
|
|
231
|
-
print_item "$prefix" "Autostart | sfimage=$sf
|
|
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
|
|
296
|
-
print_item "$prefix" "Tailscale | sfimage=$sf
|
|
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/
|
|
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/
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
461
|
-
|
|
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/
|
|
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
|
-
|
|
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" == "$
|
|
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
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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" "$
|
|
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
|
|
580
|
-
|
|
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
|
-
|
|
589
|
-
|
|
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=
|
|
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/
|
|
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/
|
|
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 |
|
|
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="$(
|
|
134
|
-
httpPort="$(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
74
|
-
local wrapper="$HAPPY_LOCAL_DIR/extras/swiftbar/
|
|
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
|
-
|
|
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
|
|
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
|