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.
- package/README.md +130 -74
- package/bin/happys.mjs +140 -9
- package/docs/edison.md +381 -0
- package/docs/happy-development.md +733 -0
- package/docs/menubar.md +54 -0
- package/docs/paths-and-env.md +141 -0
- package/docs/server-flavors.md +61 -2
- package/docs/stacks.md +55 -4
- package/extras/swiftbar/auth-login.sh +10 -7
- package/extras/swiftbar/git-cache-refresh.sh +130 -0
- package/extras/swiftbar/happy-stacks.5s.sh +175 -83
- package/extras/swiftbar/happys-term.sh +128 -0
- package/extras/swiftbar/happys.sh +35 -0
- package/extras/swiftbar/install.sh +99 -13
- package/extras/swiftbar/lib/git.sh +309 -1
- package/extras/swiftbar/lib/icons.sh +2 -2
- package/extras/swiftbar/lib/render.sh +279 -132
- package/extras/swiftbar/lib/system.sh +64 -10
- package/extras/swiftbar/lib/utils.sh +469 -10
- package/extras/swiftbar/pnpm-term.sh +2 -122
- package/extras/swiftbar/pnpm.sh +4 -14
- package/extras/swiftbar/set-interval.sh +10 -5
- package/extras/swiftbar/set-server-flavor.sh +19 -10
- package/extras/swiftbar/wt-pr.sh +10 -3
- package/package.json +2 -1
- package/scripts/auth.mjs +833 -14
- package/scripts/build.mjs +24 -4
- package/scripts/cli-link.mjs +3 -3
- package/scripts/completion.mjs +15 -8
- package/scripts/daemon.mjs +200 -23
- package/scripts/dev.mjs +230 -57
- package/scripts/doctor.mjs +26 -21
- package/scripts/edison.mjs +1828 -0
- package/scripts/happy.mjs +3 -7
- package/scripts/init.mjs +275 -46
- package/scripts/install.mjs +14 -8
- package/scripts/lint.mjs +145 -0
- package/scripts/menubar.mjs +81 -8
- package/scripts/migrate.mjs +302 -0
- package/scripts/mobile.mjs +59 -21
- package/scripts/run.mjs +222 -43
- package/scripts/self.mjs +3 -7
- package/scripts/server_flavor.mjs +3 -3
- package/scripts/service.mjs +190 -38
- package/scripts/setup.mjs +790 -0
- package/scripts/setup_pr.mjs +182 -0
- package/scripts/stack.mjs +2273 -92
- package/scripts/stop.mjs +160 -0
- package/scripts/tailscale.mjs +164 -23
- package/scripts/test.mjs +144 -0
- package/scripts/tui.mjs +556 -0
- package/scripts/typecheck.mjs +145 -0
- package/scripts/ui_gateway.mjs +248 -0
- package/scripts/uninstall.mjs +21 -13
- package/scripts/utils/auth_files.mjs +58 -0
- package/scripts/utils/auth_login_ux.mjs +76 -0
- package/scripts/utils/auth_sources.mjs +12 -0
- package/scripts/utils/browser.mjs +22 -0
- package/scripts/utils/canonical_home.mjs +20 -0
- package/scripts/utils/{cli_registry.mjs → cli/cli_registry.mjs} +71 -0
- package/scripts/utils/{wizard.mjs → cli/wizard.mjs} +1 -1
- package/scripts/utils/config.mjs +13 -1
- package/scripts/utils/dev_auth_key.mjs +169 -0
- package/scripts/utils/dev_daemon.mjs +104 -0
- package/scripts/utils/dev_expo_web.mjs +112 -0
- package/scripts/utils/dev_server.mjs +183 -0
- package/scripts/utils/env.mjs +94 -23
- package/scripts/utils/env_file.mjs +36 -0
- package/scripts/utils/expo.mjs +96 -0
- package/scripts/utils/handy_master_secret.mjs +94 -0
- package/scripts/utils/happy_server_infra.mjs +484 -0
- package/scripts/utils/localhost_host.mjs +17 -0
- package/scripts/utils/ownership.mjs +135 -0
- package/scripts/utils/paths.mjs +5 -2
- package/scripts/utils/pm.mjs +132 -22
- package/scripts/utils/ports.mjs +51 -13
- package/scripts/utils/proc.mjs +75 -7
- package/scripts/utils/runtime.mjs +1 -3
- package/scripts/utils/sandbox.mjs +14 -0
- package/scripts/utils/server.mjs +61 -0
- package/scripts/utils/server_port.mjs +9 -0
- package/scripts/utils/server_urls.mjs +54 -0
- package/scripts/utils/stack_context.mjs +23 -0
- package/scripts/utils/stack_runtime_state.mjs +104 -0
- package/scripts/utils/stack_startup.mjs +208 -0
- package/scripts/utils/stack_stop.mjs +255 -0
- package/scripts/utils/stacks.mjs +38 -0
- package/scripts/utils/validate.mjs +42 -1
- package/scripts/utils/watch.mjs +63 -0
- package/scripts/utils/worktrees.mjs +57 -1
- package/scripts/where.mjs +14 -7
- package/scripts/worktrees.mjs +135 -15
- /package/scripts/utils/{args.mjs → cli/args.mjs} +0 -0
- /package/scripts/utils/{cli.mjs → cli/cli.mjs} +0 -0
- /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
|
|
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
|
|
74
|
-
local wrapper="$HAPPY_LOCAL_DIR/extras/swiftbar/
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
if [[ -
|
|
83
|
-
echo "$
|
|
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
|
-
|
|
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
|
|
463
|
+
# 2) main stack env
|
|
127
464
|
# 3) home env.local
|
|
128
465
|
# 4) home .env
|
|
129
|
-
#
|
|
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
|
-
#
|
|
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" "$@"
|
package/extras/swiftbar/pnpm.sh
CHANGED
|
@@ -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
|
|
5
|
+
# Historically this executed `pnpm`; now it delegates to `happys.sh`.
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
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" "$@"
|