happy-stacks 0.1.2 → 0.3.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 (116) hide show
  1. package/README.md +164 -89
  2. package/bin/happys.mjs +70 -10
  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/stacks.md +39 -0
  8. package/extras/swiftbar/auth-login.sh +5 -2
  9. package/extras/swiftbar/git-cache-refresh.sh +130 -0
  10. package/extras/swiftbar/happy-stacks.5s.sh +131 -81
  11. package/extras/swiftbar/happys-term.sh +15 -38
  12. package/extras/swiftbar/happys.sh +15 -32
  13. package/extras/swiftbar/install.sh +99 -13
  14. package/extras/swiftbar/lib/git.sh +309 -1
  15. package/extras/swiftbar/lib/icons.sh +2 -2
  16. package/extras/swiftbar/lib/render.sh +209 -80
  17. package/extras/swiftbar/lib/system.sh +27 -4
  18. package/extras/swiftbar/lib/utils.sh +311 -28
  19. package/extras/swiftbar/pnpm.sh +2 -1
  20. package/extras/swiftbar/set-interval.sh +10 -5
  21. package/extras/swiftbar/set-server-flavor.sh +11 -2
  22. package/extras/swiftbar/wt-pr.sh +9 -2
  23. package/package.json +2 -1
  24. package/scripts/auth.mjs +521 -226
  25. package/scripts/build.mjs +29 -10
  26. package/scripts/cli-link.mjs +6 -6
  27. package/scripts/completion.mjs +18 -11
  28. package/scripts/daemon.mjs +133 -31
  29. package/scripts/dev.mjs +196 -137
  30. package/scripts/doctor.mjs +44 -55
  31. package/scripts/edison.mjs +1853 -0
  32. package/scripts/happy.mjs +10 -25
  33. package/scripts/init.mjs +46 -31
  34. package/scripts/install.mjs +21 -15
  35. package/scripts/lint.mjs +124 -0
  36. package/scripts/menubar.mjs +76 -10
  37. package/scripts/migrate.mjs +35 -35
  38. package/scripts/mobile.mjs +24 -17
  39. package/scripts/run.mjs +122 -35
  40. package/scripts/self.mjs +13 -35
  41. package/scripts/server_flavor.mjs +7 -7
  42. package/scripts/service.mjs +31 -28
  43. package/scripts/setup.mjs +694 -0
  44. package/scripts/setup_pr.mjs +165 -0
  45. package/scripts/stack.mjs +1851 -363
  46. package/scripts/stop.mjs +9 -6
  47. package/scripts/tailscale.mjs +23 -11
  48. package/scripts/test.mjs +123 -0
  49. package/scripts/tui.mjs +526 -0
  50. package/scripts/typecheck.mjs +10 -31
  51. package/scripts/ui_gateway.mjs +3 -3
  52. package/scripts/uninstall.mjs +21 -13
  53. package/scripts/utils/auth/dev_key.mjs +163 -0
  54. package/scripts/utils/auth/files.mjs +56 -0
  55. package/scripts/utils/auth/handy_master_secret.mjs +68 -0
  56. package/scripts/utils/auth/login_ux.mjs +76 -0
  57. package/scripts/utils/auth/sources.mjs +12 -0
  58. package/scripts/utils/{cli_registry.mjs → cli/cli_registry.mjs} +48 -0
  59. package/scripts/utils/cli/flags.mjs +17 -0
  60. package/scripts/utils/cli/normalize.mjs +16 -0
  61. package/scripts/utils/{smoke_help.mjs → cli/smoke_help.mjs} +2 -2
  62. package/scripts/utils/{wizard.mjs → cli/wizard.mjs} +1 -1
  63. package/scripts/utils/crypto/tokens.mjs +14 -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/{config.mjs → env/config.mjs} +8 -3
  68. package/scripts/utils/{dotenv.mjs → env/dotenv.mjs} +3 -0
  69. package/scripts/utils/{env.mjs → env/env.mjs} +64 -13
  70. package/scripts/utils/{env_file.mjs → env/env_file.mjs} +38 -1
  71. package/scripts/utils/{env_local.mjs → env/env_local.mjs} +1 -0
  72. package/scripts/utils/env/read.mjs +30 -0
  73. package/scripts/utils/env/sandbox.mjs +14 -0
  74. package/scripts/utils/env/values.mjs +13 -0
  75. package/scripts/utils/{expo.mjs → expo/expo.mjs} +7 -11
  76. package/scripts/utils/fs/json.mjs +25 -0
  77. package/scripts/utils/fs/ops.mjs +29 -0
  78. package/scripts/utils/fs/package_json.mjs +8 -0
  79. package/scripts/utils/fs/tail.mjs +12 -0
  80. package/scripts/utils/git/refs.mjs +26 -0
  81. package/scripts/utils/{worktrees.mjs → git/worktrees.mjs} +60 -4
  82. package/scripts/utils/net/dns.mjs +10 -0
  83. package/scripts/utils/{ports.mjs → net/ports.mjs} +3 -5
  84. package/scripts/utils/paths/canonical_home.mjs +20 -0
  85. package/scripts/utils/paths/localhost_host.mjs +9 -0
  86. package/scripts/utils/{paths.mjs → paths/paths.mjs} +14 -8
  87. package/scripts/utils/{runtime.mjs → paths/runtime.mjs} +4 -4
  88. package/scripts/utils/proc/commands.mjs +34 -0
  89. package/scripts/utils/proc/ownership.mjs +135 -0
  90. package/scripts/utils/proc/package_scripts.mjs +31 -0
  91. package/scripts/utils/proc/pids.mjs +11 -0
  92. package/scripts/utils/proc/pm.mjs +317 -0
  93. package/scripts/utils/{proc.mjs → proc/proc.mjs} +30 -2
  94. package/scripts/utils/proc/watch.mjs +63 -0
  95. package/scripts/utils/{happy_server_infra.mjs → server/infra/happy_server_infra.mjs} +109 -94
  96. package/scripts/utils/server/port.mjs +68 -0
  97. package/scripts/utils/{server.mjs → server/server.mjs} +36 -0
  98. package/scripts/utils/server/urls.mjs +91 -0
  99. package/scripts/utils/{validate.mjs → server/validate.mjs} +1 -1
  100. package/scripts/utils/service/autostart_darwin.mjs +142 -0
  101. package/scripts/utils/stack/context.mjs +23 -0
  102. package/scripts/utils/stack/dirs.mjs +27 -0
  103. package/scripts/utils/stack/editor_workspace.mjs +152 -0
  104. package/scripts/utils/stack/names.mjs +12 -0
  105. package/scripts/utils/stack/runtime_state.mjs +87 -0
  106. package/scripts/utils/stack/stacks.mjs +45 -0
  107. package/scripts/utils/stack/startup.mjs +208 -0
  108. package/scripts/utils/{stack_stop.mjs → stack/stop.mjs} +85 -42
  109. package/scripts/utils/ui/browser.mjs +22 -0
  110. package/scripts/utils/ui/text.mjs +16 -0
  111. package/scripts/where.mjs +17 -10
  112. package/scripts/worktrees.mjs +110 -64
  113. package/scripts/utils/pm.mjs +0 -303
  114. /package/scripts/utils/{args.mjs → cli/args.mjs} +0 -0
  115. /package/scripts/utils/{cli.mjs → cli/cli.mjs} +0 -0
  116. /package/scripts/utils/{fs.mjs → fs/fs.mjs} +0 -0
package/README.md CHANGED
@@ -1,11 +1,6 @@
1
1
  # Happy Stacks
2
2
 
3
-
4
-
5
- Run the **Happy** stack locally (or many stacks in parallel) and access it remotely and securely (using Tailscale).
6
-
7
- `happy-stacks` is a CLI (`happys`) that orchestrate the real upstream repos
8
- cloned under your configured workspace (default: `~/.happy-stacks/workspace/components/*`).
3
+ Run [**Happy**](https://happy.engineering/) locally and access it remotely and securely (using Tailscale).
9
4
 
10
5
  ## What is Happy?
11
6
 
@@ -13,62 +8,24 @@ Happy is an UI/CLI stack (server + web UI + CLI + daemon) who let you monitor an
13
8
 
14
9
  ## What is Happy Stacks?
15
10
 
16
- happy-stacks is a “launcher + workflow toolkit” to:
11
+ happy-stacks is a guided installer + local orchestration CLI for Happy.
17
12
 
18
- - run Happy fully on your own machine (no hosted dependency)
19
- - safely access it remotely (HTTPS secure context) via Tailscale
20
- - manage **worktrees** for clean upstream PRs while keeping a patched fork
21
- - run **multiple isolated stacks** (ports + dirs + component overrides)
22
- - optionally manage autostart (macOS LaunchAgent) and a SwiftBar menu bar control panel
13
+ If you only want to **self-host Happy**, start with the **Self-host** section below.
14
+ If you want to **develop Happy** (worktrees, multiple stacks, upstream PR workflows), see the **Development** section further down.
23
15
 
24
- ## Quickstart
16
+ ## Self-host Happy (install + run)
25
17
 
26
- ### Step 1: Install / bootstrap
18
+ ### Step 1: Setup
27
19
 
28
20
  Recommended:
29
21
 
30
22
  ```bash
31
- npx happy-stacks init
32
- export PATH="$HOME/.happy-stacks/bin:$PATH"
33
- ```
34
-
35
- Alternative (global install):
36
-
37
- ```bash
38
- npm install -g happy-stacks
39
- happys init
40
- export PATH="$HOME/.happy-stacks/bin:$PATH"
23
+ npx happy-stacks setup --profile=selfhost
41
24
  ```
42
25
 
43
- (`init` will run `bootstrap` automatically. Use `--no-bootstrap` if you only want to write config and shims.)
44
-
45
- Developer mode (clone this repo):
26
+ `setup` can optionally start Happy and guide you through authentication.
46
27
 
47
- ```bash
48
- git clone https://github.com/leeroybrun/happy-stacks.git
49
- cd happy-stacks
50
-
51
- node ./bin/happys.mjs bootstrap --interactive
52
- # legacy:
53
- # pnpm bootstrap -- --interactive
54
- ```
55
-
56
- Notes:
57
-
58
- - In a cloned repo, `pnpm <script>` still works, but `happys <command>` is now the recommended UX (same underlying scripts).
59
- - To make the installed `~/.happy-stacks/bin/happys` shim (LaunchAgents / SwiftBar) run your local checkout without publishing to npm, set:
60
-
61
- ```bash
62
- echo 'HAPPY_STACKS_CLI_ROOT_DIR=/path/to/your/happy-stacks-checkout' >> ~/.happy-stacks/.env
63
- ```
64
-
65
- Or (recommended) persist it via init:
66
-
67
- ```bash
68
- happys init --cli-root-dir=/path/to/your/happy-stacks-checkout
69
- ```
70
-
71
- ### Step 2: Run the main stack
28
+ ### Step 2: Start Happy
72
29
 
73
30
  Starts the local server, CLI daemon, and serves the pre-built UI.
74
31
 
@@ -76,7 +33,7 @@ Starts the local server, CLI daemon, and serves the pre-built UI.
76
33
  happys start
77
34
  ```
78
35
 
79
- ### Step 2b (first run only): authenticate the daemon
36
+ ### Step 3 (first run only): authenticate
80
37
 
81
38
  On a **fresh machine** (or any new stack), the daemon needs to authenticate once before it can register a “machine”.
82
39
 
@@ -84,34 +41,20 @@ On a **fresh machine** (or any new stack), the daemon needs to authenticate once
84
41
  happys auth login
85
42
  ```
86
43
 
87
- #### Troubleshooting: “no machine” on first run (daemon auth)
88
-
89
- If `.../new` shows “no machine” check whether this is **auth** vs a **daemon/runtime** issue:
44
+ If you want a quick diagnosis:
90
45
 
91
46
  ```bash
92
47
  happys auth status
93
48
  ```
94
49
 
95
- If it says **auth is required**, run:
96
-
97
- ```bash
98
- happys auth login
99
- ```
100
-
101
- If auth is OK but the daemon isn’t running, run:
102
-
103
- ```bash
104
- happys doctor
105
- ```
106
-
107
- ### Step 3: Enable Tailscale Serve (recommended for remote devices)
50
+ ### Step 4: Enable Tailscale Serve (recommended for mobile/remote)
108
51
 
109
52
  ```bash
110
53
  happys tailscale enable
111
54
  happys tailscale url
112
55
  ```
113
56
 
114
- ### Step 4: Mobile access
57
+ ### Step 5: Mobile access
115
58
 
116
59
  Make sure Tailscale is [installed and running]
117
60
  ([https://tailscale.com/kb/1347/installation](https://tailscale.com/kb/1347/installation)) on your
@@ -124,9 +67,41 @@ your local server](docs/remote-access.md).
124
67
 
125
68
  Details (secure context, phone instructions, automation knobs): `[docs/remote-access.md](docs/remote-access.md)`.
126
69
 
127
- ## Why this exists
70
+ ## Development (worktrees, stacks, contributor workflows)
71
+
72
+ ### Setup (guided)
73
+
74
+ ```bash
75
+ npx happy-stacks setup --profile=dev
76
+ ```
77
+
78
+ ### Developing from a cloned repo
79
+
80
+ ```bash
81
+ git clone https://github.com/leeroybrun/happy-stacks.git
82
+ cd happy-stacks
83
+
84
+ node ./bin/happys.mjs setup --profile=dev
85
+ ```
86
+
87
+ Notes:
88
+
89
+ - In a cloned repo, `pnpm <script>` still works, but `happys <command>` is the recommended UX (same underlying scripts).
90
+ - To make the installed `~/.happy-stacks/bin/happys` shim (LaunchAgents / SwiftBar) run your local checkout without publishing to npm, set:
91
+
92
+ ```bash
93
+ echo 'HAPPY_STACKS_CLI_ROOT_DIR=/path/to/your/happy-stacks-checkout' >> ~/.happy-stacks/.env
94
+ ```
95
+
96
+ Or (recommended) persist it via init:
97
+
98
+ ```bash
99
+ happys init --cli-root-dir=/path/to/your/happy-stacks-checkout
100
+ ```
101
+
102
+ ### Why this exists
128
103
 
129
- - **Automated setup**: `happys init` + `happys start` gets the whole stack up and running.
104
+ - **Automated setup**: `happys setup` + `happys start` gets the whole stack up and running.
130
105
  - **No hosted dependency**: run the full stack on your own computer.
131
106
  - **Lower latency**: localhost/LAN is typically much faster than remote hosted servers.
132
107
  - **Custom forks**: easily use forks of the Happy UI + CLI (e.g. `leeroybrun/*`) while still contributing upstream to `slopus/*`.
@@ -134,7 +109,7 @@ Details (secure context, phone instructions, automation knobs): `[docs/remote-ac
134
109
  - **Stacks**: run multiple isolated instances in parallel (ports + dirs + component overrides).
135
110
  - **Remote access**: `happys tailscale ...` helps you get an HTTPS URL for mobile/remote devices.
136
111
 
137
- ## How Happy Stacks wires “local” URLs
112
+ ### How Happy Stacks wires “local” URLs
138
113
 
139
114
  There are two “URLs” to understand:
140
115
 
@@ -170,7 +145,7 @@ Diagram:
170
145
 
171
146
  More details + automation: `[docs/remote-access.md](docs/remote-access.md)`.
172
147
 
173
- ## How it’s organized
148
+ ### How it’s organized
174
149
 
175
150
  - **Scripts**: `scripts/*.mjs` (bootstrap/dev/start/build/stacks/worktrees/service/tailscale/mobile)
176
151
  - **Components**: `components/*` (each is its own Git repo)
@@ -183,9 +158,9 @@ Components:
183
158
  - `happy-server-light` (light server, can serve built UI)
184
159
  - `happy-server` (full server)
185
160
 
186
- ## Quickstarts (feature-focused)
161
+ ### Quickstarts (feature-focused)
187
162
 
188
- ### Remote access (Tailscale Serve)
163
+ #### Remote access (Tailscale Serve)
189
164
 
190
165
  ```bash
191
166
  happys tailscale enable
@@ -194,7 +169,7 @@ happys tailscale url
194
169
 
195
170
  Details: `[docs/remote-access.md](docs/remote-access.md)`.
196
171
 
197
- ### Worktrees + forks (clean upstream PRs)
172
+ #### Worktrees + forks (clean upstream PRs)
198
173
 
199
174
  Create a clean upstream PR worktree:
200
175
 
@@ -210,9 +185,80 @@ happys wt pr happy https://github.com/slopus/happy/pull/123 --use
210
185
  happys wt pr happy 123 --update --stash
211
186
  ```
212
187
 
188
+ ##### Developer quickstart: create a PR stack (isolated ports/dirs; idempotent updates)
189
+
190
+ This creates (or reuses) a named stack, checks out PR worktrees for the selected components, optionally seeds auth, and starts the stack.
191
+ Re-run with `--reuse` to update the existing worktrees when the PR changes.
192
+
193
+ ```bash
194
+ happys stack pr pr123 \
195
+ --happy=https://github.com/slopus/happy/pull/123 \
196
+ --happy-cli=https://github.com/slopus/happy-cli/pull/456 \
197
+ --seed-auth --copy-auth-from=dev-auth --link-auth \
198
+ --dev
199
+ ```
200
+
201
+ Optional: run it in a self-contained sandbox folder (delete it to uninstall completely):
202
+
203
+ ```bash
204
+ SANDBOX="$(mktemp -d /tmp/happy-stacks-sandbox.XXXXXX)"
205
+ happys --sandbox-dir "$SANDBOX" stack pr pr123 --happy=123 --happy-cli=456 --dev
206
+ rm -rf "$SANDBOX"
207
+ ```
208
+
209
+ Update when the PR changes:
210
+
211
+ - Re-run with `--reuse` to fast-forward worktrees when possible.
212
+ - If the PR was force-pushed, add `--force`.
213
+
214
+ ```bash
215
+ happys stack pr pr123 --happy=123 --happy-cli=456 --reuse
216
+ happys stack pr pr123 --happy=123 --happy-cli=456 --reuse --force
217
+ ```
218
+
219
+ ##### Maintainer quickstart: one-shot “install + run PR stack” (idempotent)
220
+
221
+ This is the maintainer-friendly entrypoint. It is safe to re-run and will keep the PR stack wiring intact.
222
+
223
+ ```bash
224
+ npx happy-stacks setup-pr \
225
+ --happy=https://github.com/slopus/happy/pull/123 \
226
+ --happy-cli=https://github.com/slopus/happy-cli/pull/456
227
+ ```
228
+
229
+ Optional: run it in a self-contained sandbox folder (delete it to uninstall completely):
230
+
231
+ ```bash
232
+ SANDBOX="$(mktemp -d /tmp/happy-stacks-sandbox.XXXXXX)"
233
+ npx happy-stacks --sandbox-dir "$SANDBOX" setup-pr --happy=123 --happy-cli=456
234
+ rm -rf "$SANDBOX"
235
+ ```
236
+
237
+ Short form (PR numbers):
238
+
239
+ ```bash
240
+ npx happy-stacks setup-pr --happy=123 --happy-cli=456
241
+ ```
242
+
243
+ Override stack name (optional):
244
+
245
+ ```bash
246
+ npx happy-stacks setup-pr --name=pr123 --happy=123 --happy-cli=456
247
+ ```
248
+
249
+ Update when the PR changes:
250
+
251
+ - Re-run the same command to fast-forward the PR worktrees.
252
+ - If the PR was force-pushed, add `--force`.
253
+
254
+ ```bash
255
+ npx happy-stacks setup-pr --happy=123 --happy-cli=456
256
+ npx happy-stacks setup-pr --happy=123 --happy-cli=456 --force
257
+ ```
258
+
213
259
  Details: `[docs/worktrees-and-forks.md](docs/worktrees-and-forks.md)`.
214
260
 
215
- ### Server flavor (server-light vs full server)
261
+ #### Server flavor (server-light vs full server)
216
262
 
217
263
  - Use `happy-server-light` for a light local stack (no Redis, no Postgres, no Docker), and UI serving via server-light.
218
264
  - Use `happy-server` when you need a more production-like server (Postgres + Redis + S3-compatible storage) or want to develop server changes for upstream.
@@ -233,7 +279,7 @@ happys stack srv exp1 -- use --interactive
233
279
 
234
280
  Details: `[docs/server-flavors.md](docs/server-flavors.md)`.
235
281
 
236
- ### Stacks (multiple isolated instances)
282
+ #### Stacks (multiple isolated instances)
237
283
 
238
284
  ```bash
239
285
  happys stack new exp1 --interactive
@@ -250,7 +296,15 @@ happys stack dev exp1
250
296
 
251
297
  Details: `[docs/stacks.md](docs/stacks.md)`.
252
298
 
253
- ### Menu bar (SwiftBar)
299
+ #### Dev stacks: browser origin isolation (IMPORTANT)
300
+
301
+ Non-main stacks use a stack-specific localhost hostname (no `/etc/hosts` changes required):
302
+
303
+ - `http://happy-<stack>.localhost:<uiPort>`
304
+
305
+ This avoids browser auth/session collisions between stacks (separate origin per stack).
306
+
307
+ #### Menu bar (SwiftBar)
254
308
 
255
309
  ```bash
256
310
  happys menubar install
@@ -259,7 +313,7 @@ happys menubar open
259
313
 
260
314
  Details: `[docs/menubar.md](docs/menubar.md)`.
261
315
 
262
- ### Mobile iOS dev (optional)
316
+ #### Mobile iOS dev (optional)
263
317
 
264
318
  ```bash
265
319
  happys mobile --help
@@ -268,7 +322,7 @@ happys mobile --json
268
322
 
269
323
  Details: `[docs/mobile-ios.md](docs/mobile-ios.md)`.
270
324
 
271
- ### Tauri desktop app (optional)
325
+ #### Tauri desktop app (optional)
272
326
 
273
327
  ```bash
274
328
  happys build --tauri
@@ -276,13 +330,12 @@ happys build --tauri
276
330
 
277
331
  Details: `[docs/tauri.md](docs/tauri.md)`.
278
332
 
279
- ## Commands (high-signal)
333
+ ### Commands (high-signal)
280
334
 
281
335
  - **Setup**:
282
- - `happys init`
283
- - `happys bootstrap --interactive` (wizard)
284
- - `happys bootstrap --forks|--upstream`
285
- - `happys bootstrap --server=happy-server|happy-server-light|both`
336
+ - `happys setup` (guided; selfhost or dev)
337
+ - (advanced) `happys init` (plumbing: shims/runtime/pointer env)
338
+ - (advanced) `happys bootstrap --interactive` (component installer wizard)
286
339
  - **Run**:
287
340
  - `happys start` (production-like; serves built UI via server-light)
288
341
  - `happys dev` (dev; Expo web dev server for UI)
@@ -303,17 +356,18 @@ Details: `[docs/tauri.md](docs/tauri.md)`.
303
356
  - **Menu bar (SwiftBar)**:
304
357
  - `happys menubar install`
305
358
 
306
- ## Docs (deep dives)
359
+ ### Docs (deep dives)
307
360
 
308
361
  - **Remote access (Tailscale + phone)**: `[docs/remote-access.md](docs/remote-access.md)`
309
362
  - **Server flavors (server-light vs server)**: `[docs/server-flavors.md](docs/server-flavors.md)`
310
363
  - **Worktrees + forks workflow**: `[docs/worktrees-and-forks.md](docs/worktrees-and-forks.md)`
311
364
  - **Stacks (multiple instances)**: `[docs/stacks.md](docs/stacks.md)`
365
+ - **Paths + env precedence (home/workspace/runtime/stacks)**: `[docs/paths-and-env.md](docs/paths-and-env.md)`
312
366
  - **Menu bar (SwiftBar)**: `[docs/menubar.md](docs/menubar.md)`
313
367
  - **Mobile iOS dev**: `[docs/mobile-ios.md](docs/mobile-ios.md)`
314
368
  - **Tauri desktop app**: `[docs/tauri.md](docs/tauri.md)`
315
369
 
316
- ## Configuration
370
+ ### Configuration
317
371
 
318
372
  Where config lives by default:
319
373
 
@@ -329,4 +383,25 @@ Notes:
329
383
  - **Use `.env.example` as the canonical template** (copy it to `.env` if you’re running this repo directly).
330
384
  - If an LLM tool refuses to read/edit `.env.example` due to safety restrictions, **do not create an `env.example` workaround**—instead, ask the user to apply the change manually.
331
385
 
386
+ ### Sandbox / test installs (fully isolated)
387
+
388
+ If you want to test the full setup flow (including PR stacks) without impacting your “real” install, run everything with `--sandbox-dir`.
389
+ To fully uninstall the test run, stop the sandbox stacks and delete the sandbox folder.
390
+
391
+ ```bash
392
+ SANDBOX="$(mktemp -d /tmp/happy-stacks-sandbox.XXXXXX)"
393
+
394
+ # Run a PR stack (fully isolated install)
395
+ npx happy-stacks --sandbox-dir "$SANDBOX" setup-pr --happy=123 --happy-cli=456
396
+
397
+ # Tear down + uninstall
398
+ npx happy-stacks --sandbox-dir "$SANDBOX" stop --yes --no-service
399
+ rm -rf "$SANDBOX"
400
+ ```
401
+
402
+ Notes:
403
+
404
+ - Sandbox mode disables global OS side effects (**PATH edits**, **SwiftBar plugin install**, **LaunchAgents/systemd services**, **Tailscale Serve enable/disable**) by default.
405
+ - To explicitly allow those for testing, set `HAPPY_STACKS_SANDBOX_ALLOW_GLOBAL=1` (still recommended to clean up after).
406
+
332
407
  For contributor/LLM workflow expectations: `[AGENTS.md](AGENTS.md)`.
package/bin/happys.mjs CHANGED
@@ -7,15 +7,14 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
7
7
  import { homedir } from 'node:os';
8
8
  import { dirname, join } from 'node:path';
9
9
  import { fileURLToPath } from 'node:url';
10
- import { commandHelpArgs, renderHappysRootHelp, resolveHappysCommand } from '../scripts/utils/cli_registry.mjs';
10
+ import { commandHelpArgs, renderHappysRootHelp, resolveHappysCommand } from '../scripts/utils/cli/cli_registry.mjs';
11
+ import { expandHome, getCanonicalHomeEnvPathFromEnv } from '../scripts/utils/paths/canonical_home.mjs';
11
12
 
12
13
  function getCliRootDir() {
13
14
  return dirname(dirname(fileURLToPath(import.meta.url)));
14
15
  }
15
16
 
16
- function expandHome(p) {
17
- return String(p ?? '').replace(/^~(?=\/)/, homedir());
18
- }
17
+ // expandHome is imported from scripts/utils/paths/canonical_home.mjs
19
18
 
20
19
  function dotenvGetQuick(envPath, key) {
21
20
  try {
@@ -47,7 +46,7 @@ function resolveCliRootDir() {
47
46
  if (fromEnv) return expandHome(fromEnv);
48
47
 
49
48
  // Stable pointer file: even if the real home dir is elsewhere, `happys init` writes the pointer here.
50
- const canonicalEnv = join(homedir(), '.happy-stacks', '.env');
49
+ const canonicalEnv = getCanonicalHomeEnvPathFromEnv(process.env);
51
50
  const v =
52
51
  dotenvGetQuick(canonicalEnv, 'HAPPY_STACKS_CLI_ROOT_DIR') ||
53
52
  dotenvGetQuick(canonicalEnv, 'HAPPY_LOCAL_CLI_ROOT_DIR') ||
@@ -86,11 +85,66 @@ function resolveHomeDir() {
86
85
  if (fromEnv) return expandHome(fromEnv);
87
86
 
88
87
  // Stable pointer file: even if the real home dir is elsewhere, `happys init` writes the pointer here.
89
- const canonicalEnv = join(homedir(), '.happy-stacks', '.env');
88
+ const canonicalEnv = getCanonicalHomeEnvPathFromEnv(process.env);
90
89
  const v = dotenvGetQuick(canonicalEnv, 'HAPPY_STACKS_HOME_DIR') || dotenvGetQuick(canonicalEnv, 'HAPPY_LOCAL_HOME_DIR') || '';
91
90
  return v ? expandHome(v) : join(homedir(), '.happy-stacks');
92
91
  }
93
92
 
93
+ function stripGlobalOpt(argv, { name, aliases = [] }) {
94
+ const names = [name, ...aliases];
95
+ for (const n of names) {
96
+ const eq = `${n}=`;
97
+ const iEq = argv.findIndex((a) => a.startsWith(eq));
98
+ if (iEq >= 0) {
99
+ const value = argv[iEq].slice(eq.length);
100
+ const next = [...argv.slice(0, iEq), ...argv.slice(iEq + 1)];
101
+ return { value, argv: next };
102
+ }
103
+ const i = argv.indexOf(n);
104
+ if (i >= 0 && argv[i + 1] && !argv[i + 1].startsWith('-')) {
105
+ const value = argv[i + 1];
106
+ const next = [...argv.slice(0, i), ...argv.slice(i + 2)];
107
+ return { value, argv: next };
108
+ }
109
+ }
110
+ return { value: '', argv };
111
+ }
112
+
113
+ function applySandboxDirIfRequested(argv) {
114
+ const explicit = (process.env.HAPPY_STACKS_SANDBOX_DIR ?? '').trim();
115
+ const { value, argv: nextArgv } = stripGlobalOpt(argv, { name: '--sandbox-dir', aliases: ['--sandbox'] });
116
+ const raw = value || explicit;
117
+ if (!raw) return { argv: nextArgv, enabled: false };
118
+
119
+ const sandboxDir = expandHome(raw);
120
+ // Keep all state under one folder that can be deleted to reset completely.
121
+ const canonicalHomeDir = join(sandboxDir, 'canonical');
122
+ const homeDir = join(sandboxDir, 'home');
123
+ const workspaceDir = join(sandboxDir, 'workspace');
124
+ const runtimeDir = join(sandboxDir, 'runtime');
125
+ const storageDir = join(sandboxDir, 'storage');
126
+
127
+ process.env.HAPPY_STACKS_SANDBOX_DIR = sandboxDir;
128
+ process.env.HAPPY_STACKS_CLI_ROOT_DISABLE = '1'; // never re-exec into a user's "real" install when sandboxing
129
+
130
+ process.env.HAPPY_STACKS_CANONICAL_HOME_DIR = process.env.HAPPY_STACKS_CANONICAL_HOME_DIR ?? canonicalHomeDir;
131
+ process.env.HAPPY_LOCAL_CANONICAL_HOME_DIR = process.env.HAPPY_LOCAL_CANONICAL_HOME_DIR ?? process.env.HAPPY_STACKS_CANONICAL_HOME_DIR;
132
+
133
+ process.env.HAPPY_STACKS_HOME_DIR = process.env.HAPPY_STACKS_HOME_DIR ?? homeDir;
134
+ process.env.HAPPY_LOCAL_HOME_DIR = process.env.HAPPY_LOCAL_HOME_DIR ?? process.env.HAPPY_STACKS_HOME_DIR;
135
+
136
+ process.env.HAPPY_STACKS_WORKSPACE_DIR = process.env.HAPPY_STACKS_WORKSPACE_DIR ?? workspaceDir;
137
+ process.env.HAPPY_LOCAL_WORKSPACE_DIR = process.env.HAPPY_LOCAL_WORKSPACE_DIR ?? process.env.HAPPY_STACKS_WORKSPACE_DIR;
138
+
139
+ process.env.HAPPY_STACKS_RUNTIME_DIR = process.env.HAPPY_STACKS_RUNTIME_DIR ?? runtimeDir;
140
+ process.env.HAPPY_LOCAL_RUNTIME_DIR = process.env.HAPPY_LOCAL_RUNTIME_DIR ?? process.env.HAPPY_STACKS_RUNTIME_DIR;
141
+
142
+ process.env.HAPPY_STACKS_STORAGE_DIR = process.env.HAPPY_STACKS_STORAGE_DIR ?? storageDir;
143
+ process.env.HAPPY_LOCAL_STORAGE_DIR = process.env.HAPPY_LOCAL_STORAGE_DIR ?? process.env.HAPPY_STACKS_STORAGE_DIR;
144
+
145
+ return { argv: nextArgv, enabled: true };
146
+ }
147
+
94
148
  function maybeAutoUpdateNotice(cliRootDir, cmd) {
95
149
  // Non-blocking, cached update checks:
96
150
  // - never run network calls in-process
@@ -193,17 +247,23 @@ function runNodeScript(cliRootDir, scriptRelPath, args) {
193
247
 
194
248
  function main() {
195
249
  const cliRootDir = getCliRootDir();
250
+ const initialArgv = process.argv.slice(2);
251
+ const { argv, enabled: sandboxed } = applySandboxDirIfRequested(initialArgv);
252
+ void sandboxed;
196
253
  maybeReexecToCliRoot(cliRootDir);
197
- const argv = process.argv.slice(2);
198
254
 
255
+ // If the user passed only flags (common via `npx happy-stacks --help`),
256
+ // treat it as root help rather than `help --help` (which would look like
257
+ // "unknown command: --help").
199
258
  const cmd = argv.find((a) => !a.startsWith('--')) ?? 'help';
200
- const rest = cmd === 'help' ? [] : argv.slice(argv.indexOf(cmd) + 1);
259
+ const cmdIndex = argv.indexOf(cmd);
260
+ const rest = cmdIndex >= 0 ? argv.slice(cmdIndex + 1) : [];
201
261
 
202
262
  maybeAutoUpdateNotice(cliRootDir, cmd);
203
263
 
204
264
  if (cmd === 'help' || cmd === '--help' || cmd === '-h') {
205
- const target = argv[argv.indexOf(cmd) + 1];
206
- if (!target) {
265
+ const target = rest[0];
266
+ if (!target || target.startsWith('-')) {
207
267
  console.log(usage());
208
268
  return;
209
269
  }