happy-stacks 0.3.0 → 0.5.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 (165) hide show
  1. package/README.md +93 -40
  2. package/bin/happys.mjs +158 -16
  3. package/docs/codex-mcp-resume.md +130 -0
  4. package/docs/commit-audits/happy/leeroy-wip.commit-analysis.md +17640 -0
  5. package/docs/commit-audits/happy/leeroy-wip.commit-export.fuller-stat.md +3845 -0
  6. package/docs/commit-audits/happy/leeroy-wip.commit-inventory.md +102 -0
  7. package/docs/commit-audits/happy/leeroy-wip.commit-manual-review.md +1452 -0
  8. package/docs/commit-audits/happy/leeroy-wip.manual-review-queue.md +116 -0
  9. package/docs/happy-development.md +3 -4
  10. package/docs/isolated-linux-vm.md +82 -0
  11. package/docs/mobile-ios.md +112 -54
  12. package/docs/monorepo-migration.md +286 -0
  13. package/docs/server-flavors.md +19 -3
  14. package/docs/stacks.md +35 -0
  15. package/package.json +5 -1
  16. package/scripts/auth.mjs +32 -10
  17. package/scripts/build.mjs +55 -8
  18. package/scripts/daemon.mjs +166 -10
  19. package/scripts/dev.mjs +198 -50
  20. package/scripts/doctor.mjs +0 -4
  21. package/scripts/edison.mjs +6 -4
  22. package/scripts/env.mjs +150 -0
  23. package/scripts/env_cmd.test.mjs +128 -0
  24. package/scripts/init.mjs +8 -3
  25. package/scripts/install.mjs +207 -69
  26. package/scripts/lint.mjs +24 -4
  27. package/scripts/migrate.mjs +3 -12
  28. package/scripts/mobile.mjs +88 -104
  29. package/scripts/mobile_dev_client.mjs +83 -0
  30. package/scripts/monorepo.mjs +1096 -0
  31. package/scripts/monorepo_port.test.mjs +1470 -0
  32. package/scripts/provision/linux-ubuntu-review-pr.sh +51 -0
  33. package/scripts/review.mjs +908 -0
  34. package/scripts/review_pr.mjs +353 -0
  35. package/scripts/run.mjs +101 -21
  36. package/scripts/service.mjs +2 -2
  37. package/scripts/setup.mjs +189 -68
  38. package/scripts/setup_pr.mjs +586 -38
  39. package/scripts/stack.mjs +990 -196
  40. package/scripts/stack_archive_cmd.test.mjs +91 -0
  41. package/scripts/stack_editor_workspace_monorepo_root.test.mjs +65 -0
  42. package/scripts/stack_env_cmd.test.mjs +87 -0
  43. package/scripts/stack_happy_cmd.test.mjs +126 -0
  44. package/scripts/stack_interactive_monorepo_group.test.mjs +71 -0
  45. package/scripts/stack_monorepo_defaults.test.mjs +62 -0
  46. package/scripts/stack_monorepo_server_light_from_happy_spec.test.mjs +66 -0
  47. package/scripts/stack_server_flavors_defaults.test.mjs +55 -0
  48. package/scripts/stack_shorthand_cmd.test.mjs +55 -0
  49. package/scripts/stack_wt_list.test.mjs +128 -0
  50. package/scripts/tailscale.mjs +37 -1
  51. package/scripts/test.mjs +45 -8
  52. package/scripts/tui.mjs +395 -39
  53. package/scripts/typecheck.mjs +24 -4
  54. package/scripts/utils/auth/daemon_gate.mjs +55 -0
  55. package/scripts/utils/auth/daemon_gate.test.mjs +37 -0
  56. package/scripts/utils/auth/guided_pr_auth.mjs +79 -0
  57. package/scripts/utils/auth/guided_stack_web_login.mjs +75 -0
  58. package/scripts/utils/auth/interactive_stack_auth.mjs +72 -0
  59. package/scripts/utils/auth/login_ux.mjs +32 -13
  60. package/scripts/utils/auth/sources.mjs +26 -0
  61. package/scripts/utils/auth/stack_guided_login.mjs +353 -0
  62. package/scripts/utils/cli/cli_registry.mjs +43 -4
  63. package/scripts/utils/cli/cwd_scope.mjs +136 -0
  64. package/scripts/utils/cli/cwd_scope.test.mjs +110 -0
  65. package/scripts/utils/cli/log_forwarder.mjs +157 -0
  66. package/scripts/utils/cli/prereqs.mjs +75 -0
  67. package/scripts/utils/cli/prereqs.test.mjs +34 -0
  68. package/scripts/utils/cli/progress.mjs +126 -0
  69. package/scripts/utils/cli/verbosity.mjs +12 -0
  70. package/scripts/utils/cli/wizard.mjs +17 -9
  71. package/scripts/utils/cli/wizard_prompt_worktree_source_lazy.test.mjs +60 -0
  72. package/scripts/utils/dev/daemon.mjs +61 -4
  73. package/scripts/utils/dev/expo_dev.mjs +430 -0
  74. package/scripts/utils/dev/expo_dev.test.mjs +76 -0
  75. package/scripts/utils/dev/server.mjs +36 -42
  76. package/scripts/utils/dev_auth_key.mjs +169 -0
  77. package/scripts/utils/edison/git_roots.mjs +29 -0
  78. package/scripts/utils/edison/git_roots.test.mjs +36 -0
  79. package/scripts/utils/env/env.mjs +7 -3
  80. package/scripts/utils/env/env_file.mjs +4 -2
  81. package/scripts/utils/env/env_file.test.mjs +44 -0
  82. package/scripts/utils/expo/command.mjs +52 -0
  83. package/scripts/utils/expo/expo.mjs +20 -1
  84. package/scripts/utils/expo/metro_ports.mjs +114 -0
  85. package/scripts/utils/git/git.mjs +67 -0
  86. package/scripts/utils/git/worktrees.mjs +80 -25
  87. package/scripts/utils/git/worktrees_monorepo.test.mjs +54 -0
  88. package/scripts/utils/handy_master_secret.mjs +94 -0
  89. package/scripts/utils/mobile/config.mjs +31 -0
  90. package/scripts/utils/mobile/dev_client_links.mjs +60 -0
  91. package/scripts/utils/mobile/identifiers.mjs +47 -0
  92. package/scripts/utils/mobile/identifiers.test.mjs +42 -0
  93. package/scripts/utils/mobile/ios_xcodeproj_patch.mjs +128 -0
  94. package/scripts/utils/mobile/ios_xcodeproj_patch.test.mjs +98 -0
  95. package/scripts/utils/net/lan_ip.mjs +24 -0
  96. package/scripts/utils/net/ports.mjs +9 -1
  97. package/scripts/utils/net/tcp_forward.mjs +162 -0
  98. package/scripts/utils/net/url.mjs +30 -0
  99. package/scripts/utils/net/url.test.mjs +20 -0
  100. package/scripts/utils/paths/localhost_host.mjs +50 -3
  101. package/scripts/utils/paths/paths.mjs +159 -40
  102. package/scripts/utils/paths/paths_monorepo.test.mjs +58 -0
  103. package/scripts/utils/paths/paths_server_flavors.test.mjs +45 -0
  104. package/scripts/utils/proc/commands.mjs +2 -3
  105. package/scripts/utils/proc/parallel.mjs +25 -0
  106. package/scripts/utils/proc/pm.mjs +176 -22
  107. package/scripts/utils/proc/pm_spawn.test.mjs +76 -0
  108. package/scripts/utils/proc/pm_stack_cache_env.test.mjs +142 -0
  109. package/scripts/utils/proc/proc.mjs +136 -4
  110. package/scripts/utils/proc/proc.test.mjs +77 -0
  111. package/scripts/utils/review/base_ref.mjs +74 -0
  112. package/scripts/utils/review/base_ref.test.mjs +54 -0
  113. package/scripts/utils/review/chunks.mjs +55 -0
  114. package/scripts/utils/review/chunks.test.mjs +51 -0
  115. package/scripts/utils/review/findings.mjs +165 -0
  116. package/scripts/utils/review/findings.test.mjs +85 -0
  117. package/scripts/utils/review/head_slice.mjs +153 -0
  118. package/scripts/utils/review/head_slice.test.mjs +91 -0
  119. package/scripts/utils/review/instructions/deep.md +20 -0
  120. package/scripts/utils/review/runners/coderabbit.mjs +61 -0
  121. package/scripts/utils/review/runners/coderabbit.test.mjs +59 -0
  122. package/scripts/utils/review/runners/codex.mjs +61 -0
  123. package/scripts/utils/review/runners/codex.test.mjs +35 -0
  124. package/scripts/utils/review/slices.mjs +140 -0
  125. package/scripts/utils/review/slices.test.mjs +32 -0
  126. package/scripts/utils/review/targets.mjs +24 -0
  127. package/scripts/utils/review/targets.test.mjs +36 -0
  128. package/scripts/utils/sandbox/review_pr_sandbox.mjs +106 -0
  129. package/scripts/utils/server/flavor_scripts.mjs +98 -0
  130. package/scripts/utils/server/flavor_scripts.test.mjs +146 -0
  131. package/scripts/utils/server/mobile_api_url.mjs +61 -0
  132. package/scripts/utils/server/mobile_api_url.test.mjs +41 -0
  133. package/scripts/utils/server/prisma_import.mjs +37 -0
  134. package/scripts/utils/server/prisma_import.test.mjs +70 -0
  135. package/scripts/utils/server/ui_env.mjs +14 -0
  136. package/scripts/utils/server/ui_env.test.mjs +46 -0
  137. package/scripts/utils/server/urls.mjs +14 -4
  138. package/scripts/utils/server/validate.mjs +53 -16
  139. package/scripts/utils/server/validate.test.mjs +89 -0
  140. package/scripts/utils/service/autostart_darwin.mjs +42 -2
  141. package/scripts/utils/service/autostart_darwin.test.mjs +50 -0
  142. package/scripts/utils/stack/context.mjs +2 -2
  143. package/scripts/utils/stack/editor_workspace.mjs +6 -6
  144. package/scripts/utils/stack/interactive_stack_config.mjs +185 -0
  145. package/scripts/utils/stack/pr_stack_name.mjs +16 -0
  146. package/scripts/utils/stack/runtime_state.mjs +2 -1
  147. package/scripts/utils/stack/startup.mjs +120 -13
  148. package/scripts/utils/stack/startup_server_light_dirs.test.mjs +64 -0
  149. package/scripts/utils/stack/startup_server_light_generate.test.mjs +70 -0
  150. package/scripts/utils/stack/startup_server_light_legacy.test.mjs +88 -0
  151. package/scripts/utils/stack/stop.mjs +15 -4
  152. package/scripts/utils/stack_context.mjs +23 -0
  153. package/scripts/utils/stack_runtime_state.mjs +104 -0
  154. package/scripts/utils/stacks.mjs +38 -0
  155. package/scripts/utils/tailscale/ip.mjs +116 -0
  156. package/scripts/utils/ui/ansi.mjs +39 -0
  157. package/scripts/utils/ui/qr.mjs +17 -0
  158. package/scripts/utils/validate.mjs +88 -0
  159. package/scripts/where.mjs +2 -2
  160. package/scripts/worktrees.mjs +755 -179
  161. package/scripts/worktrees_archive_cmd.test.mjs +245 -0
  162. package/scripts/worktrees_cursor_monorepo_root.test.mjs +63 -0
  163. package/scripts/worktrees_list_specs_no_recurse.test.mjs +33 -0
  164. package/scripts/worktrees_monorepo_use_group.test.mjs +67 -0
  165. package/scripts/utils/dev/expo_web.mjs +0 -112
package/README.md CHANGED
@@ -4,28 +4,42 @@ Run [**Happy**](https://happy.engineering/) locally and access it remotely and s
4
4
 
5
5
  ## What is Happy?
6
6
 
7
- Happy is an UI/CLI stack (server + web UI + CLI + daemon) who let you monitor and interact with Claude Code, Codex and Gemini sessions from your mobile, a web UI and/or a desktop app.
7
+ Happy is an UI/CLI stack (server + web UI + CLI + daemon) who let you monitor and interact with Claude Code, Codex and Gemini sessions from your mobile, from a web UI and/or from a desktop app.
8
8
 
9
9
  ## What is Happy Stacks?
10
10
 
11
11
  happy-stacks is a guided installer + local orchestration CLI for Happy.
12
12
 
13
- If you only want to **self-host Happy**, start with the **Self-host** section below.
13
+ If you only want to **use Happy** and self-host it on your computer, start with the **Self-host** section below.
14
14
  If you want to **develop Happy** (worktrees, multiple stacks, upstream PR workflows), see the **Development** section further down.
15
15
 
16
16
  ## Self-host Happy (install + run)
17
17
 
18
- ### Step 1: Setup
19
-
20
- Recommended:
18
+ ### Quickstart
21
19
 
22
20
  ```bash
23
21
  npx happy-stacks setup --profile=selfhost
24
22
  ```
25
23
 
26
- `setup` can optionally start Happy and guide you through authentication.
24
+ Follow the guided instructions to install Happy and launch it.
25
+
26
+ ### Daily use
27
+
28
+ #### Configure provider API keys for the daemon
27
29
 
28
- ### Step 2: Start Happy
30
+ If you want the daemon to have access to provider API keys (for example OpenAI), you can set them so they are automatically loaded when the daemon starts:
31
+
32
+ ```bash
33
+ happys env set OPENAI_API_KEY=sk-...
34
+ ```
35
+
36
+ Then restart so the daemon picks up the new environment:
37
+
38
+ ```bash
39
+ happys start --restart
40
+ ```
41
+
42
+ ### Start Happy
29
43
 
30
44
  Starts the local server, CLI daemon, and serves the pre-built UI.
31
45
 
@@ -33,9 +47,9 @@ Starts the local server, CLI daemon, and serves the pre-built UI.
33
47
  happys start
34
48
  ```
35
49
 
36
- ### Step 3 (first run only): authenticate
50
+ ### Authentication
37
51
 
38
- On a **fresh machine** (or any new stack), the daemon needs to authenticate once before it can register a “machine”.
52
+ On a **fresh machine**, the daemon needs to authenticate once before it can register a “machine”.
39
53
 
40
54
  ```bash
41
55
  happys auth login
@@ -47,17 +61,16 @@ If you want a quick diagnosis:
47
61
  happys auth status
48
62
  ```
49
63
 
50
- ### Step 4: Enable Tailscale Serve (recommended for mobile/remote)
64
+ ### Enable Tailscale Serve (recommended for mobile/remote)
51
65
 
52
66
  ```bash
53
67
  happys tailscale enable
54
68
  happys tailscale url
55
69
  ```
56
70
 
57
- ### Step 5: Mobile access
71
+ ### Mobile access
58
72
 
59
- Make sure Tailscale is [installed and running]
60
- ([https://tailscale.com/kb/1347/installation](https://tailscale.com/kb/1347/installation)) on your
73
+ Make sure Tailscale is [installed and running](https://tailscale.com/kb/1347/installation) on your
61
74
  phone, then either:
62
75
 
63
76
  - Open the URL from `happys tailscale url` on your phone and “Add to Home Screen”, or
@@ -69,36 +82,21 @@ Details (secure context, phone instructions, automation knobs): `[docs/remote-ac
69
82
 
70
83
  ## Development (worktrees, stacks, contributor workflows)
71
84
 
85
+ If you want to **develop Happy** (worktrees, multiple stacks, upstream PR workflows), you can install Happy Stacks for development with:
86
+
72
87
  ### Setup (guided)
73
88
 
74
89
  ```bash
75
90
  npx happy-stacks setup --profile=dev
76
91
  ```
77
92
 
78
- ### Developing from a cloned repo
93
+ During setup, you’ll be asked where to store your **workspace** (the folder that will contain `components/` and `components/.worktrees/`).
94
+ You can also set it non-interactively:
79
95
 
80
96
  ```bash
81
- git clone https://github.com/leeroybrun/happy-stacks.git
82
- cd happy-stacks
83
-
84
- node ./bin/happys.mjs setup --profile=dev
97
+ npx happy-stacks setup --profile=dev --workspace-dir=~/Development/happy
85
98
  ```
86
99
 
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
100
  ### Why this exists
103
101
 
104
102
  - **Automated setup**: `happys setup` + `happys start` gets the whole stack up and running.
@@ -150,6 +148,7 @@ More details + automation: `[docs/remote-access.md](docs/remote-access.md)`.
150
148
  - **Scripts**: `scripts/*.mjs` (bootstrap/dev/start/build/stacks/worktrees/service/tailscale/mobile)
151
149
  - **Components**: `components/*` (each is its own Git repo)
152
150
  - **Worktrees**: `components/.worktrees/<component>/<owner>/<branch...>`
151
+ - **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.
153
152
 
154
153
  Components:
155
154
 
@@ -198,6 +197,12 @@ happys stack pr pr123 \
198
197
  --dev
199
198
  ```
200
199
 
200
+ Optional: enable Expo dev-client for mobile reviewers (reuses the same Expo dev server; no second Metro process):
201
+
202
+ ```bash
203
+ happys stack pr pr123 --happy=123 --happy-cli=456 --dev --mobile
204
+ ```
205
+
201
206
  Optional: run it in a self-contained sandbox folder (delete it to uninstall completely):
202
207
 
203
208
  ```bash
@@ -226,12 +231,16 @@ npx happy-stacks setup-pr \
226
231
  --happy-cli=https://github.com/slopus/happy-cli/pull/456
227
232
  ```
228
233
 
229
- Optional: run it in a self-contained sandbox folder (delete it to uninstall completely):
234
+ Optional: enable Expo dev-client for mobile reviewers (works with both default `--dev` and `--start`):
230
235
 
231
236
  ```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"
237
+ npx happy-stacks setup-pr --happy=123 --happy-cli=456 --mobile
238
+ ```
239
+
240
+ Optional: run it in a self-contained sandbox folder (auto-cleaned):
241
+
242
+ ```bash
243
+ npx happy-stacks review-pr --happy=123 --happy-cli=456
235
244
  ```
236
245
 
237
246
  Short form (PR numbers):
@@ -316,12 +325,20 @@ Details: `[docs/menubar.md](docs/menubar.md)`.
316
325
  #### Mobile iOS dev (optional)
317
326
 
318
327
  ```bash
319
- happys mobile --help
320
- happys mobile --json
328
+ # Install the shared "Happy Stacks Dev" dev-client app on your iPhone:
329
+ happys mobile-dev-client --install
330
+
331
+ # Install an isolated per-stack app (Release config, unique bundle id + scheme):
332
+ happys stack mobile:install <stack> --name="Happy (<stack>)"
321
333
  ```
322
334
 
323
335
  Details: `[docs/mobile-ios.md](docs/mobile-ios.md)`.
324
336
 
337
+ #### Reviewing PRs in an isolated sandbox
338
+
339
+ - **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.
340
+ - **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.
341
+
325
342
  #### Tauri desktop app (optional)
326
343
 
327
344
  ```bash
@@ -338,7 +355,7 @@ Details: `[docs/tauri.md](docs/tauri.md)`.
338
355
  - (advanced) `happys bootstrap --interactive` (component installer wizard)
339
356
  - **Run**:
340
357
  - `happys start` (production-like; serves built UI via server-light)
341
- - `happys dev` (dev; Expo web dev server for UI)
358
+ - `happys dev` (dev; Expo dev server for UI, optional dev-client via `--mobile`)
342
359
  - **Server flavor**:
343
360
  - `happys srv status`
344
361
  - `happys srv use --interactive`
@@ -352,7 +369,10 @@ Details: `[docs/tauri.md](docs/tauri.md)`.
352
369
  - `happys stack dev <name>` / `happys stack start <name>`
353
370
  - `happys stack edit <name> --interactive`
354
371
  - `happys stack wt <name> -- use --interactive`
372
+ - `happys stack review <name> [component...] [--reviewers=coderabbit,codex] [--base-ref=<ref>]`
355
373
  - `happys stack migrate`
374
+ - **Reviews (local diff review)**:
375
+ - `happys review [component...] [--reviewers=coderabbit,codex] [--base-remote=<remote>] [--base-branch=<branch>] [--base-ref=<ref>]`
356
376
  - **Menu bar (SwiftBar)**:
357
377
  - `happys menubar install`
358
378
 
@@ -379,6 +399,15 @@ Notes:
379
399
 
380
400
  - Canonical env prefix is `HAPPY_STACKS_*` (legacy `HAPPY_LOCAL_*` still works).
381
401
  - Canonical stack storage is `~/.happy/stacks` (legacy `~/.happy/local` is still supported).
402
+ - To edit per-stack environment variables (including provider keys like `OPENAI_API_KEY`), use:
403
+
404
+ ```bash
405
+ happys stack env <stack> set KEY=VALUE
406
+ happys stack env <stack> unset KEY
407
+ happys stack env <stack> get KEY
408
+ happys stack env <stack> list
409
+ ```
410
+
382
411
  - **Repo env templates**:
383
412
  - **Use `.env.example` as the canonical template** (copy it to `.env` if you’re running this repo directly).
384
413
  - 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.
@@ -405,3 +434,27 @@ Notes:
405
434
  - To explicitly allow those for testing, set `HAPPY_STACKS_SANDBOX_ALLOW_GLOBAL=1` (still recommended to clean up after).
406
435
 
407
436
  For contributor/LLM workflow expectations: `[AGENTS.md](AGENTS.md)`.
437
+
438
+ ### Developing Happy Stacks itself
439
+
440
+ ```bash
441
+ git clone https://github.com/leeroybrun/happy-stacks.git
442
+ cd happy-stacks
443
+
444
+ node ./bin/happys.mjs setup --profile=dev
445
+ ```
446
+
447
+ Notes:
448
+
449
+ - In a cloned repo, `pnpm <script>` still works, but `happys <command>` is the recommended UX (same underlying scripts).
450
+ - To make the installed `~/.happy-stacks/bin/happys` shim (LaunchAgents / SwiftBar) run your local checkout without publishing to npm, set:
451
+
452
+ ```bash
453
+ echo 'HAPPY_STACKS_CLI_ROOT_DIR=/path/to/your/happy-stacks-checkout' >> ~/.happy-stacks/.env
454
+ ```
455
+
456
+ Or (recommended) persist it via init:
457
+
458
+ ```bash
459
+ happys init --cli-root-dir=/path/to/your/happy-stacks-checkout
460
+ ```
package/bin/happys.mjs CHANGED
@@ -9,6 +9,7 @@ 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
11
  import { expandHome, getCanonicalHomeEnvPathFromEnv } from '../scripts/utils/paths/canonical_home.mjs';
12
+ import { resolveStackEnvPath } from '../scripts/utils/paths/paths.mjs';
12
13
 
13
14
  function getCliRootDir() {
14
15
  return dirname(dirname(fileURLToPath(import.meta.url)));
@@ -110,6 +111,42 @@ function stripGlobalOpt(argv, { name, aliases = [] }) {
110
111
  return { value: '', argv };
111
112
  }
112
113
 
114
+ function applyVerbosityIfRequested(argv) {
115
+ // Global verbosity:
116
+ // - supports -v/-vv/-vvv anywhere before/after the command
117
+ // - supports --verbose and --verbose=N
118
+ //
119
+ // We set HAPPY_STACKS_VERBOSE (0-3) and strip these args so downstream scripts don't need to support them.
120
+ let level = Number.isFinite(Number(process.env.HAPPY_STACKS_VERBOSE)) ? Number(process.env.HAPPY_STACKS_VERBOSE) : null;
121
+ let next = [];
122
+ for (const a of argv) {
123
+ if (a === '-v' || a === '-vv' || a === '-vvv') {
124
+ const n = a.length - 1;
125
+ level = Math.max(level ?? 0, n);
126
+ continue;
127
+ }
128
+ if (a === '--verbose') {
129
+ level = Math.max(level ?? 0, 1);
130
+ continue;
131
+ }
132
+ if (a.startsWith('--verbose=')) {
133
+ const raw = a.slice('--verbose='.length).trim();
134
+ const n = Number(raw);
135
+ if (Number.isFinite(n)) {
136
+ level = Math.max(level ?? 0, Math.max(0, Math.min(3, Math.floor(n))));
137
+ } else {
138
+ level = Math.max(level ?? 0, 1);
139
+ }
140
+ continue;
141
+ }
142
+ next.push(a);
143
+ }
144
+ if (level != null) {
145
+ process.env.HAPPY_STACKS_VERBOSE = String(Math.max(0, Math.min(3, Math.floor(level))));
146
+ }
147
+ return next;
148
+ }
149
+
113
150
  function applySandboxDirIfRequested(argv) {
114
151
  const explicit = (process.env.HAPPY_STACKS_SANDBOX_DIR ?? '').trim();
115
152
  const { value, argv: nextArgv } = stripGlobalOpt(argv, { name: '--sandbox-dir', aliases: ['--sandbox'] });
@@ -117,6 +154,8 @@ function applySandboxDirIfRequested(argv) {
117
154
  if (!raw) return { argv: nextArgv, enabled: false };
118
155
 
119
156
  const sandboxDir = expandHome(raw);
157
+ const allowGlobalRaw = (process.env.HAPPY_STACKS_SANDBOX_ALLOW_GLOBAL ?? '').trim().toLowerCase();
158
+ const allowGlobal = allowGlobalRaw === '1' || allowGlobalRaw === 'true' || allowGlobalRaw === 'yes' || allowGlobalRaw === 'y';
120
159
  // Keep all state under one folder that can be deleted to reset completely.
121
160
  const canonicalHomeDir = join(sandboxDir, 'canonical');
122
161
  const homeDir = join(sandboxDir, 'home');
@@ -124,23 +163,76 @@ function applySandboxDirIfRequested(argv) {
124
163
  const runtimeDir = join(sandboxDir, 'runtime');
125
164
  const storageDir = join(sandboxDir, 'storage');
126
165
 
166
+ // Sandbox isolation MUST win over any pre-exported Happy Stacks env vars.
167
+ // Otherwise sandbox runs can accidentally read/write "real" machine state.
168
+ //
169
+ // Keep only a tiny set of sandbox-safe globals; everything else should be driven by flags
170
+ // and stack env files inside the sandbox.
171
+ const preserved = new Map();
172
+ const keepKeys = [
173
+ 'HAPPY_STACKS_VERBOSE',
174
+ 'HAPPY_STACKS_INVOKED_CWD',
175
+ 'HAPPY_STACKS_SANDBOX_DIR',
176
+ 'HAPPY_STACKS_SANDBOX_ALLOW_GLOBAL',
177
+ 'HAPPY_STACKS_UPDATE_CHECK',
178
+ 'HAPPY_STACKS_UPDATE_CHECK_INTERVAL_MS',
179
+ 'HAPPY_STACKS_UPDATE_NOTIFY_INTERVAL_MS',
180
+ ];
181
+ for (const k of keepKeys) {
182
+ if (process.env[k] != null && String(process.env[k]).trim() !== '') {
183
+ preserved.set(k, process.env[k]);
184
+ }
185
+ }
186
+ for (const k of Object.keys(process.env)) {
187
+ if (k.startsWith('HAPPY_STACKS_') || k.startsWith('HAPPY_LOCAL_')) {
188
+ delete process.env[k];
189
+ continue;
190
+ }
191
+ // Also clear unprefixed Happy vars; sandbox commands should compute these from stack state.
192
+ if (k === 'HAPPY_HOME_DIR' || k === 'HAPPY_SERVER_URL' || k === 'HAPPY_WEBAPP_URL') {
193
+ delete process.env[k];
194
+ }
195
+ }
196
+ for (const [k, v] of preserved.entries()) {
197
+ process.env[k] = v;
198
+ }
199
+
127
200
  process.env.HAPPY_STACKS_SANDBOX_DIR = sandboxDir;
128
201
  process.env.HAPPY_STACKS_CLI_ROOT_DISABLE = '1'; // never re-exec into a user's "real" install when sandboxing
129
202
 
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;
203
+ // In sandbox mode, we MUST force all state directories into the sandbox, even if the user
204
+ // exported HAPPY_STACKS_* in their shell. Otherwise sandbox runs can accidentally read/write
205
+ // "real" machine state (breaking isolation).
206
+ process.env.HAPPY_STACKS_CANONICAL_HOME_DIR = canonicalHomeDir;
207
+ process.env.HAPPY_LOCAL_CANONICAL_HOME_DIR = canonicalHomeDir;
208
+
209
+ process.env.HAPPY_STACKS_HOME_DIR = homeDir;
210
+ process.env.HAPPY_LOCAL_HOME_DIR = homeDir;
211
+
212
+ process.env.HAPPY_STACKS_WORKSPACE_DIR = workspaceDir;
213
+ process.env.HAPPY_LOCAL_WORKSPACE_DIR = workspaceDir;
214
+
215
+ process.env.HAPPY_STACKS_RUNTIME_DIR = runtimeDir;
216
+ process.env.HAPPY_LOCAL_RUNTIME_DIR = runtimeDir;
217
+
218
+ process.env.HAPPY_STACKS_STORAGE_DIR = storageDir;
219
+ process.env.HAPPY_LOCAL_STORAGE_DIR = storageDir;
220
+
221
+ // Sandbox default: disallow global side effects unless explicitly opted in.
222
+ // This keeps sandbox runs fast, deterministic, and isolated.
223
+ if (!allowGlobal) {
224
+ // Network-y UX (background update checks) are not useful in a temporary sandbox.
225
+ process.env.HAPPY_STACKS_UPDATE_CHECK = '0';
226
+ process.env.HAPPY_STACKS_UPDATE_CHECK_INTERVAL_MS = '0';
227
+ process.env.HAPPY_STACKS_UPDATE_NOTIFY_INTERVAL_MS = '0';
228
+
229
+ // Never auto-enable or reset Tailscale Serve in sandbox.
230
+ // (Tailscale is global machine state; sandbox runs must not touch it.)
231
+ process.env.HAPPY_LOCAL_TAILSCALE_SERVE = '0';
232
+ process.env.HAPPY_STACKS_TAILSCALE_SERVE = '0';
233
+ process.env.HAPPY_LOCAL_TAILSCALE_RESET_ON_EXIT = '0';
234
+ process.env.HAPPY_STACKS_TAILSCALE_RESET_ON_EXIT = '0';
235
+ }
144
236
 
145
237
  return { argv: nextArgv, enabled: true };
146
238
  }
@@ -248,8 +340,16 @@ function runNodeScript(cliRootDir, scriptRelPath, args) {
248
340
  function main() {
249
341
  const cliRootDir = getCliRootDir();
250
342
  const initialArgv = process.argv.slice(2);
251
- const { argv, enabled: sandboxed } = applySandboxDirIfRequested(initialArgv);
343
+ const argv0 = applyVerbosityIfRequested(initialArgv);
344
+ const { argv, enabled: sandboxed } = applySandboxDirIfRequested(argv0);
252
345
  void sandboxed;
346
+
347
+ // Preserve the original working directory across re-exec to the CLI root so commands can infer
348
+ // component/worktree context even when the actual scripts run with cwd=cliRootDir.
349
+ if (!(process.env.HAPPY_STACKS_INVOKED_CWD ?? '').trim()) {
350
+ process.env.HAPPY_STACKS_INVOKED_CWD = process.cwd();
351
+ }
352
+
253
353
  maybeReexecToCliRoot(cliRootDir);
254
354
 
255
355
  // If the user passed only flags (common via `npx happy-stacks --help`),
@@ -278,8 +378,50 @@ function main() {
278
378
  return runNodeScript(cliRootDir, targetCmd.scriptRelPath, helpArgs);
279
379
  }
280
380
 
281
- const resolved = resolveHappysCommand(cmd);
381
+ let resolved = resolveHappysCommand(cmd);
282
382
  if (!resolved) {
383
+ // Stack shorthand:
384
+ // If the first token is not a known command, but it *is* an existing stack name,
385
+ // treat `happys <stack> <command> ...` as `happys stack <command> <stack> ...`.
386
+ const stackName = cmd;
387
+ const { envPath } = resolveStackEnvPath(stackName, process.env);
388
+ const stackExists = existsSync(envPath);
389
+ if (stackExists) {
390
+ const cmdIdx = rest.findIndex((a) => !a.startsWith('-'));
391
+ if (cmdIdx < 0) {
392
+ if (rest.includes('--help') || rest.includes('-h')) {
393
+ const stackCmd = resolveHappysCommand('stack');
394
+ if (!stackCmd || stackCmd.kind !== 'node') {
395
+ console.error('[happys] internal error: missing stack command');
396
+ process.exit(1);
397
+ }
398
+ return runNodeScript(cliRootDir, stackCmd.scriptRelPath, ['--help']);
399
+ }
400
+ console.error(`[happys] missing command after stack name: ${stackName}`);
401
+ console.error('');
402
+ console.error('Try one of:');
403
+ console.error(` happys ${stackName} env list`);
404
+ console.error(` happys ${stackName} dev`);
405
+ console.error(` happys ${stackName} start`);
406
+ console.error('');
407
+ console.error('Equivalent long form:');
408
+ console.error(` happys stack <command> ${stackName} ...`);
409
+ process.exit(1);
410
+ }
411
+
412
+ const stackSubcmd = rest[cmdIdx];
413
+ const preFlags = rest.slice(0, cmdIdx);
414
+ const post = rest.slice(cmdIdx + 1);
415
+ const stackArgs = [stackSubcmd, stackName, ...preFlags, ...post];
416
+
417
+ resolved = resolveHappysCommand('stack');
418
+ if (!resolved || resolved.kind !== 'node') {
419
+ console.error('[happys] internal error: missing stack command');
420
+ process.exit(1);
421
+ }
422
+ return runNodeScript(cliRootDir, resolved.scriptRelPath, stackArgs);
423
+ }
424
+
283
425
  console.error(`[happys] unknown command: ${cmd}`);
284
426
  console.error('');
285
427
  console.error(usage());
@@ -0,0 +1,130 @@
1
+ # Codex MCP resume (experimental) — Happy integration spec
2
+
3
+ This document describes how to integrate an **experimental** Codex MCP server fork
4
+ that can **resume sessions from rollout JSONL** after restarts/crashes.
5
+
6
+ ## Goals
7
+
8
+ - Allow Happy to use a Codex MCP server that supports **resume-from-rollout**.
9
+ - Keep the feature **opt-in** and **safe by default** (pinned versions, clear UI).
10
+ - Avoid requiring users to replace their global `codex` install.
11
+
12
+ ## Non-goals
13
+
14
+ - Replacing OpenAI’s `@openai/codex` installation for general CLI usage.
15
+ - Auto-updating to “latest” without user opt-in.
16
+
17
+ ## Key facts (implementation reality)
18
+
19
+ - `codex-mcp-server` is **not** a thin wrapper around a user-installed `codex` binary.
20
+ It embeds Codex core and runs threads internally.
21
+ - Therefore, when Happy uses this MCP server, the “engine” for those sessions is
22
+ the forked Codex Rust core shipped with the MCP server build.
23
+
24
+ ## Distribution model (recommended)
25
+
26
+ Ship the MCP server as an npm package that bundles native binaries:
27
+
28
+ - **npm package**: `@leeroy/codex-mcp-resume`
29
+ - **binary launcher**: `npx -y @leeroy/codex-mcp-resume@<pinned-version>`
30
+ - **native payload**: `vendor/<targetTriple>/codex-mcp-server/codex-mcp-server[.exe]`
31
+
32
+ This mirrors how upstream Codex ships platform binaries via npm.
33
+
34
+ ## Versioning + updates
35
+
36
+ ### Pinned by Happy (default)
37
+
38
+ - Happy pins an exact semver (example): `0.84.0-resume.123.a1`
39
+ - Happy invokes:
40
+ - `npx -y @leeroy/codex-mcp-resume@0.84.0-resume.123.a1`
41
+
42
+ **Update path**: users get new MCP builds when they update Happy (or when Happy updates the pin).
43
+
44
+ ### Optional “auto-update experimental tools” (opt-in)
45
+
46
+ If you want faster iteration:
47
+
48
+ - Happy uses `@latest` or a dedicated dist-tag (e.g. `resume`)
49
+ - This should be opt-in, clearly labeled “may break”.
50
+
51
+ ## Happy-side feature flag / settings
52
+
53
+ ### Setting name (proposed)
54
+
55
+ - **Config**: `experimental.codexMcpResume = true|false`
56
+ - **Env override** (optional): `HAPPY_EXPERIMENTAL_CODEX_MCP_RESUME=1`
57
+
58
+ ### UX (proposed)
59
+
60
+ - Settings → Experimental → “Use Codex MCP resume fork”
61
+ - Subtext: “Runs a separate MCP server shipped by Happy; may differ from your global Codex install.”
62
+
63
+ ## Process + wiring
64
+
65
+ ### 1) Resolve the MCP server command
66
+
67
+ When feature enabled, Happy should register an MCP server entry equivalent to:
68
+
69
+ ```toml
70
+ [mcp_servers.codex_resume]
71
+ command = "npx"
72
+ args = ["-y", "@leeroy/codex-mcp-resume@0.84.0-resume.123.a1"]
73
+ ```
74
+
75
+ Notes:
76
+ - The MCP server is stdio-based; Happy should spawn it and speak MCP JSON-RPC over stdin/stdout.
77
+ - Use `-y` to make it non-interactive.
78
+ - Ensure Happy’s environment does **not** leak secrets in logs.
79
+
80
+ ### 2) Decide the Codex “home” used for sessions
81
+
82
+ Session restoration depends on reading rollout JSONL files.
83
+
84
+ Two viable modes:
85
+
86
+ - **Shared home (recommended for seamless resume)**:
87
+ - Run with the user’s default Codex home (usually `~/.codex`), so the MCP server sees the same rollouts.
88
+ - **Happy-managed home (more isolated)**:
89
+ - Provide a separate home (e.g. `~/.happy/codex`) but then “resume existing Codex sessions” won’t work unless you import/migrate.
90
+
91
+ If you need an explicit home, pass env vars when spawning the MCP server:
92
+
93
+ - `CODEX_HOME=<path>`
94
+
95
+ ### 3) Tool usage from Happy
96
+
97
+ Happy should call MCP tools:
98
+
99
+ - **Start new session**: `tools/call name="codex"` with desired config (model, cwd, approval_policy/sandbox, etc).
100
+ - **Continue session**: `tools/call name="codex-reply"` with `{ threadId, prompt }`.
101
+
102
+ The forked MCP server handles:
103
+
104
+ - in-memory threads normally
105
+ - on restart/crash: “resume thread” by reading rollout history
106
+
107
+ ### 4) Concurrency expectations
108
+
109
+ Happy may issue multiple `codex-reply` calls concurrently (UI retries, double-submit, etc).
110
+ The MCP server should serialize per-thread reply/resume to avoid event-stream races.
111
+
112
+ ## CI + publishing requirements (for maintainers)
113
+
114
+ The fork’s CI should:
115
+
116
+ - build `codex-mcp-server` for macOS/Linux/Windows (arm64 + x64 where relevant)
117
+ - pack those into an npm tarball
118
+ - publish via npm trusted publishing (OIDC)
119
+
120
+ ## What Happy devs need to implement (checklist)
121
+
122
+ - Add a feature flag + settings surface (opt-in).
123
+ - Add MCP server registry entry for `codex_resume` (command: `npx`, args pinned).
124
+ - Decide and document which Codex home dir is used (shared vs isolated).
125
+ - Route Codex session creation + reply calls through the selected MCP server.
126
+ - Add telemetry/logging that records:
127
+ - selected MCP server (default vs resume fork)
128
+ - package version (pinned version string)
129
+ - threadId for correlation (no prompt contents)
130
+