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.
- package/README.md +93 -40
- package/bin/happys.mjs +158 -16
- package/docs/codex-mcp-resume.md +130 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-analysis.md +17640 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-export.fuller-stat.md +3845 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-inventory.md +102 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-manual-review.md +1452 -0
- package/docs/commit-audits/happy/leeroy-wip.manual-review-queue.md +116 -0
- package/docs/happy-development.md +3 -4
- package/docs/isolated-linux-vm.md +82 -0
- package/docs/mobile-ios.md +112 -54
- package/docs/monorepo-migration.md +286 -0
- package/docs/server-flavors.md +19 -3
- package/docs/stacks.md +35 -0
- package/package.json +5 -1
- package/scripts/auth.mjs +32 -10
- package/scripts/build.mjs +55 -8
- package/scripts/daemon.mjs +166 -10
- package/scripts/dev.mjs +198 -50
- package/scripts/doctor.mjs +0 -4
- package/scripts/edison.mjs +6 -4
- package/scripts/env.mjs +150 -0
- package/scripts/env_cmd.test.mjs +128 -0
- package/scripts/init.mjs +8 -3
- package/scripts/install.mjs +207 -69
- package/scripts/lint.mjs +24 -4
- package/scripts/migrate.mjs +3 -12
- package/scripts/mobile.mjs +88 -104
- package/scripts/mobile_dev_client.mjs +83 -0
- package/scripts/monorepo.mjs +1096 -0
- package/scripts/monorepo_port.test.mjs +1470 -0
- package/scripts/provision/linux-ubuntu-review-pr.sh +51 -0
- package/scripts/review.mjs +908 -0
- package/scripts/review_pr.mjs +353 -0
- package/scripts/run.mjs +101 -21
- package/scripts/service.mjs +2 -2
- package/scripts/setup.mjs +189 -68
- package/scripts/setup_pr.mjs +586 -38
- package/scripts/stack.mjs +990 -196
- package/scripts/stack_archive_cmd.test.mjs +91 -0
- package/scripts/stack_editor_workspace_monorepo_root.test.mjs +65 -0
- package/scripts/stack_env_cmd.test.mjs +87 -0
- package/scripts/stack_happy_cmd.test.mjs +126 -0
- package/scripts/stack_interactive_monorepo_group.test.mjs +71 -0
- package/scripts/stack_monorepo_defaults.test.mjs +62 -0
- package/scripts/stack_monorepo_server_light_from_happy_spec.test.mjs +66 -0
- package/scripts/stack_server_flavors_defaults.test.mjs +55 -0
- package/scripts/stack_shorthand_cmd.test.mjs +55 -0
- package/scripts/stack_wt_list.test.mjs +128 -0
- package/scripts/tailscale.mjs +37 -1
- package/scripts/test.mjs +45 -8
- package/scripts/tui.mjs +395 -39
- package/scripts/typecheck.mjs +24 -4
- package/scripts/utils/auth/daemon_gate.mjs +55 -0
- package/scripts/utils/auth/daemon_gate.test.mjs +37 -0
- package/scripts/utils/auth/guided_pr_auth.mjs +79 -0
- package/scripts/utils/auth/guided_stack_web_login.mjs +75 -0
- package/scripts/utils/auth/interactive_stack_auth.mjs +72 -0
- package/scripts/utils/auth/login_ux.mjs +32 -13
- package/scripts/utils/auth/sources.mjs +26 -0
- package/scripts/utils/auth/stack_guided_login.mjs +353 -0
- package/scripts/utils/cli/cli_registry.mjs +43 -4
- package/scripts/utils/cli/cwd_scope.mjs +136 -0
- package/scripts/utils/cli/cwd_scope.test.mjs +110 -0
- package/scripts/utils/cli/log_forwarder.mjs +157 -0
- package/scripts/utils/cli/prereqs.mjs +75 -0
- package/scripts/utils/cli/prereqs.test.mjs +34 -0
- package/scripts/utils/cli/progress.mjs +126 -0
- package/scripts/utils/cli/verbosity.mjs +12 -0
- package/scripts/utils/cli/wizard.mjs +17 -9
- package/scripts/utils/cli/wizard_prompt_worktree_source_lazy.test.mjs +60 -0
- package/scripts/utils/dev/daemon.mjs +61 -4
- package/scripts/utils/dev/expo_dev.mjs +430 -0
- package/scripts/utils/dev/expo_dev.test.mjs +76 -0
- package/scripts/utils/dev/server.mjs +36 -42
- package/scripts/utils/dev_auth_key.mjs +169 -0
- package/scripts/utils/edison/git_roots.mjs +29 -0
- package/scripts/utils/edison/git_roots.test.mjs +36 -0
- package/scripts/utils/env/env.mjs +7 -3
- package/scripts/utils/env/env_file.mjs +4 -2
- package/scripts/utils/env/env_file.test.mjs +44 -0
- package/scripts/utils/expo/command.mjs +52 -0
- package/scripts/utils/expo/expo.mjs +20 -1
- package/scripts/utils/expo/metro_ports.mjs +114 -0
- package/scripts/utils/git/git.mjs +67 -0
- package/scripts/utils/git/worktrees.mjs +80 -25
- package/scripts/utils/git/worktrees_monorepo.test.mjs +54 -0
- package/scripts/utils/handy_master_secret.mjs +94 -0
- package/scripts/utils/mobile/config.mjs +31 -0
- package/scripts/utils/mobile/dev_client_links.mjs +60 -0
- package/scripts/utils/mobile/identifiers.mjs +47 -0
- package/scripts/utils/mobile/identifiers.test.mjs +42 -0
- package/scripts/utils/mobile/ios_xcodeproj_patch.mjs +128 -0
- package/scripts/utils/mobile/ios_xcodeproj_patch.test.mjs +98 -0
- package/scripts/utils/net/lan_ip.mjs +24 -0
- package/scripts/utils/net/ports.mjs +9 -1
- package/scripts/utils/net/tcp_forward.mjs +162 -0
- package/scripts/utils/net/url.mjs +30 -0
- package/scripts/utils/net/url.test.mjs +20 -0
- package/scripts/utils/paths/localhost_host.mjs +50 -3
- package/scripts/utils/paths/paths.mjs +159 -40
- package/scripts/utils/paths/paths_monorepo.test.mjs +58 -0
- package/scripts/utils/paths/paths_server_flavors.test.mjs +45 -0
- package/scripts/utils/proc/commands.mjs +2 -3
- package/scripts/utils/proc/parallel.mjs +25 -0
- package/scripts/utils/proc/pm.mjs +176 -22
- package/scripts/utils/proc/pm_spawn.test.mjs +76 -0
- package/scripts/utils/proc/pm_stack_cache_env.test.mjs +142 -0
- package/scripts/utils/proc/proc.mjs +136 -4
- package/scripts/utils/proc/proc.test.mjs +77 -0
- package/scripts/utils/review/base_ref.mjs +74 -0
- package/scripts/utils/review/base_ref.test.mjs +54 -0
- package/scripts/utils/review/chunks.mjs +55 -0
- package/scripts/utils/review/chunks.test.mjs +51 -0
- package/scripts/utils/review/findings.mjs +165 -0
- package/scripts/utils/review/findings.test.mjs +85 -0
- package/scripts/utils/review/head_slice.mjs +153 -0
- package/scripts/utils/review/head_slice.test.mjs +91 -0
- package/scripts/utils/review/instructions/deep.md +20 -0
- package/scripts/utils/review/runners/coderabbit.mjs +61 -0
- package/scripts/utils/review/runners/coderabbit.test.mjs +59 -0
- package/scripts/utils/review/runners/codex.mjs +61 -0
- package/scripts/utils/review/runners/codex.test.mjs +35 -0
- package/scripts/utils/review/slices.mjs +140 -0
- package/scripts/utils/review/slices.test.mjs +32 -0
- package/scripts/utils/review/targets.mjs +24 -0
- package/scripts/utils/review/targets.test.mjs +36 -0
- package/scripts/utils/sandbox/review_pr_sandbox.mjs +106 -0
- package/scripts/utils/server/flavor_scripts.mjs +98 -0
- package/scripts/utils/server/flavor_scripts.test.mjs +146 -0
- package/scripts/utils/server/mobile_api_url.mjs +61 -0
- package/scripts/utils/server/mobile_api_url.test.mjs +41 -0
- package/scripts/utils/server/prisma_import.mjs +37 -0
- package/scripts/utils/server/prisma_import.test.mjs +70 -0
- package/scripts/utils/server/ui_env.mjs +14 -0
- package/scripts/utils/server/ui_env.test.mjs +46 -0
- package/scripts/utils/server/urls.mjs +14 -4
- package/scripts/utils/server/validate.mjs +53 -16
- package/scripts/utils/server/validate.test.mjs +89 -0
- package/scripts/utils/service/autostart_darwin.mjs +42 -2
- package/scripts/utils/service/autostart_darwin.test.mjs +50 -0
- package/scripts/utils/stack/context.mjs +2 -2
- package/scripts/utils/stack/editor_workspace.mjs +6 -6
- package/scripts/utils/stack/interactive_stack_config.mjs +185 -0
- package/scripts/utils/stack/pr_stack_name.mjs +16 -0
- package/scripts/utils/stack/runtime_state.mjs +2 -1
- package/scripts/utils/stack/startup.mjs +120 -13
- package/scripts/utils/stack/startup_server_light_dirs.test.mjs +64 -0
- package/scripts/utils/stack/startup_server_light_generate.test.mjs +70 -0
- package/scripts/utils/stack/startup_server_light_legacy.test.mjs +88 -0
- package/scripts/utils/stack/stop.mjs +15 -4
- package/scripts/utils/stack_context.mjs +23 -0
- package/scripts/utils/stack_runtime_state.mjs +104 -0
- package/scripts/utils/stacks.mjs +38 -0
- package/scripts/utils/tailscale/ip.mjs +116 -0
- package/scripts/utils/ui/ansi.mjs +39 -0
- package/scripts/utils/ui/qr.mjs +17 -0
- package/scripts/utils/validate.mjs +88 -0
- package/scripts/where.mjs +2 -2
- package/scripts/worktrees.mjs +755 -179
- package/scripts/worktrees_archive_cmd.test.mjs +245 -0
- package/scripts/worktrees_cursor_monorepo_root.test.mjs +63 -0
- package/scripts/worktrees_list_specs_no_recurse.test.mjs +33 -0
- package/scripts/worktrees_monorepo_use_group.test.mjs +67 -0
- 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
|
|
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
|
-
###
|
|
19
|
-
|
|
20
|
-
Recommended:
|
|
18
|
+
### Quickstart
|
|
21
19
|
|
|
22
20
|
```bash
|
|
23
21
|
npx happy-stacks setup --profile=selfhost
|
|
24
22
|
```
|
|
25
23
|
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
50
|
+
### Authentication
|
|
37
51
|
|
|
38
|
-
On a **fresh 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
|
-
###
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
234
|
+
Optional: enable Expo dev-client for mobile reviewers (works with both default `--dev` and `--start`):
|
|
230
235
|
|
|
231
236
|
```bash
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
320
|
-
happys mobile --
|
|
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
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
process.env.
|
|
134
|
-
process.env.
|
|
135
|
-
|
|
136
|
-
process.env.
|
|
137
|
-
process.env.
|
|
138
|
-
|
|
139
|
-
process.env.
|
|
140
|
-
process.env.
|
|
141
|
-
|
|
142
|
-
process.env.
|
|
143
|
-
process.env.
|
|
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
|
|
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
|
-
|
|
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
|
+
|