happy-stacks 0.0.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 (67) hide show
  1. package/README.md +314 -0
  2. package/bin/happys.mjs +168 -0
  3. package/docs/menubar.md +186 -0
  4. package/docs/mobile-ios.md +134 -0
  5. package/docs/remote-access.md +43 -0
  6. package/docs/server-flavors.md +79 -0
  7. package/docs/stacks.md +218 -0
  8. package/docs/tauri.md +62 -0
  9. package/docs/worktrees-and-forks.md +395 -0
  10. package/extras/swiftbar/auth-login.sh +31 -0
  11. package/extras/swiftbar/happy-stacks.5s.sh +218 -0
  12. package/extras/swiftbar/icons/happy-green.png +0 -0
  13. package/extras/swiftbar/icons/happy-orange.png +0 -0
  14. package/extras/swiftbar/icons/happy-red.png +0 -0
  15. package/extras/swiftbar/icons/logo-white.png +0 -0
  16. package/extras/swiftbar/install.sh +191 -0
  17. package/extras/swiftbar/lib/git.sh +330 -0
  18. package/extras/swiftbar/lib/icons.sh +105 -0
  19. package/extras/swiftbar/lib/render.sh +774 -0
  20. package/extras/swiftbar/lib/system.sh +190 -0
  21. package/extras/swiftbar/lib/utils.sh +205 -0
  22. package/extras/swiftbar/pnpm-term.sh +125 -0
  23. package/extras/swiftbar/pnpm.sh +21 -0
  24. package/extras/swiftbar/set-interval.sh +62 -0
  25. package/extras/swiftbar/set-server-flavor.sh +57 -0
  26. package/extras/swiftbar/wt-pr.sh +95 -0
  27. package/package.json +58 -0
  28. package/scripts/auth.mjs +272 -0
  29. package/scripts/build.mjs +204 -0
  30. package/scripts/cli-link.mjs +58 -0
  31. package/scripts/completion.mjs +364 -0
  32. package/scripts/daemon.mjs +349 -0
  33. package/scripts/dev.mjs +181 -0
  34. package/scripts/doctor.mjs +342 -0
  35. package/scripts/happy.mjs +79 -0
  36. package/scripts/init.mjs +232 -0
  37. package/scripts/install.mjs +379 -0
  38. package/scripts/menubar.mjs +107 -0
  39. package/scripts/mobile.mjs +305 -0
  40. package/scripts/run.mjs +236 -0
  41. package/scripts/self.mjs +298 -0
  42. package/scripts/server_flavor.mjs +125 -0
  43. package/scripts/service.mjs +526 -0
  44. package/scripts/stack.mjs +815 -0
  45. package/scripts/tailscale.mjs +278 -0
  46. package/scripts/uninstall.mjs +190 -0
  47. package/scripts/utils/args.mjs +17 -0
  48. package/scripts/utils/cli.mjs +24 -0
  49. package/scripts/utils/cli_registry.mjs +262 -0
  50. package/scripts/utils/config.mjs +40 -0
  51. package/scripts/utils/dotenv.mjs +30 -0
  52. package/scripts/utils/env.mjs +138 -0
  53. package/scripts/utils/env_file.mjs +59 -0
  54. package/scripts/utils/env_local.mjs +25 -0
  55. package/scripts/utils/fs.mjs +11 -0
  56. package/scripts/utils/paths.mjs +184 -0
  57. package/scripts/utils/pm.mjs +294 -0
  58. package/scripts/utils/ports.mjs +66 -0
  59. package/scripts/utils/proc.mjs +66 -0
  60. package/scripts/utils/runtime.mjs +30 -0
  61. package/scripts/utils/server.mjs +41 -0
  62. package/scripts/utils/smoke_help.mjs +45 -0
  63. package/scripts/utils/validate.mjs +47 -0
  64. package/scripts/utils/wizard.mjs +69 -0
  65. package/scripts/utils/worktrees.mjs +78 -0
  66. package/scripts/where.mjs +105 -0
  67. package/scripts/worktrees.mjs +1721 -0
@@ -0,0 +1,395 @@
1
+ # Worktrees + forks (happy-stacks)
2
+
3
+ This repo is designed to run the Happy stack locally, while still making it easy to:
4
+
5
+ - keep using **your fork** day-to-day (your fork’s `main` stays your “distribution” branch)
6
+ - create **clean upstream PR branches** quickly (without carrying fork-only patches)
7
+
8
+ ## Key idea: keep “active components” stable, put worktrees elsewhere
9
+
10
+ `happy-stacks` runs components from these default paths (in your workspace):
11
+
12
+ - `components/happy`
13
+ - `components/happy-cli`
14
+ - `components/happy-server-light`
15
+ - (optional) `components/happy-server`
16
+
17
+ All worktrees live under a hidden folder:
18
+
19
+ ```
20
+ components/.worktrees/<component>/<owner>/<branch...>
21
+ ```
22
+
23
+ Examples:
24
+
25
+ - `components/.worktrees/happy/slopus/pr/session-rename-upstream`
26
+ - `components/.worktrees/happy-cli/slopus/ci/typecheck-gha-upstream`
27
+ - `components/.worktrees/happy/leeroybrun/local/my-fork-only-patch`
28
+
29
+ ## Branch naming convention
30
+
31
+ Branches created/managed by `happy-stacks` worktree tooling are named:
32
+
33
+ ```
34
+ <owner>/<branch...>
35
+ ```
36
+
37
+ Where:
38
+
39
+ - `<owner>` is derived from the repo remote you’re basing on
40
+ - **origin** → usually your fork owner (e.g. `leeroybrun`)
41
+ - **upstream** → upstream owner (e.g. `slopus`)
42
+ - `<branch...>` is whatever you choose (`pr/...`, `feat/...`, `local/...`, etc.)
43
+
44
+ ## Choosing which checkout happy-stacks runs
45
+
46
+ `happy-stacks` supports per-component directory overrides via the stack env file (main: `~/.happy/stacks/main/env`, or a specific stack: `~/.happy/stacks/<name>/env`):
47
+
48
+ - `HAPPY_STACKS_COMPONENT_DIR_HAPPY` (legacy: `HAPPY_LOCAL_COMPONENT_DIR_HAPPY`)
49
+ - `HAPPY_STACKS_COMPONENT_DIR_HAPPY_CLI` (legacy: `HAPPY_LOCAL_COMPONENT_DIR_HAPPY_CLI`)
50
+ - `HAPPY_STACKS_COMPONENT_DIR_HAPPY_SERVER_LIGHT` (legacy: `HAPPY_LOCAL_COMPONENT_DIR_HAPPY_SERVER_LIGHT`)
51
+ - `HAPPY_STACKS_COMPONENT_DIR_HAPPY_SERVER` (legacy: `HAPPY_LOCAL_COMPONENT_DIR_HAPPY_SERVER`)
52
+
53
+ The easiest way to set these is with:
54
+
55
+ ```bash
56
+ happys wt use happy slopus/pr/session-rename-upstream
57
+ happys wt use happy-cli slopus/pr/resume-session-from-ui-upstream
58
+ ```
59
+
60
+ Now `happys dev`, `happys start`, and `happys build` will use those active checkouts.
61
+
62
+ ## Switching server flavor (server-light vs full server)
63
+
64
+ You can persistently switch which server implementation is used by setting `HAPPY_STACKS_SERVER_COMPONENT` (legacy: `HAPPY_LOCAL_SERVER_COMPONENT`) in the stack env file (main: `~/.happy/stacks/main/env`).
65
+
66
+ Use the convenience CLI (recommended):
67
+
68
+ ```bash
69
+ happys srv status
70
+ happys srv use happy-server-light
71
+ happys srv use happy-server
72
+ happys srv use --interactive
73
+ ```
74
+
75
+ Note: in a cloned repo, the legacy equivalent is `pnpm srv -- ...`.
76
+
77
+ Reset back to default:
78
+
79
+ ```bash
80
+ happys wt use happy default
81
+ happys wt use happy-cli default
82
+ happys wt use happy-server-light default
83
+ happys wt use happy-server default
84
+ ```
85
+
86
+ Note:
87
+ - `happys srv use ...` picks **which** server component is run.
88
+ - `happys wt use happy-server-light ...` / `happys wt use happy-server ...` pick **which checkout** is used for each server component.
89
+ - `happys start/dev/doctor` will error if these are accidentally mismatched (e.g. server-light selected but its component dir points inside a `happy-server` checkout).
90
+
91
+ ## Creating worktrees
92
+
93
+ Create a new worktree branch based on **upstream** (for upstream PRs):
94
+
95
+ ```bash
96
+ happys wt new happy pr/my-feature --from=upstream --use
97
+ ```
98
+
99
+ ## Testing a GitHub PR locally (`wt pr`)
100
+
101
+ If you have a GitHub PR URL (or just the PR number), you can create a worktree at the PR head ref:
102
+
103
+ ```bash
104
+ happys wt pr happy https://github.com/slopus/happy/pull/123 --use
105
+
106
+ # same, but specify the remote explicitly
107
+ happys wt pr happy 123 --remote=upstream --use
108
+ ```
109
+
110
+ Notes:
111
+ - This uses GitHub’s standard `refs/pull/<N>/head` ref on the chosen remote (default: `upstream`).
112
+ - To refresh the worktree when new commits are pushed to the PR, re-run with `--update`:
113
+
114
+ ```bash
115
+ happys wt pr happy 123 --update
116
+ ```
117
+
118
+ - If you have uncommitted changes in the PR worktree, you can use `--stash` / `--stash-keep`:
119
+
120
+ ```bash
121
+ happys wt pr happy 123 --update --stash
122
+ ```
123
+
124
+ - If the PR was force-pushed and the update is not a fast-forward, `--update` will abort and tell you to re-run with `--update --force`.
125
+ - Use `--slug=<name>` to create a nicer local branch name (example: `slopus/pr/123-fix-thing`).
126
+
127
+ ### Testing a PR inside a stack (recommended)
128
+
129
+ Create a dedicated stack, then apply the PR into that stack env:
130
+
131
+ ```bash
132
+ happys stack new pr123
133
+ happys stack wt pr123 -- pr happy https://github.com/slopus/happy/pull/123 --use
134
+ happys stack dev pr123
135
+ ```
136
+
137
+ Create a new worktree branch based on **your fork** (for fork-only patches):
138
+
139
+ ```bash
140
+ happys wt new happy local/my-patch --from=origin --use
141
+ ```
142
+
143
+ ### Remote + base behavior (automatic)
144
+
145
+ If you do **not** pass `--remote`, `happys wt new` defaults to using the Git remote named `upstream`.
146
+
147
+ It will also keep a local **mirror branch** named after the remote owner **and that remote’s default branch**:
148
+
149
+ - if `upstream` points at `slopus/*` and its default branch is `main`, it will create/update `slopus/main` tracking `upstream/main`
150
+ - if `origin` (or `fork`) points at `leeroybrun/*` and its default branch is `happy-server-light`, it will create/update `leeroybrun/happy-server-light` tracking `fork/happy-server-light`
151
+
152
+ New PR worktrees created without `--base` will default to using that mirror branch (example: `slopus/main`) as the base.
153
+
154
+ ### Syncing a remote to a local mirror branch
155
+
156
+ `happys wt sync <component>` keeps a local mirror branch up to date inside that component repo:
157
+
158
+ - It fetches the remote’s **default branch** (for that remote + component)
159
+ - Then it updates a local branch named `<owner>/<default-branch>` to track it
160
+
161
+ Examples:
162
+
163
+ ```bash
164
+ # Sync upstream (usually slopus/main)
165
+ happys wt sync happy --remote=upstream
166
+
167
+ # Sync your fork remote (origin/fork). For happy-server-light this is typically leeroybrun/happy-server-light.
168
+ happys wt sync happy-server-light --remote=origin
169
+ ```
170
+
171
+ After syncing, you can explicitly base a new worktree on the mirror branch if you want:
172
+
173
+ ```bash
174
+ happys wt new happy pr/my-feature --remote=upstream --base=slopus/main --use
175
+ ```
176
+
177
+ ### Interactive mode
178
+
179
+ If you prefer prompts:
180
+
181
+ ```bash
182
+ happys wt new --interactive
183
+ happys wt use --interactive
184
+ ```
185
+
186
+ ### JSON mode
187
+
188
+ For programmatic usage:
189
+
190
+ ```bash
191
+ happys wt list happy --json
192
+ happys wt sync happy --json
193
+ happys wt new happy pr/my-feature --use --json
194
+ happys wt status happy --json
195
+ happys wt update happy default --dry-run --json
196
+ happys wt push happy default --dry-run --json
197
+ ```
198
+
199
+ ## Migrating old worktree layout (one-time)
200
+
201
+ ## Workflow helpers (sync / update / push)
202
+
203
+ These are convenience commands to keep PR branches updated and to automate the “check conflicts first” loop.
204
+
205
+ ### `wt status`
206
+
207
+ Shows branch / upstream / ahead/behind / clean state:
208
+
209
+ ```bash
210
+ happys wt status happy
211
+ happys wt status happy --json
212
+ ```
213
+
214
+ ## Worktree selector semantics (`default` / `main` / `active`)
215
+
216
+ Many `happys wt` commands accept an optional “selector” argument to choose *which checkout* you mean.
217
+
218
+ - **(omitted)** or **`active`**: the currently active checkout for that component (env override if set; otherwise `components/<component>`)
219
+ - **`default`** or **`main`**: the default embedded checkout at `components/<component>`
220
+ - **`<owner>/<branch...>`**: resolves to `components/.worktrees/<component>/<owner>/<branch...>`
221
+ - **`/absolute/path`**: explicit checkout path
222
+
223
+ Important: `default` / `main` refers to the **checkout location**, not the Git branch name.
224
+
225
+ ### `wt update`
226
+
227
+ Update a worktree branch from its upstream base.
228
+
229
+ - **Default base**: the per-repo mirror branch (example: `slopus/main`, or `leeroybrun/happy-server-light` when syncing from your fork remote)
230
+ - **Default mode**: `rebase` (recommended for clean PR branches)
231
+
232
+ ```bash
233
+ # Check if update would conflict (no changes applied)
234
+ happys wt update happy default --dry-run
235
+
236
+ # Apply rebase if clean; if conflicts, abort and print conflicting files
237
+ happys wt update happy default
238
+
239
+ # If you have uncommitted changes, auto-stash, update, then pop the stash back (only if the update was clean)
240
+ happys wt update happy default --stash
241
+
242
+ # Same, but keep the stash (do not pop) so you can apply it later
243
+ happys wt update happy default --stash-keep
244
+
245
+ # Keep conflict state in place for manual resolution
246
+ happys wt update happy default --force
247
+
248
+ # Use merge instead of rebase (optional)
249
+ happys wt update happy default --merge
250
+ ```
251
+
252
+ ## Open a “real” shell in a worktree (`wt shell`)
253
+
254
+ This starts a new interactive shell **with cwd set to the worktree**, which is the closest thing to a “real cd” a CLI can do:
255
+
256
+ ```bash
257
+ happys wt shell happy slopus/pr/123
258
+
259
+ # choose a shell explicitly
260
+ happys wt shell happy slopus/pr/123 --shell=/bin/zsh
261
+ ```
262
+
263
+ You can also ask it to open a new terminal window/tab (best-effort):
264
+
265
+ ```bash
266
+ happys wt shell happy slopus/pr/123 --new-window
267
+ ```
268
+
269
+ On macOS, auto-detection tries: Ghostty → iTerm → Terminal. You can override via:
270
+
271
+ - `HAPPY_STACKS_WT_TERMINAL=ghostty|iterm|terminal|current` (legacy: `HAPPY_LOCAL_WT_TERMINAL`)
272
+ - `HAPPY_STACKS_WT_SHELL=/path/to/shell` (legacy: `HAPPY_LOCAL_WT_SHELL`)
273
+
274
+ Works with stacks too:
275
+
276
+ ```bash
277
+ happys stack wt pr123 -- shell happy active
278
+ ```
279
+
280
+ ## Open in editors (`wt code` / `wt cursor`)
281
+
282
+ ```bash
283
+ happys wt code happy slopus/pr/123
284
+ happys wt cursor happy slopus/pr/123
285
+ ```
286
+
287
+ Notes:
288
+ - `wt code` requires VS Code’s `code` CLI on PATH.
289
+ - `wt cursor` uses the `cursor` CLI if available; on macOS it falls back to `open -a Cursor`.
290
+
291
+ ### `wt push`
292
+
293
+ Push current HEAD branch to a remote:
294
+
295
+ ```bash
296
+ happys wt push happy default
297
+ happys wt push happy default --remote=origin
298
+ happys wt push happy default --dry-run
299
+ ```
300
+
301
+ ### Create worktrees from an existing worktree/branch
302
+
303
+ If you want to base a new worktree off another worktree’s current branch/HEAD:
304
+
305
+ ```bash
306
+ happys wt new happy pr/next-step --base-worktree=slopus/pr/my-existing-thing
307
+ ```
308
+
309
+ ## Run git inside a worktree (`wt git`)
310
+
311
+ This is a convenience wrapper that runs `git` in a selected checkout:
312
+
313
+ ```bash
314
+ happys wt git happy main -- status
315
+ happys wt git happy active -- log -n 5 --oneline
316
+ happys wt git happy slopus/pr/session-rename-upstream -- diff
317
+ ```
318
+
319
+ For programmatic usage:
320
+
321
+ ```bash
322
+ happys wt git happy main -- status --porcelain -b --json
323
+ ```
324
+
325
+ ## Sync/update everything
326
+
327
+ ```bash
328
+ happys wt sync-all
329
+ happys wt sync-all --json
330
+
331
+ # Dry-run updates across all worktrees for a component (or all components if omitted)
332
+ happys wt update-all happy --dry-run
333
+ happys wt update-all --dry-run --json
334
+ ```
335
+
336
+ If you previously had worktrees under `components/happy-worktrees/*`, run:
337
+
338
+ ```bash
339
+ happys wt migrate
340
+ ```
341
+
342
+ This will:
343
+
344
+ - move worktrees into `components/.worktrees/...`
345
+ - rename checked-out branches to the `<owner>/...` convention
346
+
347
+ ## Server selection: happy-server-light vs happy-server
348
+
349
+ By default, `happy-stacks` uses `happy-server-light`.
350
+
351
+ To run the full upstream server instead:
352
+
353
+ ```bash
354
+ happys bootstrap --server=happy-server
355
+ happys dev --server=happy-server
356
+ ```
357
+
358
+ Notes:
359
+
360
+ - `happys start` (production-like) serves the built UI via `happy-server-light`; UI serving is **disabled** automatically when using `happy-server`.
361
+ - `happys dev` works with either server (it runs the UI separately via Expo).
362
+
363
+ ### Selecting server via env (including LaunchAgent service)
364
+
365
+ You can set a default server implementation via:
366
+
367
+ - `HAPPY_STACKS_SERVER_COMPONENT=happy-server-light` (default) (legacy: `HAPPY_LOCAL_SERVER_COMPONENT`)
368
+ - `HAPPY_STACKS_SERVER_COMPONENT=happy-server` (legacy: `HAPPY_LOCAL_SERVER_COMPONENT`)
369
+
370
+ If you use the macOS LaunchAgent (`happys service install`), the service persists only a pointer to the env file path; the server flavor is read from that env file on every start.
371
+
372
+ ## Env precedence (important)
373
+
374
+ When `happy-stacks` starts, it loads env in this order:
375
+
376
+ 1) `~/.happy-stacks/.env` (defaults)
377
+ 2) `~/.happy-stacks/env.local` (optional global overrides; most config is written to the stack env)
378
+ 3) `HAPPY_STACKS_ENV_FILE` (stack env / explicit overlay; highest precedence for `HAPPY_STACKS_*` / `HAPPY_LOCAL_*`)
379
+
380
+ Notes:
381
+
382
+ - `HAPPY_STACKS_ENV_FILE` (legacy: `HAPPY_LOCAL_ENV_FILE`) is the mechanism used by `happys stack ...` to apply stack-specific settings.
383
+ - For stack runs, the stack wrapper clears any already-exported `HAPPY_STACKS_*` / legacy `HAPPY_LOCAL_*` variables so the stack env file stays authoritative.
384
+ - By default (after `happys init`), commands that “persist config” write to the main stack env file: `~/.happy/stacks/main/env`.
385
+
386
+ ## Repo URLs
387
+
388
+ You can override clone sources in your main stack env file (`~/.happy/stacks/main/env`) or any explicit `HAPPY_STACKS_ENV_FILE`:
389
+
390
+ - `HAPPY_STACKS_REPO_SOURCE=forks|upstream` (legacy: `HAPPY_LOCAL_REPO_SOURCE`)
391
+ - `HAPPY_STACKS_UI_REPO_URL` (legacy: `HAPPY_LOCAL_UI_REPO_URL`)
392
+ - `HAPPY_STACKS_CLI_REPO_URL` (legacy: `HAPPY_LOCAL_CLI_REPO_URL`)
393
+ - `HAPPY_STACKS_SERVER_REPO_URL` (legacy: `HAPPY_LOCAL_SERVER_REPO_URL`) (server-light, backwards compatible)
394
+ - `HAPPY_STACKS_SERVER_LIGHT_REPO_URL` (legacy: `HAPPY_LOCAL_SERVER_LIGHT_REPO_URL`)
395
+ - `HAPPY_STACKS_SERVER_FULL_REPO_URL` (legacy: `HAPPY_LOCAL_SERVER_FULL_REPO_URL`)
@@ -0,0 +1,31 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # Run auth login (interactive) in the user's preferred terminal.
5
+ #
6
+ # Usage (backwards compatible with older callers):
7
+ # ./auth-login.sh main <serverUrl> <webappUrl>
8
+ # ./auth-login.sh <stackName> <serverUrl> <webappUrl> <cliHomeDir>
9
+ #
10
+ # New behavior:
11
+ # - Delegate to `happys auth login` / `happys stack auth <name> login` so URL + cliHome resolution stays centralized.
12
+
13
+ stack="${1:-main}"
14
+ _server_url="${2:-}" # ignored (kept for backwards compatibility)
15
+ _webapp_url="${3:-}" # ignored (kept for backwards compatibility)
16
+ _cli_home_dir="${4:-}" # ignored (kept for backwards compatibility)
17
+
18
+ HAPPY_STACKS_HOME_DIR="${HAPPY_STACKS_HOME_DIR:-$HOME/.happy-stacks}"
19
+ HAPPY_LOCAL_DIR="${HAPPY_LOCAL_DIR:-$HAPPY_STACKS_HOME_DIR}"
20
+
21
+ PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
22
+ if [[ ! -x "$PNPM_TERM" ]]; then
23
+ echo "missing terminal happys wrapper: $PNPM_TERM" >&2
24
+ exit 1
25
+ fi
26
+
27
+ if [[ "$stack" == "main" ]]; then
28
+ exec "$PNPM_TERM" auth login
29
+ fi
30
+
31
+ exec "$PNPM_TERM" stack auth "$stack" login
@@ -0,0 +1,218 @@
1
+ #!/bin/bash
2
+
3
+ # <xbar.title>Happy Stacks</xbar.title>
4
+ # <xbar.version>1.0.0</xbar.version>
5
+ # <xbar.author>Happy Stacks</xbar.author>
6
+ # <xbar.author.github>leeroybrun</xbar.author.github>
7
+ # <xbar.desc>Monitor and control your Happy stacks from the menu bar</xbar.desc>
8
+ # <xbar.dependencies>node</xbar.dependencies>
9
+ # <swiftbar.hideAbout>true</swiftbar.hideAbout>
10
+ # <swiftbar.hideRunInTerminal>true</swiftbar.hideRunInTerminal>
11
+ # <swiftbar.hideLastUpdated>false</swiftbar.hideLastUpdated>
12
+ # <swiftbar.hideDisablePlugin>true</swiftbar.hideDisablePlugin>
13
+ # <swiftbar.hideSwiftBar>true</swiftbar.hideSwiftBar>
14
+ # <swiftbar.refreshOnOpen>false</swiftbar.refreshOnOpen>
15
+
16
+ # ============================================================================
17
+ # Configuration
18
+ # ============================================================================
19
+
20
+ HAPPY_STACKS_HOME_DIR="${HAPPY_STACKS_HOME_DIR:-$HOME/.happy-stacks}"
21
+ HAPPY_LOCAL_DIR="${HAPPY_LOCAL_DIR:-$HAPPY_STACKS_HOME_DIR}"
22
+ HAPPY_LOCAL_PORT="${HAPPY_LOCAL_PORT:-3005}"
23
+
24
+ # Map common preferences from HAPPY_STACKS_* -> HAPPY_LOCAL_* (SwiftBar scripts are shell-based).
25
+ if [[ -n "${HAPPY_STACKS_WT_TERMINAL:-}" && -z "${HAPPY_LOCAL_WT_TERMINAL:-}" ]]; then HAPPY_LOCAL_WT_TERMINAL="$HAPPY_STACKS_WT_TERMINAL"; fi
26
+ if [[ -n "${HAPPY_STACKS_WT_SHELL:-}" && -z "${HAPPY_LOCAL_WT_SHELL:-}" ]]; then HAPPY_LOCAL_WT_SHELL="$HAPPY_STACKS_WT_SHELL"; fi
27
+ if [[ -n "${HAPPY_STACKS_SWIFTBAR_ICON_PATH:-}" && -z "${HAPPY_LOCAL_SWIFTBAR_ICON_PATH:-}" ]]; then HAPPY_LOCAL_SWIFTBAR_ICON_PATH="$HAPPY_STACKS_SWIFTBAR_ICON_PATH"; fi
28
+
29
+ # Storage root migrated from ~/.happy/local -> ~/.happy/stacks/main.
30
+ if [[ -z "${HAPPY_HOME_DIR:-}" ]]; then
31
+ if [[ -d "$HOME/.happy/stacks/main" ]] || [[ ! -d "$HOME/.happy/local" ]]; then
32
+ HAPPY_HOME_DIR="$HOME/.happy/stacks/main"
33
+ else
34
+ HAPPY_HOME_DIR="$HOME/.happy/local"
35
+ fi
36
+ fi
37
+ CLI_HOME_DIR="$HAPPY_HOME_DIR/cli"
38
+ LOGS_DIR="$HAPPY_HOME_DIR/logs"
39
+
40
+ # Colors
41
+ GREEN="#34C759"
42
+ RED="#FF3B30"
43
+ YELLOW="#FFCC00"
44
+ GRAY="#8E8E93"
45
+ BLUE="#007AFF"
46
+
47
+ # ============================================================================
48
+ # Load libs
49
+ # ============================================================================
50
+
51
+ LIB_DIR="$HAPPY_LOCAL_DIR/extras/swiftbar/lib"
52
+ if [[ ! -f "$LIB_DIR/utils.sh" ]]; then
53
+ echo "Happy Stacks"
54
+ echo "---"
55
+ echo "SwiftBar libs missing at: $LIB_DIR"
56
+ echo "↪ run: happys menubar install"
57
+ exit 0
58
+ fi
59
+
60
+ # shellcheck source=/dev/null
61
+ source "$LIB_DIR/utils.sh"
62
+ HAPPY_LOCAL_DIR="$(resolve_happy_local_dir)"
63
+ LIB_DIR="$HAPPY_LOCAL_DIR/extras/swiftbar/lib"
64
+ # shellcheck source=/dev/null
65
+ source "$LIB_DIR/icons.sh"
66
+ # shellcheck source=/dev/null
67
+ source "$LIB_DIR/system.sh"
68
+ # shellcheck source=/dev/null
69
+ source "$LIB_DIR/git.sh"
70
+ # shellcheck source=/dev/null
71
+ source "$LIB_DIR/render.sh"
72
+
73
+ # ============================================================================
74
+ # Menu
75
+ # ============================================================================
76
+
77
+ PNPM_BIN="$(resolve_pnpm_bin)"
78
+ MAIN_PORT="$(resolve_main_port)"
79
+ MAIN_SERVER_COMPONENT="$(resolve_main_server_component)"
80
+ TAILSCALE_URL="$(get_tailscale_url)"
81
+ MAIN_ENV_FILE="$(resolve_main_env_file)"
82
+
83
+ ensure_launchctl_cache
84
+
85
+ MAIN_COLLECT="$(collect_stack_status "$MAIN_PORT" "$CLI_HOME_DIR" "com.happy.stacks" "$HAPPY_HOME_DIR")"
86
+ IFS=$'\t' read -r MAIN_LEVEL MAIN_SERVER_STATUS MAIN_SERVER_PID MAIN_SERVER_METRICS MAIN_DAEMON_STATUS MAIN_DAEMON_PID MAIN_DAEMON_METRICS MAIN_DAEMON_UPTIME MAIN_LAST_HEARTBEAT MAIN_LAUNCHAGENT_STATUS MAIN_AUTOSTART_PID MAIN_AUTOSTART_METRICS <<<"$MAIN_COLLECT"
87
+ for v in MAIN_SERVER_PID MAIN_SERVER_METRICS MAIN_DAEMON_PID MAIN_DAEMON_METRICS MAIN_DAEMON_UPTIME MAIN_LAST_HEARTBEAT MAIN_AUTOSTART_PID MAIN_AUTOSTART_METRICS; do
88
+ if [[ "${!v}" == "-" ]]; then
89
+ printf -v "$v" '%s' ""
90
+ fi
91
+ done
92
+
93
+ # Menu bar icon
94
+ MENU_STATUS_ICON_B64="$(status_icon_b64 "$MAIN_LEVEL" 18)"
95
+ if [[ -n "$MENU_STATUS_ICON_B64" ]]; then
96
+ echo " | image=$MENU_STATUS_ICON_B64"
97
+ else
98
+ STATUS_COLOR="$(color_for_level "$MAIN_LEVEL")"
99
+ ICON_B64="$(get_menu_icon_b64)"
100
+ if [[ -n "$ICON_B64" ]]; then
101
+ echo "● | templateImage=$ICON_B64 color=$STATUS_COLOR"
102
+ else
103
+ echo "Happy"
104
+ fi
105
+ fi
106
+
107
+ echo "---"
108
+ echo "Happy Stacks | size=14 font=SF Pro Display"
109
+ echo "---"
110
+
111
+ # Main stack (inline)
112
+ echo "Main stack"
113
+ echo "---"
114
+ export MAIN_LEVEL="$MAIN_LEVEL"
115
+ render_stack_info "" "main" "$MAIN_PORT" "$MAIN_SERVER_COMPONENT" "$HAPPY_HOME_DIR" "$CLI_HOME_DIR" "com.happy.stacks" "$MAIN_ENV_FILE" "$TAILSCALE_URL"
116
+ render_component_server "" "main" "$MAIN_PORT" "$MAIN_SERVER_COMPONENT" "$MAIN_SERVER_STATUS" "$MAIN_SERVER_PID" "$MAIN_SERVER_METRICS" "$TAILSCALE_URL" "com.happy.stacks"
117
+ render_component_daemon "" "$MAIN_DAEMON_STATUS" "$MAIN_DAEMON_PID" "$MAIN_DAEMON_METRICS" "$MAIN_DAEMON_UPTIME" "$MAIN_LAST_HEARTBEAT" "$CLI_HOME_DIR/daemon.state.json" "main"
118
+ render_component_autostart "" "main" "com.happy.stacks" "$MAIN_LAUNCHAGENT_STATUS" "$MAIN_AUTOSTART_PID" "$MAIN_AUTOSTART_METRICS" "$LOGS_DIR"
119
+ render_component_tailscale "" "main" "$TAILSCALE_URL"
120
+
121
+ echo "---"
122
+ echo "Stacks"
123
+ echo "---"
124
+
125
+ if [[ -n "$PNPM_BIN" ]]; then
126
+ PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
127
+ echo "New stack (interactive) | bash=$PNPM_TERM param1=stack param2=new param3=--interactive dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
128
+ echo "List stacks | bash=$PNPM_TERM param1=stack param2=list dir=$HAPPY_LOCAL_DIR terminal=false"
129
+ echo "---"
130
+ fi
131
+
132
+ STACKS_DIR="$HOME/.happy/stacks"
133
+ if [[ -d "$STACKS_DIR" ]]; then
134
+ STACK_NAMES="$(ls -1 "$STACKS_DIR" 2>/dev/null || true)"
135
+ if [[ -z "$STACK_NAMES" ]]; then
136
+ echo "No stacks found | color=$GRAY"
137
+ fi
138
+ for s in $STACK_NAMES; do
139
+ env_file="$STACKS_DIR/$s/env"
140
+ [[ -f "$env_file" ]] || continue
141
+
142
+ port="$(dotenv_get "$env_file" "HAPPY_LOCAL_SERVER_PORT")"
143
+ [[ -n "$port" ]] || continue
144
+
145
+ server_component="$(dotenv_get "$env_file" "HAPPY_LOCAL_SERVER_COMPONENT")"
146
+ [[ -n "$server_component" ]] || server_component="happy-server-light"
147
+
148
+ cli_home_dir="$(dotenv_get "$env_file" "HAPPY_LOCAL_CLI_HOME_DIR")"
149
+ [[ -n "$cli_home_dir" ]] || cli_home_dir="$STACKS_DIR/$s/cli"
150
+
151
+ base_dir="$STACKS_DIR/$s"
152
+ label="com.happy.stacks.$s"
153
+
154
+ COLLECT="$(collect_stack_status "$port" "$cli_home_dir" "$label" "$base_dir")"
155
+ IFS=$'\t' read -r LEVEL SERVER_STATUS SERVER_PID SERVER_METRICS DAEMON_STATUS DAEMON_PID DAEMON_METRICS DAEMON_UPTIME LAST_HEARTBEAT LAUNCHAGENT_STATUS AUTOSTART_PID AUTOSTART_METRICS <<<"$COLLECT"
156
+ for v in SERVER_PID SERVER_METRICS DAEMON_PID DAEMON_METRICS DAEMON_UPTIME LAST_HEARTBEAT AUTOSTART_PID AUTOSTART_METRICS; do
157
+ if [[ "${!v}" == "-" ]]; then
158
+ printf -v "$v" '%s' ""
159
+ fi
160
+ done
161
+
162
+ render_stack_overview_item "Stack: $s" "$LEVEL" ""
163
+ export STACK_LEVEL="$LEVEL"
164
+ render_stack_info "--" "$s" "$port" "$server_component" "$base_dir" "$cli_home_dir" "$label" "$env_file" ""
165
+ render_component_server "--" "$s" "$port" "$server_component" "$SERVER_STATUS" "$SERVER_PID" "$SERVER_METRICS" "" "$label"
166
+ render_component_daemon "--" "$DAEMON_STATUS" "$DAEMON_PID" "$DAEMON_METRICS" "$DAEMON_UPTIME" "$LAST_HEARTBEAT" "$cli_home_dir/daemon.state.json" "$s"
167
+ render_component_autostart "--" "$s" "$label" "$LAUNCHAGENT_STATUS" "$AUTOSTART_PID" "$AUTOSTART_METRICS" "$base_dir/logs"
168
+ render_component_tailscale "--" "$s" ""
169
+ render_components_menu "--" "stack" "$s" "$env_file"
170
+ done
171
+ else
172
+ echo "No stacks dir found at ~/.happy/stacks | color=$GRAY"
173
+ fi
174
+
175
+ echo "---"
176
+ render_components_menu "" "main" "main" ""
177
+
178
+ echo "Worktrees | sfimage=arrow.triangle.branch"
179
+ if [[ -z "$PNPM_BIN" ]]; then
180
+ echo "--⚠️ happys not found (run: npx happy-stacks init, or install happy-stacks globally)"
181
+ else
182
+ PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
183
+ echo "--Use (interactive) | bash=$PNPM_TERM param1=wt param2=use param3=--interactive dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
184
+ echo "--New (interactive) | bash=$PNPM_TERM param1=wt param2=new param3=--interactive dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
185
+ echo "--PR worktree (prompt) | bash=$HAPPY_LOCAL_DIR/extras/swiftbar/wt-pr.sh dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
186
+ echo "--Sync mirrors (all) | bash=$PNPM_BIN param1=wt param2=sync-all dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
187
+ echo "--Update all (dry-run) | bash=$PNPM_TERM param1=wt param2=update-all param3=--dry-run dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
188
+ echo "--Update all (apply) | bash=$PNPM_BIN param1=wt param2=update-all dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
189
+ fi
190
+
191
+ echo "---"
192
+ echo "Setup / Tools"
193
+ if [[ -z "$PNPM_BIN" ]]; then
194
+ echo "--⚠️ happys not found (run: npx happy-stacks init, or install happy-stacks globally)"
195
+ else
196
+ PNPM_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/pnpm-term.sh"
197
+ echo "--Bootstrap (clone/install) | bash=$PNPM_TERM param1=bootstrap dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
198
+ echo "--CLI link (install happy wrapper) | bash=$PNPM_TERM param1=cli:link dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
199
+ echo "--Mobile dev helper | bash=$PNPM_TERM param1=mobile dir=$HAPPY_LOCAL_DIR terminal=false"
200
+ fi
201
+
202
+ echo "---"
203
+ echo "Refresh | sfimage=arrow.clockwise refresh=true"
204
+ echo "---"
205
+ echo "Refresh interval | sfimage=timer"
206
+ SET_INTERVAL="$HAPPY_LOCAL_DIR/extras/swiftbar/set-interval.sh"
207
+ echo "--10s | bash=$SET_INTERVAL param1=10s dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
208
+ echo "--30s | bash=$SET_INTERVAL param1=30s dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
209
+ echo "--1m | bash=$SET_INTERVAL param1=1m dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
210
+ echo "--5m (recommended) | bash=$SET_INTERVAL param1=5m dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
211
+ echo "--10m | bash=$SET_INTERVAL param1=10m dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
212
+ echo "--15m | bash=$SET_INTERVAL param1=15m dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
213
+ echo "--30m | bash=$SET_INTERVAL param1=30m dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
214
+ echo "--1h | bash=$SET_INTERVAL param1=1h dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
215
+ echo "--2h | bash=$SET_INTERVAL param1=2h dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
216
+ echo "--6h | bash=$SET_INTERVAL param1=6h dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
217
+ echo "--12h | bash=$SET_INTERVAL param1=12h dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
218
+ echo "--1d | bash=$SET_INTERVAL param1=1d dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"exit 0