happy-stacks 0.2.0 → 0.4.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 (149) hide show
  1. package/README.md +84 -25
  2. package/bin/happys.mjs +116 -17
  3. package/docs/happy-development.md +2 -2
  4. package/docs/isolated-linux-vm.md +82 -0
  5. package/docs/mobile-ios.md +112 -54
  6. package/package.json +5 -1
  7. package/scripts/auth.mjs +59 -208
  8. package/scripts/build.mjs +58 -12
  9. package/scripts/cli-link.mjs +3 -3
  10. package/scripts/completion.mjs +5 -5
  11. package/scripts/daemon.mjs +168 -20
  12. package/scripts/dev.mjs +196 -70
  13. package/scripts/doctor.mjs +20 -36
  14. package/scripts/edison.mjs +105 -78
  15. package/scripts/happy.mjs +8 -19
  16. package/scripts/init.mjs +8 -14
  17. package/scripts/install.mjs +119 -23
  18. package/scripts/lint.mjs +31 -32
  19. package/scripts/menubar.mjs +6 -13
  20. package/scripts/migrate.mjs +11 -21
  21. package/scripts/mobile.mjs +93 -108
  22. package/scripts/mobile_dev_client.mjs +83 -0
  23. package/scripts/provision/linux-ubuntu-review-pr.sh +51 -0
  24. package/scripts/review.mjs +217 -0
  25. package/scripts/review_pr.mjs +368 -0
  26. package/scripts/run.mjs +95 -21
  27. package/scripts/self.mjs +11 -29
  28. package/scripts/server_flavor.mjs +4 -4
  29. package/scripts/service.mjs +19 -29
  30. package/scripts/setup.mjs +63 -160
  31. package/scripts/setup_pr.mjs +592 -52
  32. package/scripts/stack.mjs +608 -200
  33. package/scripts/stop.mjs +3 -3
  34. package/scripts/tailscale.mjs +44 -11
  35. package/scripts/test.mjs +52 -36
  36. package/scripts/tui.mjs +314 -74
  37. package/scripts/typecheck.mjs +31 -32
  38. package/scripts/ui_gateway.mjs +1 -1
  39. package/scripts/uninstall.mjs +6 -6
  40. package/scripts/utils/auth/daemon_gate.mjs +55 -0
  41. package/scripts/utils/auth/daemon_gate.test.mjs +37 -0
  42. package/scripts/utils/auth/dev_key.mjs +163 -0
  43. package/scripts/utils/{auth_files.mjs → auth/files.mjs} +2 -4
  44. package/scripts/utils/auth/guided_pr_auth.mjs +79 -0
  45. package/scripts/utils/auth/guided_stack_web_login.mjs +75 -0
  46. package/scripts/utils/auth/handy_master_secret.mjs +68 -0
  47. package/scripts/utils/auth/interactive_stack_auth.mjs +72 -0
  48. package/scripts/utils/{auth_login_ux.mjs → auth/login_ux.mjs} +32 -13
  49. package/scripts/utils/auth/sources.mjs +38 -0
  50. package/scripts/utils/auth/stack_guided_login.mjs +353 -0
  51. package/scripts/utils/cli/cli_registry.mjs +24 -0
  52. package/scripts/utils/cli/cwd_scope.mjs +82 -0
  53. package/scripts/utils/cli/cwd_scope.test.mjs +77 -0
  54. package/scripts/utils/cli/flags.mjs +17 -0
  55. package/scripts/utils/cli/log_forwarder.mjs +157 -0
  56. package/scripts/utils/cli/normalize.mjs +16 -0
  57. package/scripts/utils/cli/prereqs.mjs +72 -0
  58. package/scripts/utils/cli/progress.mjs +126 -0
  59. package/scripts/utils/cli/smoke_help.mjs +2 -2
  60. package/scripts/utils/cli/verbosity.mjs +12 -0
  61. package/scripts/utils/cli/wizard.mjs +1 -1
  62. package/scripts/utils/crypto/tokens.mjs +14 -0
  63. package/scripts/utils/{dev_daemon.mjs → dev/daemon.mjs} +51 -7
  64. package/scripts/utils/dev/expo_dev.mjs +246 -0
  65. package/scripts/utils/dev/expo_dev.test.mjs +76 -0
  66. package/scripts/utils/{dev_server.mjs → dev/server.mjs} +22 -32
  67. package/scripts/utils/dev_auth_key.mjs +1 -1
  68. package/scripts/utils/{config.mjs → env/config.mjs} +3 -2
  69. package/scripts/utils/{dotenv.mjs → env/dotenv.mjs} +3 -0
  70. package/scripts/utils/{env.mjs → env/env.mjs} +5 -3
  71. package/scripts/utils/{env_file.mjs → env/env_file.mjs} +2 -1
  72. package/scripts/utils/{env_local.mjs → env/env_local.mjs} +1 -0
  73. package/scripts/utils/env/read.mjs +30 -0
  74. package/scripts/utils/env/values.mjs +13 -0
  75. package/scripts/utils/expo/command.mjs +52 -0
  76. package/scripts/utils/{expo.mjs → expo/expo.mjs} +23 -10
  77. package/scripts/utils/expo/metro_ports.mjs +114 -0
  78. package/scripts/utils/fs/json.mjs +25 -0
  79. package/scripts/utils/fs/ops.mjs +29 -0
  80. package/scripts/utils/fs/package_json.mjs +8 -0
  81. package/scripts/utils/fs/tail.mjs +12 -0
  82. package/scripts/utils/git/git.mjs +67 -0
  83. package/scripts/utils/git/refs.mjs +26 -0
  84. package/scripts/utils/{worktrees.mjs → git/worktrees.mjs} +27 -23
  85. package/scripts/utils/handy_master_secret.mjs +2 -2
  86. package/scripts/utils/mobile/config.mjs +31 -0
  87. package/scripts/utils/mobile/dev_client_links.mjs +60 -0
  88. package/scripts/utils/mobile/identifiers.mjs +47 -0
  89. package/scripts/utils/mobile/identifiers.test.mjs +42 -0
  90. package/scripts/utils/mobile/ios_xcodeproj_patch.mjs +128 -0
  91. package/scripts/utils/mobile/ios_xcodeproj_patch.test.mjs +98 -0
  92. package/scripts/utils/net/dns.mjs +10 -0
  93. package/scripts/utils/net/lan_ip.mjs +24 -0
  94. package/scripts/utils/{ports.mjs → net/ports.mjs} +12 -6
  95. package/scripts/utils/net/url.mjs +30 -0
  96. package/scripts/utils/net/url.test.mjs +20 -0
  97. package/scripts/utils/paths/localhost_host.mjs +56 -0
  98. package/scripts/utils/{paths.mjs → paths/paths.mjs} +52 -45
  99. package/scripts/utils/{runtime.mjs → paths/runtime.mjs} +3 -1
  100. package/scripts/utils/proc/commands.mjs +34 -0
  101. package/scripts/utils/{ownership.mjs → proc/ownership.mjs} +1 -1
  102. package/scripts/utils/proc/package_scripts.mjs +31 -0
  103. package/scripts/utils/proc/parallel.mjs +25 -0
  104. package/scripts/utils/proc/pids.mjs +11 -0
  105. package/scripts/utils/{pm.mjs → proc/pm.mjs} +128 -158
  106. package/scripts/utils/{proc.mjs → proc/proc.mjs} +77 -2
  107. package/scripts/utils/review/base_ref.mjs +74 -0
  108. package/scripts/utils/review/base_ref.test.mjs +54 -0
  109. package/scripts/utils/review/runners/coderabbit.mjs +19 -0
  110. package/scripts/utils/review/runners/codex.mjs +51 -0
  111. package/scripts/utils/review/targets.mjs +24 -0
  112. package/scripts/utils/review/targets.test.mjs +36 -0
  113. package/scripts/utils/sandbox/review_pr_sandbox.mjs +106 -0
  114. package/scripts/utils/{happy_server_infra.mjs → server/infra/happy_server_infra.mjs} +10 -49
  115. package/scripts/utils/server/mobile_api_url.mjs +61 -0
  116. package/scripts/utils/server/mobile_api_url.test.mjs +41 -0
  117. package/scripts/utils/server/port.mjs +68 -0
  118. package/scripts/utils/{server.mjs → server/server.mjs} +12 -0
  119. package/scripts/utils/server/urls.mjs +101 -0
  120. package/scripts/utils/server/validate.mjs +88 -0
  121. package/scripts/utils/service/autostart_darwin.mjs +182 -0
  122. package/scripts/utils/service/autostart_darwin.test.mjs +50 -0
  123. package/scripts/utils/stack/context.mjs +23 -0
  124. package/scripts/utils/stack/dirs.mjs +27 -0
  125. package/scripts/utils/stack/editor_workspace.mjs +152 -0
  126. package/scripts/utils/stack/names.mjs +12 -0
  127. package/scripts/utils/stack/pr_stack_name.mjs +16 -0
  128. package/scripts/utils/stack/runtime_state.mjs +88 -0
  129. package/scripts/utils/stack/stacks.mjs +45 -0
  130. package/scripts/utils/{stack_startup.mjs → stack/startup.mjs} +9 -2
  131. package/scripts/utils/{stack_stop.mjs → stack/stop.mjs} +24 -19
  132. package/scripts/utils/stack_context.mjs +3 -3
  133. package/scripts/utils/stack_runtime_state.mjs +1 -1
  134. package/scripts/utils/stacks.mjs +2 -2
  135. package/scripts/utils/{browser.mjs → ui/browser.mjs} +1 -1
  136. package/scripts/utils/ui/qr.mjs +17 -0
  137. package/scripts/utils/ui/text.mjs +16 -0
  138. package/scripts/utils/validate.mjs +1 -1
  139. package/scripts/where.mjs +6 -6
  140. package/scripts/worktrees.mjs +171 -113
  141. package/scripts/utils/auth_sources.mjs +0 -12
  142. package/scripts/utils/dev_expo_web.mjs +0 -112
  143. package/scripts/utils/localhost_host.mjs +0 -17
  144. package/scripts/utils/server_port.mjs +0 -9
  145. package/scripts/utils/server_urls.mjs +0 -54
  146. /package/scripts/utils/{sandbox.mjs → env/sandbox.mjs} +0 -0
  147. /package/scripts/utils/{fs.mjs → fs/fs.mjs} +0 -0
  148. /package/scripts/utils/{canonical_home.mjs → paths/canonical_home.mjs} +0 -0
  149. /package/scripts/utils/{watch.mjs → proc/watch.mjs} +0 -0
package/README.md CHANGED
@@ -23,13 +23,6 @@ Recommended:
23
23
  npx happy-stacks setup --profile=selfhost
24
24
  ```
25
25
 
26
- Alternative (global install):
27
-
28
- ```bash
29
- npm install -g happy-stacks
30
- happys setup --profile=selfhost
31
- ```
32
-
33
26
  `setup` can optionally start Happy and guide you through authentication.
34
27
 
35
28
  ### Step 2: Start Happy
@@ -157,6 +150,7 @@ More details + automation: `[docs/remote-access.md](docs/remote-access.md)`.
157
150
  - **Scripts**: `scripts/*.mjs` (bootstrap/dev/start/build/stacks/worktrees/service/tailscale/mobile)
158
151
  - **Components**: `components/*` (each is its own Git repo)
159
152
  - **Worktrees**: `components/.worktrees/<component>/<owner>/<branch...>`
153
+ - **CWD-scoped commands**: if you run `happys test/typecheck/lint` from inside a component checkout/worktree and omit components, it runs just that component; `happys build/dev/start` also prefer the checkout you’re currently inside.
160
154
 
161
155
  Components:
162
156
 
@@ -192,7 +186,10 @@ happys wt pr happy https://github.com/slopus/happy/pull/123 --use
192
186
  happys wt pr happy 123 --update --stash
193
187
  ```
194
188
 
195
- Create a fully isolated PR stack (creates stack + PR worktrees + optional auth seeding + starts dev):
189
+ ##### Developer quickstart: create a PR stack (isolated ports/dirs; idempotent updates)
190
+
191
+ This creates (or reuses) a named stack, checks out PR worktrees for the selected components, optionally seeds auth, and starts the stack.
192
+ Re-run with `--reuse` to update the existing worktrees when the PR changes.
196
193
 
197
194
  ```bash
198
195
  happys stack pr pr123 \
@@ -202,15 +199,33 @@ happys stack pr pr123 \
202
199
  --dev
203
200
  ```
204
201
 
205
- One-shot “install + run PR stack” (best for maintainers who don’t have Happy Stacks set up yet):
202
+ Optional: enable Expo dev-client for mobile reviewers (reuses the same Expo dev server; no second Metro process):
206
203
 
207
204
  ```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
205
+ happys stack pr pr123 --happy=123 --happy-cli=456 --dev --mobile
206
+ ```
207
+
208
+ Optional: run it in a self-contained sandbox folder (delete it to uninstall completely):
209
+
210
+ ```bash
211
+ SANDBOX="$(mktemp -d /tmp/happy-stacks-sandbox.XXXXXX)"
212
+ happys --sandbox-dir "$SANDBOX" stack pr pr123 --happy=123 --happy-cli=456 --dev
213
+ rm -rf "$SANDBOX"
211
214
  ```
212
215
 
213
- You can also run it as:
216
+ Update when the PR changes:
217
+
218
+ - Re-run with `--reuse` to fast-forward worktrees when possible.
219
+ - If the PR was force-pushed, add `--force`.
220
+
221
+ ```bash
222
+ happys stack pr pr123 --happy=123 --happy-cli=456 --reuse
223
+ happys stack pr pr123 --happy=123 --happy-cli=456 --reuse --force
224
+ ```
225
+
226
+ ##### Maintainer quickstart: one-shot “install + run PR stack” (idempotent)
227
+
228
+ This is the maintainer-friendly entrypoint. It is safe to re-run and will keep the PR stack wiring intact.
214
229
 
215
230
  ```bash
216
231
  npx happy-stacks setup-pr \
@@ -218,11 +233,40 @@ npx happy-stacks setup-pr \
218
233
  --happy-cli=https://github.com/slopus/happy-cli/pull/456
219
234
  ```
220
235
 
221
- Updating when the PR changes:
236
+ Optional: enable Expo dev-client for mobile reviewers (works with both default `--dev` and `--start`):
237
+
238
+ ```bash
239
+ npx happy-stacks setup-pr --happy=123 --happy-cli=456 --mobile
240
+ ```
241
+
242
+ Optional: run it in a self-contained sandbox folder (auto-cleaned):
243
+
244
+ ```bash
245
+ npx happy-stacks review-pr --happy=123 --happy-cli=456
246
+ ```
247
+
248
+ Short form (PR numbers):
249
+
250
+ ```bash
251
+ npx happy-stacks setup-pr --happy=123 --happy-cli=456
252
+ ```
253
+
254
+ Override stack name (optional):
255
+
256
+ ```bash
257
+ npx happy-stacks setup-pr --name=pr123 --happy=123 --happy-cli=456
258
+ ```
259
+
260
+ Update when the PR changes:
222
261
 
223
262
  - Re-run the same command to fast-forward the PR worktrees.
224
263
  - If the PR was force-pushed, add `--force`.
225
264
 
265
+ ```bash
266
+ npx happy-stacks setup-pr --happy=123 --happy-cli=456
267
+ npx happy-stacks setup-pr --happy=123 --happy-cli=456 --force
268
+ ```
269
+
226
270
  Details: `[docs/worktrees-and-forks.md](docs/worktrees-and-forks.md)`.
227
271
 
228
272
  #### Server flavor (server-light vs full server)
@@ -283,12 +327,20 @@ Details: `[docs/menubar.md](docs/menubar.md)`.
283
327
  #### Mobile iOS dev (optional)
284
328
 
285
329
  ```bash
286
- happys mobile --help
287
- happys mobile --json
330
+ # Install the shared "Happy Stacks Dev" dev-client app on your iPhone:
331
+ happys mobile-dev-client --install
332
+
333
+ # Install an isolated per-stack app (Release config, unique bundle id + scheme):
334
+ happys stack mobile:install <stack> --name="Happy (<stack>)"
288
335
  ```
289
336
 
290
337
  Details: `[docs/mobile-ios.md](docs/mobile-ios.md)`.
291
338
 
339
+ #### Reviewing PRs in an isolated sandbox
340
+
341
+ - **Unique hostname per run (default)**: `happys review-pr` generates a unique stack name by default, which results in a unique `happy-<stack>.localhost` hostname. This prevents browser storage collisions when the sandbox is deleted between runs.
342
+ - **Reuse an existing sandbox**: if a previous run preserved a sandbox (e.g. `--keep-sandbox` or a failure in verbose mode), re-running `happys review-pr` offers an interactive choice to reuse it (keeping the same hostname + on-disk auth), or create a fresh sandbox.
343
+
292
344
  #### Tauri desktop app (optional)
293
345
 
294
346
  ```bash
@@ -305,7 +357,7 @@ Details: `[docs/tauri.md](docs/tauri.md)`.
305
357
  - (advanced) `happys bootstrap --interactive` (component installer wizard)
306
358
  - **Run**:
307
359
  - `happys start` (production-like; serves built UI via server-light)
308
- - `happys dev` (dev; Expo web dev server for UI)
360
+ - `happys dev` (dev; Expo dev server for UI, optional dev-client via `--mobile`)
309
361
  - **Server flavor**:
310
362
  - `happys srv status`
311
363
  - `happys srv use --interactive`
@@ -319,7 +371,10 @@ Details: `[docs/tauri.md](docs/tauri.md)`.
319
371
  - `happys stack dev <name>` / `happys stack start <name>`
320
372
  - `happys stack edit <name> --interactive`
321
373
  - `happys stack wt <name> -- use --interactive`
374
+ - `happys stack review <name> [component...] [--reviewers=coderabbit,codex] [--base-ref=<ref>]`
322
375
  - `happys stack migrate`
376
+ - **Reviews (local diff review)**:
377
+ - `happys review [component...] [--reviewers=coderabbit,codex] [--base-remote=<remote>] [--base-branch=<branch>] [--base-ref=<ref>]`
323
378
  - **Menu bar (SwiftBar)**:
324
379
  - `happys menubar install`
325
380
 
@@ -352,19 +407,23 @@ Notes:
352
407
 
353
408
  ### Sandbox / test installs (fully isolated)
354
409
 
355
- If you want to test the full setup flow (including PR stacks) without impacting your “real” install, run with:
410
+ If you want to test the full setup flow (including PR stacks) without impacting your “real” install, run everything with `--sandbox-dir`.
411
+ To fully uninstall the test run, stop the sandbox stacks and delete the sandbox folder.
356
412
 
357
413
  ```bash
358
- npx happy-stacks --sandbox-dir /tmp/happy-stacks-sandbox setup pr --happy=123 --happy-cli=456
359
- ```
414
+ SANDBOX="$(mktemp -d /tmp/happy-stacks-sandbox.XXXXXX)"
360
415
 
361
- To reset completely, just delete the sandbox folder:
416
+ # Run a PR stack (fully isolated install)
417
+ npx happy-stacks --sandbox-dir "$SANDBOX" setup-pr --happy=123 --happy-cli=456
362
418
 
363
- ```bash
364
- rm -rf /tmp/happy-stacks-sandbox
419
+ # Tear down + uninstall
420
+ npx happy-stacks --sandbox-dir "$SANDBOX" stop --yes --no-service
421
+ rm -rf "$SANDBOX"
365
422
  ```
366
423
 
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`.
424
+ Notes:
425
+
426
+ - Sandbox mode disables global OS side effects (**PATH edits**, **SwiftBar plugin install**, **LaunchAgents/systemd services**, **Tailscale Serve enable/disable**) by default.
427
+ - To explicitly allow those for testing, set `HAPPY_STACKS_SANDBOX_ALLOW_GLOBAL=1` (still recommended to clean up after).
369
428
 
370
429
  For contributor/LLM workflow expectations: `[AGENTS.md](AGENTS.md)`.
package/bin/happys.mjs CHANGED
@@ -8,13 +8,13 @@ import { homedir } from 'node:os';
8
8
  import { dirname, join } from 'node:path';
9
9
  import { fileURLToPath } from 'node:url';
10
10
  import { commandHelpArgs, renderHappysRootHelp, resolveHappysCommand } from '../scripts/utils/cli/cli_registry.mjs';
11
- import { expandHome, getCanonicalHomeEnvPathFromEnv } from '../scripts/utils/canonical_home.mjs';
11
+ import { expandHome, getCanonicalHomeEnvPathFromEnv } from '../scripts/utils/paths/canonical_home.mjs';
12
12
 
13
13
  function getCliRootDir() {
14
14
  return dirname(dirname(fileURLToPath(import.meta.url)));
15
15
  }
16
16
 
17
- // expandHome is imported from scripts/utils/canonical_home.mjs
17
+ // expandHome is imported from scripts/utils/paths/canonical_home.mjs
18
18
 
19
19
  function dotenvGetQuick(envPath, key) {
20
20
  try {
@@ -110,6 +110,42 @@ function stripGlobalOpt(argv, { name, aliases = [] }) {
110
110
  return { value: '', argv };
111
111
  }
112
112
 
113
+ function applyVerbosityIfRequested(argv) {
114
+ // Global verbosity:
115
+ // - supports -v/-vv/-vvv anywhere before/after the command
116
+ // - supports --verbose and --verbose=N
117
+ //
118
+ // We set HAPPY_STACKS_VERBOSE (0-3) and strip these args so downstream scripts don't need to support them.
119
+ let level = Number.isFinite(Number(process.env.HAPPY_STACKS_VERBOSE)) ? Number(process.env.HAPPY_STACKS_VERBOSE) : null;
120
+ let next = [];
121
+ for (const a of argv) {
122
+ if (a === '-v' || a === '-vv' || a === '-vvv') {
123
+ const n = a.length - 1;
124
+ level = Math.max(level ?? 0, n);
125
+ continue;
126
+ }
127
+ if (a === '--verbose') {
128
+ level = Math.max(level ?? 0, 1);
129
+ continue;
130
+ }
131
+ if (a.startsWith('--verbose=')) {
132
+ const raw = a.slice('--verbose='.length).trim();
133
+ const n = Number(raw);
134
+ if (Number.isFinite(n)) {
135
+ level = Math.max(level ?? 0, Math.max(0, Math.min(3, Math.floor(n))));
136
+ } else {
137
+ level = Math.max(level ?? 0, 1);
138
+ }
139
+ continue;
140
+ }
141
+ next.push(a);
142
+ }
143
+ if (level != null) {
144
+ process.env.HAPPY_STACKS_VERBOSE = String(Math.max(0, Math.min(3, Math.floor(level))));
145
+ }
146
+ return next;
147
+ }
148
+
113
149
  function applySandboxDirIfRequested(argv) {
114
150
  const explicit = (process.env.HAPPY_STACKS_SANDBOX_DIR ?? '').trim();
115
151
  const { value, argv: nextArgv } = stripGlobalOpt(argv, { name: '--sandbox-dir', aliases: ['--sandbox'] });
@@ -117,6 +153,8 @@ function applySandboxDirIfRequested(argv) {
117
153
  if (!raw) return { argv: nextArgv, enabled: false };
118
154
 
119
155
  const sandboxDir = expandHome(raw);
156
+ const allowGlobalRaw = (process.env.HAPPY_STACKS_SANDBOX_ALLOW_GLOBAL ?? '').trim().toLowerCase();
157
+ const allowGlobal = allowGlobalRaw === '1' || allowGlobalRaw === 'true' || allowGlobalRaw === 'yes' || allowGlobalRaw === 'y';
120
158
  // Keep all state under one folder that can be deleted to reset completely.
121
159
  const canonicalHomeDir = join(sandboxDir, 'canonical');
122
160
  const homeDir = join(sandboxDir, 'home');
@@ -124,23 +162,76 @@ function applySandboxDirIfRequested(argv) {
124
162
  const runtimeDir = join(sandboxDir, 'runtime');
125
163
  const storageDir = join(sandboxDir, 'storage');
126
164
 
165
+ // Sandbox isolation MUST win over any pre-exported Happy Stacks env vars.
166
+ // Otherwise sandbox runs can accidentally read/write "real" machine state.
167
+ //
168
+ // Keep only a tiny set of sandbox-safe globals; everything else should be driven by flags
169
+ // and stack env files inside the sandbox.
170
+ const preserved = new Map();
171
+ const keepKeys = [
172
+ 'HAPPY_STACKS_VERBOSE',
173
+ 'HAPPY_STACKS_INVOKED_CWD',
174
+ 'HAPPY_STACKS_SANDBOX_DIR',
175
+ 'HAPPY_STACKS_SANDBOX_ALLOW_GLOBAL',
176
+ 'HAPPY_STACKS_UPDATE_CHECK',
177
+ 'HAPPY_STACKS_UPDATE_CHECK_INTERVAL_MS',
178
+ 'HAPPY_STACKS_UPDATE_NOTIFY_INTERVAL_MS',
179
+ ];
180
+ for (const k of keepKeys) {
181
+ if (process.env[k] != null && String(process.env[k]).trim() !== '') {
182
+ preserved.set(k, process.env[k]);
183
+ }
184
+ }
185
+ for (const k of Object.keys(process.env)) {
186
+ if (k.startsWith('HAPPY_STACKS_') || k.startsWith('HAPPY_LOCAL_')) {
187
+ delete process.env[k];
188
+ continue;
189
+ }
190
+ // Also clear unprefixed Happy vars; sandbox commands should compute these from stack state.
191
+ if (k === 'HAPPY_HOME_DIR' || k === 'HAPPY_SERVER_URL' || k === 'HAPPY_WEBAPP_URL') {
192
+ delete process.env[k];
193
+ }
194
+ }
195
+ for (const [k, v] of preserved.entries()) {
196
+ process.env[k] = v;
197
+ }
198
+
127
199
  process.env.HAPPY_STACKS_SANDBOX_DIR = sandboxDir;
128
200
  process.env.HAPPY_STACKS_CLI_ROOT_DISABLE = '1'; // never re-exec into a user's "real" install when sandboxing
129
201
 
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;
202
+ // In sandbox mode, we MUST force all state directories into the sandbox, even if the user
203
+ // exported HAPPY_STACKS_* in their shell. Otherwise sandbox runs can accidentally read/write
204
+ // "real" machine state (breaking isolation).
205
+ process.env.HAPPY_STACKS_CANONICAL_HOME_DIR = canonicalHomeDir;
206
+ process.env.HAPPY_LOCAL_CANONICAL_HOME_DIR = canonicalHomeDir;
207
+
208
+ process.env.HAPPY_STACKS_HOME_DIR = homeDir;
209
+ process.env.HAPPY_LOCAL_HOME_DIR = homeDir;
210
+
211
+ process.env.HAPPY_STACKS_WORKSPACE_DIR = workspaceDir;
212
+ process.env.HAPPY_LOCAL_WORKSPACE_DIR = workspaceDir;
213
+
214
+ process.env.HAPPY_STACKS_RUNTIME_DIR = runtimeDir;
215
+ process.env.HAPPY_LOCAL_RUNTIME_DIR = runtimeDir;
216
+
217
+ process.env.HAPPY_STACKS_STORAGE_DIR = storageDir;
218
+ process.env.HAPPY_LOCAL_STORAGE_DIR = storageDir;
219
+
220
+ // Sandbox default: disallow global side effects unless explicitly opted in.
221
+ // This keeps sandbox runs fast, deterministic, and isolated.
222
+ if (!allowGlobal) {
223
+ // Network-y UX (background update checks) are not useful in a temporary sandbox.
224
+ process.env.HAPPY_STACKS_UPDATE_CHECK = '0';
225
+ process.env.HAPPY_STACKS_UPDATE_CHECK_INTERVAL_MS = '0';
226
+ process.env.HAPPY_STACKS_UPDATE_NOTIFY_INTERVAL_MS = '0';
227
+
228
+ // Never auto-enable or reset Tailscale Serve in sandbox.
229
+ // (Tailscale is global machine state; sandbox runs must not touch it.)
230
+ process.env.HAPPY_LOCAL_TAILSCALE_SERVE = '0';
231
+ process.env.HAPPY_STACKS_TAILSCALE_SERVE = '0';
232
+ process.env.HAPPY_LOCAL_TAILSCALE_RESET_ON_EXIT = '0';
233
+ process.env.HAPPY_STACKS_TAILSCALE_RESET_ON_EXIT = '0';
234
+ }
144
235
 
145
236
  return { argv: nextArgv, enabled: true };
146
237
  }
@@ -248,8 +339,16 @@ function runNodeScript(cliRootDir, scriptRelPath, args) {
248
339
  function main() {
249
340
  const cliRootDir = getCliRootDir();
250
341
  const initialArgv = process.argv.slice(2);
251
- const { argv, enabled: sandboxed } = applySandboxDirIfRequested(initialArgv);
342
+ const argv0 = applyVerbosityIfRequested(initialArgv);
343
+ const { argv, enabled: sandboxed } = applySandboxDirIfRequested(argv0);
252
344
  void sandboxed;
345
+
346
+ // Preserve the original working directory across re-exec to the CLI root so commands can infer
347
+ // component/worktree context even when the actual scripts run with cwd=cliRootDir.
348
+ if (!(process.env.HAPPY_STACKS_INVOKED_CWD ?? '').trim()) {
349
+ process.env.HAPPY_STACKS_INVOKED_CWD = process.cwd();
350
+ }
351
+
253
352
  maybeReexecToCliRoot(cliRootDir);
254
353
 
255
354
  // If the user passed only flags (common via `npx happy-stacks --help`),
@@ -559,9 +559,9 @@ Most commands support `--help` and `--json`.
559
559
  ### Core run commands
560
560
 
561
561
  - **`happys start`**: production-like run (no Expo)
562
- - Flags: `--server=happy-server|happy-server-light`, `--restart`, `--no-daemon`, `--no-ui`, `--no-browser`
562
+ - Flags: `--server=happy-server|happy-server-light`, `--restart`, `--no-daemon`, `--no-ui`, `--no-browser`, `--mobile`
563
563
  - **`happys dev`**: dev run (server + daemon + Expo web)
564
- - Flags: `--server=happy-server|happy-server-light`, `--restart`, `--no-daemon`, `--no-ui`, `--watch`, `--no-watch`, `--no-browser`
564
+ - Flags: `--server=happy-server|happy-server-light`, `--restart`, `--no-daemon`, `--no-ui`, `--watch`, `--no-watch`, `--no-browser`, `--mobile`
565
565
  - **`happys stop`**: stop stacks and related processes
566
566
  - Flags: `--except-stacks=main,exp1`, `--yes`, `--aggressive`, `--sweep-owned`, `--no-docker`, `--no-service`
567
567
 
@@ -0,0 +1,82 @@
1
+ # Isolated Linux VM (Apple Silicon) for `review-pr`
2
+
3
+ If you want to validate `happys review-pr` on a **fresh system** (no existing `~/.happy-stacks`, no host tooling), the simplest repeatable approach on Apple Silicon is a Linux VM managed by **Lima** (it uses Apple’s Virtualization.framework).
4
+
5
+ This avoids Docker/container UX issues (browser opening, Expo networking, file watching) while still being truly “clean”.
6
+
7
+ ## Option A (recommended): Lima + Ubuntu ARM64
8
+
9
+ ### 1) Install Lima (macOS host)
10
+
11
+ ```bash
12
+ brew install lima
13
+ ```
14
+
15
+ ### 2) Create a VM
16
+
17
+ ```bash
18
+ limactl create --name happy-pr --tty=false template://ubuntu-24.04
19
+ limactl start happy-pr
20
+ ```
21
+
22
+ ### 3) Provision the VM (Node + build deps)
23
+
24
+ ```bash
25
+ limactl shell happy-pr
26
+ ```
27
+
28
+ Inside the VM:
29
+
30
+ ```bash
31
+ curl -fsSL https://raw.githubusercontent.com/leeroybrun/happy-local/main/scripts/provision/linux-ubuntu-review-pr.sh -o /tmp/linux-ubuntu-review-pr.sh && chmod +x /tmp/linux-ubuntu-review-pr.sh && /tmp/linux-ubuntu-review-pr.sh
32
+ ```
33
+
34
+ ### 4) Run `review-pr` via `npx` (published package)
35
+
36
+ Inside the VM:
37
+
38
+ ```bash
39
+ npx --yes happy-stacks@latest review-pr \
40
+ --happy=https://github.com/leeroybrun/happy/pull/10 \
41
+ --happy-cli=https://github.com/leeroybrun/happy-cli/pull/12 \
42
+ --no-mobile \
43
+ --verbose
44
+ ```
45
+
46
+ Notes:
47
+ - `--no-mobile` keeps the validation focused (Expo mobile dev-client adds more host requirements).
48
+ - You can also add `--keep-sandbox` if you want to inspect the sandbox contents after a failure.
49
+ - For full reproducibility, pin the version: `npx --yes happy-stacks@0.3.0 review-pr ...`
50
+
51
+ ### Optional: test **unreleased local changes**
52
+
53
+ If you need to test changes that aren’t published to npm yet:
54
+
55
+ 1) On your Mac (repo checkout):
56
+
57
+ ```bash
58
+ npm pack
59
+ ```
60
+
61
+ 2) Copy the generated `happy-stacks-*.tgz` into the VM (any method you like), then inside the VM:
62
+
63
+ ```bash
64
+ npx --yes ./happy-stacks-*.tgz review-pr ...
65
+ ```
66
+
67
+ ## Option B: GUI VM (UTM) – simplest when you want a “real desktop”
68
+
69
+ If you want the most realistic “reviewer” experience (open browser, etc.), a GUI VM is great:
70
+
71
+ 1. Install UTM (macOS host): `brew install --cask utm`
72
+ 2. Create an Ubuntu 24.04 ARM64 VM (UTM wizard).
73
+ 3. Run the same provisioning + `node bin/happys.mjs review-pr ...` inside the VM.
74
+
75
+ ## Option C: Apple “container” / Docker
76
+
77
+ Containers are excellent for server-only validation, but are usually **not** the best fit for end-to-end `review-pr` UX because:
78
+ - opening the host browser from inside the container is awkward
79
+ - Expo/dev-server workflows and networking tend to require extra port mapping and host interaction
80
+
81
+ Use containers only if you explicitly want “CLI-only” checks and are okay opening URLs manually.
82
+