happy-stacks 0.1.2 → 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 (91) hide show
  1. package/README.md +121 -83
  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 +560 -112
  25. package/scripts/build.mjs +24 -4
  26. package/scripts/cli-link.mjs +3 -3
  27. package/scripts/completion.mjs +15 -8
  28. package/scripts/daemon.mjs +130 -20
  29. package/scripts/dev.mjs +201 -133
  30. package/scripts/doctor.mjs +26 -21
  31. package/scripts/edison.mjs +1828 -0
  32. package/scripts/happy.mjs +3 -7
  33. package/scripts/init.mjs +43 -20
  34. package/scripts/install.mjs +14 -8
  35. package/scripts/lint.mjs +145 -0
  36. package/scripts/menubar.mjs +81 -8
  37. package/scripts/migrate.mjs +25 -15
  38. package/scripts/mobile.mjs +13 -7
  39. package/scripts/run.mjs +114 -27
  40. package/scripts/self.mjs +3 -7
  41. package/scripts/server_flavor.mjs +3 -3
  42. package/scripts/service.mjs +15 -2
  43. package/scripts/setup.mjs +790 -0
  44. package/scripts/setup_pr.mjs +182 -0
  45. package/scripts/stack.mjs +1792 -254
  46. package/scripts/stop.mjs +6 -3
  47. package/scripts/tailscale.mjs +17 -2
  48. package/scripts/test.mjs +144 -0
  49. package/scripts/tui.mjs +556 -0
  50. package/scripts/typecheck.mjs +2 -2
  51. package/scripts/ui_gateway.mjs +2 -2
  52. package/scripts/uninstall.mjs +18 -10
  53. package/scripts/utils/auth_files.mjs +58 -0
  54. package/scripts/utils/auth_login_ux.mjs +76 -0
  55. package/scripts/utils/auth_sources.mjs +12 -0
  56. package/scripts/utils/browser.mjs +22 -0
  57. package/scripts/utils/canonical_home.mjs +20 -0
  58. package/scripts/utils/{cli_registry.mjs → cli/cli_registry.mjs} +48 -0
  59. package/scripts/utils/{wizard.mjs → cli/wizard.mjs} +1 -1
  60. package/scripts/utils/config.mjs +6 -2
  61. package/scripts/utils/dev_auth_key.mjs +169 -0
  62. package/scripts/utils/dev_daemon.mjs +104 -0
  63. package/scripts/utils/dev_expo_web.mjs +112 -0
  64. package/scripts/utils/dev_server.mjs +183 -0
  65. package/scripts/utils/env.mjs +60 -11
  66. package/scripts/utils/env_file.mjs +36 -0
  67. package/scripts/utils/expo.mjs +4 -2
  68. package/scripts/utils/handy_master_secret.mjs +94 -0
  69. package/scripts/utils/happy_server_infra.mjs +100 -46
  70. package/scripts/utils/localhost_host.mjs +17 -0
  71. package/scripts/utils/ownership.mjs +135 -0
  72. package/scripts/utils/paths.mjs +5 -2
  73. package/scripts/utils/pm.mjs +121 -20
  74. package/scripts/utils/proc.mjs +29 -2
  75. package/scripts/utils/runtime.mjs +1 -3
  76. package/scripts/utils/sandbox.mjs +14 -0
  77. package/scripts/utils/server.mjs +24 -0
  78. package/scripts/utils/server_port.mjs +9 -0
  79. package/scripts/utils/server_urls.mjs +54 -0
  80. package/scripts/utils/stack_context.mjs +23 -0
  81. package/scripts/utils/stack_runtime_state.mjs +104 -0
  82. package/scripts/utils/stack_startup.mjs +208 -0
  83. package/scripts/utils/stack_stop.mjs +79 -30
  84. package/scripts/utils/stacks.mjs +38 -0
  85. package/scripts/utils/watch.mjs +63 -0
  86. package/scripts/utils/worktrees.mjs +57 -1
  87. package/scripts/where.mjs +14 -7
  88. package/scripts/worktrees.mjs +82 -8
  89. /package/scripts/utils/{args.mjs → cli/args.mjs} +0 -0
  90. /package/scripts/utils/{cli.mjs → cli/cli.mjs} +0 -0
  91. /package/scripts/utils/{smoke_help.mjs → cli/smoke_help.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,31 @@ 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"
23
+ npx happy-stacks setup --profile=selfhost
33
24
  ```
34
25
 
35
26
  Alternative (global install):
36
27
 
37
28
  ```bash
38
29
  npm install -g happy-stacks
39
- happys init
40
- export PATH="$HOME/.happy-stacks/bin:$PATH"
41
- ```
42
-
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):
46
-
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
30
+ happys setup --profile=selfhost
54
31
  ```
55
32
 
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
- ```
33
+ `setup` can optionally start Happy and guide you through authentication.
70
34
 
71
- ### Step 2: Run the main stack
35
+ ### Step 2: Start Happy
72
36
 
73
37
  Starts the local server, CLI daemon, and serves the pre-built UI.
74
38
 
@@ -76,7 +40,7 @@ Starts the local server, CLI daemon, and serves the pre-built UI.
76
40
  happys start
77
41
  ```
78
42
 
79
- ### Step 2b (first run only): authenticate the daemon
43
+ ### Step 3 (first run only): authenticate
80
44
 
81
45
  On a **fresh machine** (or any new stack), the daemon needs to authenticate once before it can register a “machine”.
82
46
 
@@ -84,34 +48,20 @@ On a **fresh machine** (or any new stack), the daemon needs to authenticate once
84
48
  happys auth login
85
49
  ```
86
50
 
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:
51
+ If you want a quick diagnosis:
90
52
 
91
53
  ```bash
92
54
  happys auth status
93
55
  ```
94
56
 
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)
57
+ ### Step 4: Enable Tailscale Serve (recommended for mobile/remote)
108
58
 
109
59
  ```bash
110
60
  happys tailscale enable
111
61
  happys tailscale url
112
62
  ```
113
63
 
114
- ### Step 4: Mobile access
64
+ ### Step 5: Mobile access
115
65
 
116
66
  Make sure Tailscale is [installed and running]
117
67
  ([https://tailscale.com/kb/1347/installation](https://tailscale.com/kb/1347/installation)) on your
@@ -124,9 +74,41 @@ your local server](docs/remote-access.md).
124
74
 
125
75
  Details (secure context, phone instructions, automation knobs): `[docs/remote-access.md](docs/remote-access.md)`.
126
76
 
127
- ## Why this exists
77
+ ## Development (worktrees, stacks, contributor workflows)
78
+
79
+ ### Setup (guided)
80
+
81
+ ```bash
82
+ npx happy-stacks setup --profile=dev
83
+ ```
84
+
85
+ ### Developing from a cloned repo
86
+
87
+ ```bash
88
+ git clone https://github.com/leeroybrun/happy-stacks.git
89
+ cd happy-stacks
90
+
91
+ node ./bin/happys.mjs setup --profile=dev
92
+ ```
93
+
94
+ Notes:
95
+
96
+ - In a cloned repo, `pnpm <script>` still works, but `happys <command>` is the recommended UX (same underlying scripts).
97
+ - To make the installed `~/.happy-stacks/bin/happys` shim (LaunchAgents / SwiftBar) run your local checkout without publishing to npm, set:
98
+
99
+ ```bash
100
+ echo 'HAPPY_STACKS_CLI_ROOT_DIR=/path/to/your/happy-stacks-checkout' >> ~/.happy-stacks/.env
101
+ ```
102
+
103
+ Or (recommended) persist it via init:
128
104
 
129
- - **Automated setup**: `happys init` + `happys start` gets the whole stack up and running.
105
+ ```bash
106
+ happys init --cli-root-dir=/path/to/your/happy-stacks-checkout
107
+ ```
108
+
109
+ ### Why this exists
110
+
111
+ - **Automated setup**: `happys setup` + `happys start` gets the whole stack up and running.
130
112
  - **No hosted dependency**: run the full stack on your own computer.
131
113
  - **Lower latency**: localhost/LAN is typically much faster than remote hosted servers.
132
114
  - **Custom forks**: easily use forks of the Happy UI + CLI (e.g. `leeroybrun/*`) while still contributing upstream to `slopus/*`.
@@ -134,7 +116,7 @@ Details (secure context, phone instructions, automation knobs): `[docs/remote-ac
134
116
  - **Stacks**: run multiple isolated instances in parallel (ports + dirs + component overrides).
135
117
  - **Remote access**: `happys tailscale ...` helps you get an HTTPS URL for mobile/remote devices.
136
118
 
137
- ## How Happy Stacks wires “local” URLs
119
+ ### How Happy Stacks wires “local” URLs
138
120
 
139
121
  There are two “URLs” to understand:
140
122
 
@@ -170,7 +152,7 @@ Diagram:
170
152
 
171
153
  More details + automation: `[docs/remote-access.md](docs/remote-access.md)`.
172
154
 
173
- ## How it’s organized
155
+ ### How it’s organized
174
156
 
175
157
  - **Scripts**: `scripts/*.mjs` (bootstrap/dev/start/build/stacks/worktrees/service/tailscale/mobile)
176
158
  - **Components**: `components/*` (each is its own Git repo)
@@ -183,9 +165,9 @@ Components:
183
165
  - `happy-server-light` (light server, can serve built UI)
184
166
  - `happy-server` (full server)
185
167
 
186
- ## Quickstarts (feature-focused)
168
+ ### Quickstarts (feature-focused)
187
169
 
188
- ### Remote access (Tailscale Serve)
170
+ #### Remote access (Tailscale Serve)
189
171
 
190
172
  ```bash
191
173
  happys tailscale enable
@@ -194,7 +176,7 @@ happys tailscale url
194
176
 
195
177
  Details: `[docs/remote-access.md](docs/remote-access.md)`.
196
178
 
197
- ### Worktrees + forks (clean upstream PRs)
179
+ #### Worktrees + forks (clean upstream PRs)
198
180
 
199
181
  Create a clean upstream PR worktree:
200
182
 
@@ -210,9 +192,40 @@ happys wt pr happy https://github.com/slopus/happy/pull/123 --use
210
192
  happys wt pr happy 123 --update --stash
211
193
  ```
212
194
 
195
+ Create a fully isolated PR stack (creates stack + PR worktrees + optional auth seeding + starts dev):
196
+
197
+ ```bash
198
+ happys stack pr pr123 \
199
+ --happy=https://github.com/slopus/happy/pull/123 \
200
+ --happy-cli=https://github.com/slopus/happy-cli/pull/456 \
201
+ --seed-auth --copy-auth-from=dev-auth --link-auth \
202
+ --dev
203
+ ```
204
+
205
+ One-shot “install + run PR stack” (best for maintainers who don’t have Happy Stacks set up yet):
206
+
207
+ ```bash
208
+ npx happy-stacks setup pr \
209
+ --happy=https://github.com/slopus/happy/pull/123 \
210
+ --happy-cli=https://github.com/slopus/happy-cli/pull/456
211
+ ```
212
+
213
+ You can also run it as:
214
+
215
+ ```bash
216
+ npx happy-stacks setup-pr \
217
+ --happy=https://github.com/slopus/happy/pull/123 \
218
+ --happy-cli=https://github.com/slopus/happy-cli/pull/456
219
+ ```
220
+
221
+ Updating when the PR changes:
222
+
223
+ - Re-run the same command to fast-forward the PR worktrees.
224
+ - If the PR was force-pushed, add `--force`.
225
+
213
226
  Details: `[docs/worktrees-and-forks.md](docs/worktrees-and-forks.md)`.
214
227
 
215
- ### Server flavor (server-light vs full server)
228
+ #### Server flavor (server-light vs full server)
216
229
 
217
230
  - Use `happy-server-light` for a light local stack (no Redis, no Postgres, no Docker), and UI serving via server-light.
218
231
  - 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 +246,7 @@ happys stack srv exp1 -- use --interactive
233
246
 
234
247
  Details: `[docs/server-flavors.md](docs/server-flavors.md)`.
235
248
 
236
- ### Stacks (multiple isolated instances)
249
+ #### Stacks (multiple isolated instances)
237
250
 
238
251
  ```bash
239
252
  happys stack new exp1 --interactive
@@ -250,7 +263,15 @@ happys stack dev exp1
250
263
 
251
264
  Details: `[docs/stacks.md](docs/stacks.md)`.
252
265
 
253
- ### Menu bar (SwiftBar)
266
+ #### Dev stacks: browser origin isolation (IMPORTANT)
267
+
268
+ Non-main stacks use a stack-specific localhost hostname (no `/etc/hosts` changes required):
269
+
270
+ - `http://happy-<stack>.localhost:<uiPort>`
271
+
272
+ This avoids browser auth/session collisions between stacks (separate origin per stack).
273
+
274
+ #### Menu bar (SwiftBar)
254
275
 
255
276
  ```bash
256
277
  happys menubar install
@@ -259,7 +280,7 @@ happys menubar open
259
280
 
260
281
  Details: `[docs/menubar.md](docs/menubar.md)`.
261
282
 
262
- ### Mobile iOS dev (optional)
283
+ #### Mobile iOS dev (optional)
263
284
 
264
285
  ```bash
265
286
  happys mobile --help
@@ -268,7 +289,7 @@ happys mobile --json
268
289
 
269
290
  Details: `[docs/mobile-ios.md](docs/mobile-ios.md)`.
270
291
 
271
- ### Tauri desktop app (optional)
292
+ #### Tauri desktop app (optional)
272
293
 
273
294
  ```bash
274
295
  happys build --tauri
@@ -276,13 +297,12 @@ happys build --tauri
276
297
 
277
298
  Details: `[docs/tauri.md](docs/tauri.md)`.
278
299
 
279
- ## Commands (high-signal)
300
+ ### Commands (high-signal)
280
301
 
281
302
  - **Setup**:
282
- - `happys init`
283
- - `happys bootstrap --interactive` (wizard)
284
- - `happys bootstrap --forks|--upstream`
285
- - `happys bootstrap --server=happy-server|happy-server-light|both`
303
+ - `happys setup` (guided; selfhost or dev)
304
+ - (advanced) `happys init` (plumbing: shims/runtime/pointer env)
305
+ - (advanced) `happys bootstrap --interactive` (component installer wizard)
286
306
  - **Run**:
287
307
  - `happys start` (production-like; serves built UI via server-light)
288
308
  - `happys dev` (dev; Expo web dev server for UI)
@@ -303,17 +323,18 @@ Details: `[docs/tauri.md](docs/tauri.md)`.
303
323
  - **Menu bar (SwiftBar)**:
304
324
  - `happys menubar install`
305
325
 
306
- ## Docs (deep dives)
326
+ ### Docs (deep dives)
307
327
 
308
328
  - **Remote access (Tailscale + phone)**: `[docs/remote-access.md](docs/remote-access.md)`
309
329
  - **Server flavors (server-light vs server)**: `[docs/server-flavors.md](docs/server-flavors.md)`
310
330
  - **Worktrees + forks workflow**: `[docs/worktrees-and-forks.md](docs/worktrees-and-forks.md)`
311
331
  - **Stacks (multiple instances)**: `[docs/stacks.md](docs/stacks.md)`
332
+ - **Paths + env precedence (home/workspace/runtime/stacks)**: `[docs/paths-and-env.md](docs/paths-and-env.md)`
312
333
  - **Menu bar (SwiftBar)**: `[docs/menubar.md](docs/menubar.md)`
313
334
  - **Mobile iOS dev**: `[docs/mobile-ios.md](docs/mobile-ios.md)`
314
335
  - **Tauri desktop app**: `[docs/tauri.md](docs/tauri.md)`
315
336
 
316
- ## Configuration
337
+ ### Configuration
317
338
 
318
339
  Where config lives by default:
319
340
 
@@ -329,4 +350,21 @@ Notes:
329
350
  - **Use `.env.example` as the canonical template** (copy it to `.env` if you’re running this repo directly).
330
351
  - 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
352
 
353
+ ### Sandbox / test installs (fully isolated)
354
+
355
+ If you want to test the full setup flow (including PR stacks) without impacting your “real” install, run with:
356
+
357
+ ```bash
358
+ npx happy-stacks --sandbox-dir /tmp/happy-stacks-sandbox setup pr --happy=123 --happy-cli=456
359
+ ```
360
+
361
+ To reset completely, just delete the sandbox folder:
362
+
363
+ ```bash
364
+ rm -rf /tmp/happy-stacks-sandbox
365
+ ```
366
+
367
+ Sandbox mode disables global OS side effects (PATH edits, SwiftBar plugin install, LaunchAgents/systemd services) by default.
368
+ To explicitly allow them for testing, set `HAPPY_STACKS_SANDBOX_ALLOW_GLOBAL=1`.
369
+
332
370
  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/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/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
  }