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
@@ -8,13 +8,321 @@ is_git_repo() {
8
8
  [[ -n "$dir" && -d "$dir" && ( -d "$dir/.git" || -f "$dir/.git" ) ]]
9
9
  }
10
10
 
11
+ git_cache_dir() {
12
+ local canonical="${HAPPY_STACKS_CANONICAL_HOME_DIR:-${HAPPY_LOCAL_CANONICAL_HOME_DIR:-$HOME/.happy-stacks}}"
13
+ local home="${HAPPY_STACKS_HOME_DIR:-${HAPPY_LOCAL_DIR:-$canonical}}"
14
+ local dir="${home}/cache/swiftbar/git"
15
+ mkdir -p "$dir" 2>/dev/null || true
16
+ echo "$dir"
17
+ }
18
+
19
+ git_cache_ttl_sec() {
20
+ # Default: 6 hours.
21
+ local v="${HAPPY_STACKS_SWIFTBAR_GIT_TTL_SEC:-${HAPPY_LOCAL_SWIFTBAR_GIT_TTL_SEC:-21600}}"
22
+ [[ "$v" =~ ^[0-9]+$ ]] || v=21600
23
+ echo "$v"
24
+ }
25
+
26
+ git_cache_refresh_on_stale() {
27
+ [[ "${HAPPY_STACKS_SWIFTBAR_GIT_REFRESH_ON_STALE:-${HAPPY_LOCAL_SWIFTBAR_GIT_REFRESH_ON_STALE:-0}}" == "1" ]]
28
+ }
29
+
30
+ git_cache_auto_refresh_scope() {
31
+ # off | main | all
32
+ local s="${HAPPY_STACKS_SWIFTBAR_GIT_AUTO_REFRESH_SCOPE:-${HAPPY_LOCAL_SWIFTBAR_GIT_AUTO_REFRESH_SCOPE:-main}}"
33
+ s="$(echo "$s" | tr '[:upper:]' '[:lower:]')"
34
+ case "$s" in
35
+ off|none|0) echo "off" ;;
36
+ all) echo "all" ;;
37
+ *) echo "main" ;;
38
+ esac
39
+ }
40
+
41
+ git_cache_last_refresh_file() {
42
+ local scope="${1:-main}" # main|all|stack:<name>
43
+ local dir
44
+ dir="$(git_cache_dir)"
45
+ local key="last_refresh|${scope}"
46
+ echo "${dir}/$(swiftbar_cache_hash12 "$key").last"
47
+ }
48
+
49
+ git_cache_background_refresh_lockdir() {
50
+ local scope="${1:-main}"
51
+ local dir
52
+ dir="$(git_cache_dir)"
53
+ local key="bg_refresh_lock|${scope}"
54
+ echo "${dir}/$(swiftbar_cache_hash12 "$key").lock"
55
+ }
56
+
57
+ git_cache_touch_last_refresh() {
58
+ local scope="${1:-main}"
59
+ local f
60
+ f="$(git_cache_last_refresh_file "$scope")"
61
+ mkdir -p "$(dirname "$f")" 2>/dev/null || true
62
+ date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null >"$f" || true
63
+ touch "$f" 2>/dev/null || true
64
+ }
65
+
66
+ git_cache_age_since_last_refresh_sec() {
67
+ local scope="${1:-main}"
68
+ local f
69
+ f="$(git_cache_last_refresh_file "$scope")"
70
+ [[ -f "$f" ]] || { echo ""; return; }
71
+ local mtime now
72
+ mtime="$(stat -f %m "$f" 2>/dev/null || echo 0)"
73
+ now="$(date +%s 2>/dev/null || echo 0)"
74
+ if [[ "$mtime" =~ ^[0-9]+$ && "$now" =~ ^[0-9]+$ && "$now" -ge "$mtime" ]]; then
75
+ echo $((now - mtime))
76
+ else
77
+ echo ""
78
+ fi
79
+ }
80
+
81
+ git_cache_maybe_refresh_async() {
82
+ # Non-blocking cache refresh.
83
+ # Usage: git_cache_maybe_refresh_async <scope> <refresh_cmd...>
84
+ local scope="$1"
85
+ shift
86
+
87
+ local ttl age
88
+ ttl="$(git_cache_ttl_sec)"
89
+ age="$(git_cache_age_since_last_refresh_sec "$scope")"
90
+
91
+ # If never refreshed, treat as stale and allow.
92
+ if [[ -n "$age" && "$age" =~ ^[0-9]+$ && "$age" -le "$ttl" ]]; then
93
+ return 0
94
+ fi
95
+
96
+ local lockdir
97
+ lockdir="$(git_cache_background_refresh_lockdir "$scope")"
98
+ if [[ -d "$lockdir" ]]; then
99
+ # If lock is too old, break it (e.g. crashed refresh).
100
+ local lock_age
101
+ lock_age="$(git_cache_age_sec "$lockdir" 2>/dev/null || true)"
102
+ if [[ -n "$lock_age" && "$lock_age" =~ ^[0-9]+$ && "$lock_age" -gt 3600 ]]; then
103
+ rm -rf "$lockdir" 2>/dev/null || true
104
+ else
105
+ return 0
106
+ fi
107
+ fi
108
+
109
+ mkdir "$lockdir" 2>/dev/null || return 0
110
+ echo "$$" >"${lockdir}/pid" 2>/dev/null || true
111
+ date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null >"${lockdir}/started_at" || true
112
+
113
+ # Run in the background; on success, update last-refresh marker.
114
+ (
115
+ "$@" >/dev/null 2>&1 || true
116
+ git_cache_touch_last_refresh "$scope"
117
+ rm -rf "$lockdir" >/dev/null 2>&1 || true
118
+ ) >/dev/null 2>&1 &
119
+ }
120
+
121
+ git_cache_mode() {
122
+ # cached (default) | live
123
+ local m="${HAPPY_STACKS_SWIFTBAR_GIT_MODE:-${HAPPY_LOCAL_SWIFTBAR_GIT_MODE:-cached}}"
124
+ m="$(echo "$m" | tr '[:upper:]' '[:lower:]')"
125
+ [[ "$m" == "live" ]] && echo "live" || echo "cached"
126
+ }
127
+
128
+ git_cache_key() {
129
+ # Include context+stack because stacks can point components at different worktrees/dirs.
130
+ local context="$1"
131
+ local stack="$2"
132
+ local component="$3"
133
+ local active_dir="$4"
134
+ echo "ctx=${context}|stack=${stack}|comp=${component}|dir=${active_dir}"
135
+ }
136
+
137
+ git_cache_paths() {
138
+ # Usage: git_cache_paths <key>
139
+ # Output: meta<TAB>info<TAB>worktrees
140
+ local key="$1"
141
+ local dir
142
+ dir="$(git_cache_dir)"
143
+ local h
144
+ h="$(swiftbar_hash "$key")"
145
+ echo -e "${dir}/${h}.meta\t${dir}/${h}.info.tsv\t${dir}/${h}.worktrees.tsv"
146
+ }
147
+
148
+ git_cache_age_sec() {
149
+ local meta="$1"
150
+ [[ -f "$meta" ]] || { echo ""; return; }
151
+ local mtime now
152
+ mtime="$(stat -f %m "$meta" 2>/dev/null || echo 0)"
153
+ now="$(date +%s 2>/dev/null || echo 0)"
154
+ if [[ "$mtime" =~ ^[0-9]+$ && "$now" =~ ^[0-9]+$ && "$now" -ge "$mtime" ]]; then
155
+ echo $((now - mtime))
156
+ else
157
+ echo ""
158
+ fi
159
+ }
160
+
161
+ git_cache_is_fresh() {
162
+ local meta="$1"
163
+ local ttl
164
+ ttl="$(git_cache_ttl_sec)"
165
+ local age
166
+ age="$(git_cache_age_sec "$meta")"
167
+ [[ -n "$age" && "$age" =~ ^[0-9]+$ && "$age" -le "$ttl" ]]
168
+ }
169
+
170
+ git_cache_write_meta() {
171
+ local meta="$1"
172
+ local key="$2"
173
+ mkdir -p "$(dirname "$meta")" 2>/dev/null || true
174
+ {
175
+ echo "key=$key"
176
+ echo "updated_at=$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || date)"
177
+ } >"$meta" 2>/dev/null || true
178
+ # touch to update mtime (age calculation uses mtime).
179
+ touch "$meta" 2>/dev/null || true
180
+ }
181
+
182
+ git_cache_refresh_one() {
183
+ # Computes and writes cached snapshot for one component context.
184
+ # Usage: git_cache_refresh_one <context> <stack> <component> <active_dir>
185
+ local context="$1"
186
+ local stack="$2"
187
+ local component="$3"
188
+ local active_dir="$4"
189
+
190
+ local key
191
+ key="$(git_cache_key "$context" "$stack" "$component" "$active_dir")"
192
+ local meta info wts
193
+ IFS=$'\t' read -r meta info wts <<<"$(git_cache_paths "$key")"
194
+
195
+ # Missing/non-repo: still write meta so we don't thrash.
196
+ if ! is_git_repo "$active_dir"; then
197
+ echo -e "missing\t${active_dir}\t-\t-\t-\t-\t-\t-\t-\t-\t-\t-\t-\t-\t-\t-\t-\t-" >"$info" 2>/dev/null || true
198
+ : >"$wts" 2>/dev/null || true
199
+ git_cache_write_meta "$meta" "$key"
200
+ return 0
201
+ fi
202
+
203
+ # Collect snapshot.
204
+ local branch head upstream dirty ab ahead behind
205
+ branch="$(git_head_branch "$active_dir")"
206
+ head="$(git_head_short "$active_dir")"
207
+ upstream="$(git_upstream_short "$active_dir")"
208
+ dirty="$(git_dirty_flag "$active_dir")"
209
+ ab="$(git_ahead_behind "$active_dir")"
210
+ ahead=""
211
+ behind=""
212
+ if [[ -n "$ab" ]]; then
213
+ ahead="$(echo "$ab" | cut -d'|' -f1)"
214
+ behind="$(echo "$ab" | cut -d'|' -f2)"
215
+ fi
216
+
217
+ local main_branch main_upstream main_ab main_ahead main_behind
218
+ main_branch="$(git_main_branch_name "$active_dir")"
219
+ main_upstream=""
220
+ main_ahead=""
221
+ main_behind=""
222
+ if [[ -n "$main_branch" ]]; then
223
+ main_upstream="$(git_branch_upstream_short "$active_dir" "$main_branch")"
224
+ main_ab="$(git_branch_ahead_behind "$active_dir" "$main_branch")"
225
+ if [[ -n "$main_ab" ]]; then
226
+ main_ahead="$(echo "$main_ab" | cut -d'|' -f1)"
227
+ main_behind="$(echo "$main_ab" | cut -d'|' -f2)"
228
+ fi
229
+ fi
230
+
231
+ local oref uref oab o_ahead o_behind uab u_ahead u_behind
232
+ oref="$(git_remote_main_ref "$active_dir" "origin")"
233
+ uref="$(git_remote_main_ref "$active_dir" "upstream")"
234
+ o_ahead=""; o_behind=""; u_ahead=""; u_behind=""
235
+ if [[ -n "$main_branch" && -n "$oref" ]]; then
236
+ oab="$(git_ahead_behind_refs "$active_dir" "$oref" "$main_branch")"
237
+ if [[ -n "$oab" ]]; then
238
+ o_ahead="$(echo "$oab" | cut -d'|' -f1)"
239
+ o_behind="$(echo "$oab" | cut -d'|' -f2)"
240
+ fi
241
+ fi
242
+ if [[ -n "$main_branch" && -n "$uref" ]]; then
243
+ uab="$(git_ahead_behind_refs "$active_dir" "$uref" "$main_branch")"
244
+ if [[ -n "$uab" ]]; then
245
+ u_ahead="$(echo "$uab" | cut -d'|' -f1)"
246
+ u_behind="$(echo "$uab" | cut -d'|' -f2)"
247
+ fi
248
+ fi
249
+
250
+ local wt_count
251
+ wt_count="$(git_worktree_count "$active_dir")"
252
+ git_worktrees_tsv "$active_dir" >"$wts" 2>/dev/null || true
253
+
254
+ # status active_dir branch head upstream dirty ahead behind main_branch main_upstream main_ahead main_behind oref o_ahead o_behind uref u_ahead u_behind wt_count
255
+ echo -e "ok\t${active_dir}\t${branch}\t${head}\t${upstream}\t${dirty}\t${ahead}\t${behind}\t${main_branch}\t${main_upstream}\t${main_ahead}\t${main_behind}\t${oref}\t${o_ahead}\t${o_behind}\t${uref}\t${u_ahead}\t${u_behind}\t${wt_count}" >"$info" 2>/dev/null || true
256
+ git_cache_write_meta "$meta" "$key"
257
+ return 0
258
+ }
259
+
260
+ git_cache_load_or_refresh() {
261
+ # Usage: git_cache_load_or_refresh <context> <stack> <component> <active_dir> <allow_refresh_on_miss:0|1>
262
+ # Output: meta<TAB>info<TAB>worktrees<TAB>stale(0|1)
263
+ local context="$1"
264
+ local stack="$2"
265
+ local component="$3"
266
+ local active_dir="$4"
267
+ local allow_refresh_on_miss="${5:-0}"
268
+
269
+ local key
270
+ key="$(git_cache_key "$context" "$stack" "$component" "$active_dir")"
271
+ local meta info wts
272
+ IFS=$'\t' read -r meta info wts <<<"$(git_cache_paths "$key")"
273
+
274
+ # If cache exists and is fresh, use it.
275
+ if [[ -f "$meta" && -f "$info" ]]; then
276
+ if git_cache_is_fresh "$meta"; then
277
+ echo -e "${meta}\t${info}\t${wts}\t0"
278
+ return 0
279
+ fi
280
+ # Stale: do not refresh synchronously during menu render. Background refresh is handled elsewhere.
281
+ echo -e "${meta}\t${info}\t${wts}\t1"
282
+ return 0
283
+ fi
284
+
285
+ # Missing: only refresh synchronously when allowed by caller.
286
+ if [[ "$allow_refresh_on_miss" == "1" ]]; then
287
+ git_cache_refresh_one "$context" "$stack" "$component" "$active_dir" >/dev/null 2>&1 || true
288
+ if [[ -f "$info" ]]; then
289
+ echo -e "${meta}\t${info}\t${wts}\t0"
290
+ return 0
291
+ fi
292
+ fi
293
+
294
+ # Still missing; report missing and stale=1 so callers can show "refresh" action.
295
+ echo -e "${meta}\t${info}\t${wts}\t1"
296
+ return 0
297
+ }
298
+
11
299
  git_try() {
12
300
  local dir="$1"
13
301
  shift
14
302
  if ! command -v git >/dev/null 2>&1; then
15
303
  return 1
16
304
  fi
17
- git -C "$dir" "$@" 2>/dev/null
305
+ local subcmd="${1:-}"
306
+
307
+ # Run-cache: many stacks render the same component git info; cache by repo path + args for this SwiftBar run.
308
+ local cache_key="git|${dir}|$*"
309
+ swiftbar_cache_get "$cache_key"
310
+ local cached_rc=$?
311
+ if [[ $cached_rc -ne 111 ]]; then
312
+ # Cache hit: swiftbar_cache_get already printed stdout. Preserve rc.
313
+ return $cached_rc
314
+ fi
315
+
316
+ local t0 t1 rc out
317
+ t0="$(swiftbar_now_ms 2>/dev/null || echo 0)"
318
+ out="$(git -C "$dir" "$@" 2>/dev/null)"
319
+ rc=$?
320
+ t1="$(swiftbar_now_ms 2>/dev/null || echo 0)"
321
+ swiftbar_cache_set "$cache_key" "$rc" "$out"
322
+ # Keep label short; include subcommand for aggregation.
323
+ swiftbar_profile_log "time" "label=git" "subcmd=$subcmd" "ms=$((t1 - t0))" "rc=${rc}"
324
+ printf '%s\n' "$out"
325
+ return $rc
18
326
  }
19
327
 
20
328
  git_head_branch() {
@@ -10,8 +10,8 @@ get_menu_icon_b64() {
10
10
  fi
11
11
 
12
12
  # Default: prefer a menu-bar friendly PNG icon (repo-local).
13
- if [[ -z "$source" ]] && [[ -f "$HAPPY_LOCAL_DIR/extras/swiftbar/logo-white.png" ]]; then
14
- source="$HAPPY_LOCAL_DIR/extras/swiftbar/logo-white.png"
13
+ if [[ -z "$source" ]] && [[ -f "$HAPPY_LOCAL_DIR/extras/swiftbar/icons/logo-white.png" ]]; then
14
+ source="$HAPPY_LOCAL_DIR/extras/swiftbar/icons/logo-white.png"
15
15
  fi
16
16
 
17
17
  # Fallback: use Happy's favicon if present.