happy-stacks 0.3.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 (94) hide show
  1. package/README.md +29 -7
  2. package/bin/happys.mjs +114 -15
  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 +11 -7
  8. package/scripts/build.mjs +54 -7
  9. package/scripts/daemon.mjs +166 -10
  10. package/scripts/dev.mjs +181 -46
  11. package/scripts/edison.mjs +4 -2
  12. package/scripts/init.mjs +3 -1
  13. package/scripts/install.mjs +112 -16
  14. package/scripts/lint.mjs +24 -4
  15. package/scripts/mobile.mjs +88 -104
  16. package/scripts/mobile_dev_client.mjs +83 -0
  17. package/scripts/provision/linux-ubuntu-review-pr.sh +51 -0
  18. package/scripts/review.mjs +217 -0
  19. package/scripts/review_pr.mjs +368 -0
  20. package/scripts/run.mjs +83 -9
  21. package/scripts/service.mjs +2 -2
  22. package/scripts/setup.mjs +42 -43
  23. package/scripts/setup_pr.mjs +591 -34
  24. package/scripts/stack.mjs +503 -45
  25. package/scripts/tailscale.mjs +37 -1
  26. package/scripts/test.mjs +45 -8
  27. package/scripts/tui.mjs +309 -39
  28. package/scripts/typecheck.mjs +24 -4
  29. package/scripts/utils/auth/daemon_gate.mjs +55 -0
  30. package/scripts/utils/auth/daemon_gate.test.mjs +37 -0
  31. package/scripts/utils/auth/guided_pr_auth.mjs +79 -0
  32. package/scripts/utils/auth/guided_stack_web_login.mjs +75 -0
  33. package/scripts/utils/auth/interactive_stack_auth.mjs +72 -0
  34. package/scripts/utils/auth/login_ux.mjs +32 -13
  35. package/scripts/utils/auth/sources.mjs +26 -0
  36. package/scripts/utils/auth/stack_guided_login.mjs +353 -0
  37. package/scripts/utils/cli/cli_registry.mjs +24 -0
  38. package/scripts/utils/cli/cwd_scope.mjs +82 -0
  39. package/scripts/utils/cli/cwd_scope.test.mjs +77 -0
  40. package/scripts/utils/cli/log_forwarder.mjs +157 -0
  41. package/scripts/utils/cli/prereqs.mjs +72 -0
  42. package/scripts/utils/cli/progress.mjs +126 -0
  43. package/scripts/utils/cli/verbosity.mjs +12 -0
  44. package/scripts/utils/dev/daemon.mjs +47 -3
  45. package/scripts/utils/dev/expo_dev.mjs +246 -0
  46. package/scripts/utils/dev/expo_dev.test.mjs +76 -0
  47. package/scripts/utils/dev/server.mjs +15 -25
  48. package/scripts/utils/dev_auth_key.mjs +169 -0
  49. package/scripts/utils/expo/command.mjs +52 -0
  50. package/scripts/utils/expo/expo.mjs +20 -1
  51. package/scripts/utils/expo/metro_ports.mjs +114 -0
  52. package/scripts/utils/git/git.mjs +67 -0
  53. package/scripts/utils/git/worktrees.mjs +24 -20
  54. package/scripts/utils/handy_master_secret.mjs +94 -0
  55. package/scripts/utils/mobile/config.mjs +31 -0
  56. package/scripts/utils/mobile/dev_client_links.mjs +60 -0
  57. package/scripts/utils/mobile/identifiers.mjs +47 -0
  58. package/scripts/utils/mobile/identifiers.test.mjs +42 -0
  59. package/scripts/utils/mobile/ios_xcodeproj_patch.mjs +128 -0
  60. package/scripts/utils/mobile/ios_xcodeproj_patch.test.mjs +98 -0
  61. package/scripts/utils/net/lan_ip.mjs +24 -0
  62. package/scripts/utils/net/ports.mjs +9 -1
  63. package/scripts/utils/net/url.mjs +30 -0
  64. package/scripts/utils/net/url.test.mjs +20 -0
  65. package/scripts/utils/paths/localhost_host.mjs +50 -3
  66. package/scripts/utils/paths/paths.mjs +42 -38
  67. package/scripts/utils/proc/parallel.mjs +25 -0
  68. package/scripts/utils/proc/pm.mjs +69 -12
  69. package/scripts/utils/proc/proc.mjs +76 -2
  70. package/scripts/utils/review/base_ref.mjs +74 -0
  71. package/scripts/utils/review/base_ref.test.mjs +54 -0
  72. package/scripts/utils/review/runners/coderabbit.mjs +19 -0
  73. package/scripts/utils/review/runners/codex.mjs +51 -0
  74. package/scripts/utils/review/targets.mjs +24 -0
  75. package/scripts/utils/review/targets.test.mjs +36 -0
  76. package/scripts/utils/sandbox/review_pr_sandbox.mjs +106 -0
  77. package/scripts/utils/server/mobile_api_url.mjs +61 -0
  78. package/scripts/utils/server/mobile_api_url.test.mjs +41 -0
  79. package/scripts/utils/server/urls.mjs +14 -4
  80. package/scripts/utils/service/autostart_darwin.mjs +42 -2
  81. package/scripts/utils/service/autostart_darwin.test.mjs +50 -0
  82. package/scripts/utils/stack/context.mjs +2 -2
  83. package/scripts/utils/stack/editor_workspace.mjs +2 -2
  84. package/scripts/utils/stack/pr_stack_name.mjs +16 -0
  85. package/scripts/utils/stack/runtime_state.mjs +2 -1
  86. package/scripts/utils/stack/startup.mjs +7 -0
  87. package/scripts/utils/stack/stop.mjs +15 -4
  88. package/scripts/utils/stack_context.mjs +23 -0
  89. package/scripts/utils/stack_runtime_state.mjs +104 -0
  90. package/scripts/utils/stacks.mjs +38 -0
  91. package/scripts/utils/ui/qr.mjs +17 -0
  92. package/scripts/utils/validate.mjs +88 -0
  93. package/scripts/worktrees.mjs +141 -55
  94. package/scripts/utils/dev/expo_web.mjs +0 -112
package/README.md CHANGED
@@ -150,6 +150,7 @@ More details + automation: `[docs/remote-access.md](docs/remote-access.md)`.
150
150
  - **Scripts**: `scripts/*.mjs` (bootstrap/dev/start/build/stacks/worktrees/service/tailscale/mobile)
151
151
  - **Components**: `components/*` (each is its own Git repo)
152
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.
153
154
 
154
155
  Components:
155
156
 
@@ -198,6 +199,12 @@ happys stack pr pr123 \
198
199
  --dev
199
200
  ```
200
201
 
202
+ Optional: enable Expo dev-client for mobile reviewers (reuses the same Expo dev server; no second Metro process):
203
+
204
+ ```bash
205
+ happys stack pr pr123 --happy=123 --happy-cli=456 --dev --mobile
206
+ ```
207
+
201
208
  Optional: run it in a self-contained sandbox folder (delete it to uninstall completely):
202
209
 
203
210
  ```bash
@@ -226,12 +233,16 @@ npx happy-stacks setup-pr \
226
233
  --happy-cli=https://github.com/slopus/happy-cli/pull/456
227
234
  ```
228
235
 
229
- Optional: run it in a self-contained sandbox folder (delete it to uninstall completely):
236
+ Optional: enable Expo dev-client for mobile reviewers (works with both default `--dev` and `--start`):
230
237
 
231
238
  ```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"
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
235
246
  ```
236
247
 
237
248
  Short form (PR numbers):
@@ -316,12 +327,20 @@ Details: `[docs/menubar.md](docs/menubar.md)`.
316
327
  #### Mobile iOS dev (optional)
317
328
 
318
329
  ```bash
319
- happys mobile --help
320
- 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>)"
321
335
  ```
322
336
 
323
337
  Details: `[docs/mobile-ios.md](docs/mobile-ios.md)`.
324
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
+
325
344
  #### Tauri desktop app (optional)
326
345
 
327
346
  ```bash
@@ -338,7 +357,7 @@ Details: `[docs/tauri.md](docs/tauri.md)`.
338
357
  - (advanced) `happys bootstrap --interactive` (component installer wizard)
339
358
  - **Run**:
340
359
  - `happys start` (production-like; serves built UI via server-light)
341
- - `happys dev` (dev; Expo web dev server for UI)
360
+ - `happys dev` (dev; Expo dev server for UI, optional dev-client via `--mobile`)
342
361
  - **Server flavor**:
343
362
  - `happys srv status`
344
363
  - `happys srv use --interactive`
@@ -352,7 +371,10 @@ Details: `[docs/tauri.md](docs/tauri.md)`.
352
371
  - `happys stack dev <name>` / `happys stack start <name>`
353
372
  - `happys stack edit <name> --interactive`
354
373
  - `happys stack wt <name> -- use --interactive`
374
+ - `happys stack review <name> [component...] [--reviewers=coderabbit,codex] [--base-ref=<ref>]`
355
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>]`
356
378
  - **Menu bar (SwiftBar)**:
357
379
  - `happys menubar install`
358
380
 
package/bin/happys.mjs CHANGED
@@ -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
+
@@ -8,101 +8,166 @@ see the “Using Happy from your phone” section in the main README.
8
8
  - Xcode installed
9
9
  - CocoaPods installed (`brew install cocoapods`)
10
10
 
11
- ## Step 1: Generate iOS native project + Pods (run when needed)
11
+ ## Two supported modes
12
12
 
13
- Run this after pulling changes that affect native deps/config, or if `ios/` was deleted:
13
+ - **Shared dev-client app** (recommended for development):
14
+ - Install *one* “Happy Stacks Dev” app on your phone.
15
+ - Run any stack with `--mobile`; scan the QR to open that stack inside the dev-client.
16
+ - Per-stack auth/storage is isolated via `EXPO_PUBLIC_HAPPY_STORAGE_SCOPE` (set automatically in stack mode).
14
17
 
15
- ```bash
16
- happys mobile:prebuild
17
- ```
18
+ - **Per-stack “release” app** (recommended for demos / strict isolation):
19
+ - Install a separate iOS app per stack (unique bundle id + scheme).
20
+ - Each stack app is isolated by iOS app container (no token collisions).
21
+
22
+ ## Shared dev-client app (install once)
18
23
 
19
- ## Step 2: Install the iOS dev build
24
+ Install the dedicated Happy Stacks dev-client app on your iPhone (USB).
20
25
 
21
- - **iOS Simulator**:
26
+ This command **runs a prebuild** (generates `ios/` + runs CocoaPods) and then installs a Debug build
27
+ without starting Metro:
22
28
 
23
29
  ```bash
24
- happys mobile --run-ios --device="iPhone 16 Pro"
30
+ happys mobile-dev-client --install
25
31
  ```
26
32
 
27
- - **Real iPhone** (requires code signing in Xcode once):
33
+ If you want to ensure the dev-client is built from a specific stack’s active `happy` worktree
34
+ (e.g. to include upstream changes that aren’t merged into your default checkout yet), run:
28
35
 
29
36
  ```bash
30
- happys mobile --run-ios --device="Your iPhone"
37
+ happys stack mobile-dev-client <stack> --install
31
38
  ```
32
39
 
33
- Tip: you can omit `--device` to auto-pick the first connected iPhone over USB:
40
+ Optional:
34
41
 
35
42
  ```bash
36
- happys mobile --run-ios
43
+ happys mobile-dev-client --install --device="Your iPhone"
44
+ happys mobile-dev-client --install --clean
37
45
  ```
38
46
 
39
- To see the exact device names/IDs you can pass:
47
+ Then run any stack with mobile enabled:
40
48
 
41
49
  ```bash
42
- happys mobile:devices
50
+ happys stack dev <stack> --mobile
51
+ # or:
52
+ happys dev --mobile
43
53
  ```
44
54
 
45
- If you hit a bundle identifier error (e.g. `com.slopus.happy.dev` “not available”), set a unique local bundle id:
55
+ Notes:
56
+
57
+ - **LAN requirement**: for physical iPhones, Metro must be reachable over LAN.
58
+ - Happy Stacks defaults to `lan` for mobile, and will print a QR code + deep link.
59
+ - For simulators you can usually use `localhost` (see `HAPPY_STACKS_MOBILE_HOST` below).
60
+ - **If Expo is already running in web-only mode**: re-run with `--restart` and include `--mobile`.
61
+
62
+ ## Per-stack app install (isolated)
63
+
64
+ Install an isolated app for a specific stack (unique bundle id + scheme, Release config, no Metro):
46
65
 
47
66
  ```bash
48
- HAPPY_STACKS_IOS_BUNDLE_ID="com.yourname.happy.local.dev" happys mobile --run-ios
49
- # legacy: HAPPY_LOCAL_IOS_BUNDLE_ID="com.yourname.happy.local.dev" happys mobile --run-ios
67
+ happys stack mobile:install <stack> --name="Happy (<stack>)"
68
+ happys stack mobile:install <stack> --name="Happy PR 272" --device="Your iPhone"
50
69
  ```
51
70
 
52
- ## Release build (runs without Metro)
71
+ The chosen app name is persisted in the stack env so you can re-run installs without re-typing it.
72
+
73
+ ## Native iOS regeneration / “prebuild” (critical)
74
+
75
+ You’ll need to regenerate the iOS native project + Pods when:
76
+
77
+ - you pull changes that affect native deps / Expo config
78
+ - `components/happy/ios/` was deleted
79
+ - you hit CocoaPods / deployment-target mismatches after a dependency bump
53
80
 
54
- Build + install a Release configuration (no Metro required at runtime):
81
+ Run:
55
82
 
56
83
  ```bash
57
- happys mobile:install
84
+ happys mobile --prebuild
85
+ # (optional) fully regenerate ios/:
86
+ happys mobile --prebuild --clean
58
87
  ```
59
88
 
60
- ## Step 3: Start Metro (dev client)
89
+ What this does today:
61
90
 
62
- - **iOS Simulator**:
91
+ - runs `expo prebuild --no-install` (so we can patch before CocoaPods runs)
92
+ - patches `ios/Podfile.properties.json` to:
93
+ - set `ios.deploymentTarget` to `16.0`
94
+ - set `ios.buildReactNativeFromSource` to `true`
95
+ - patches the generated Xcode project deployment target (where applicable)
96
+ - runs `pod install`
97
+
98
+ Notes:
99
+
100
+ - **You usually don’t need to run this manually** because both:
101
+ - `happys mobile-dev-client --install`
102
+ - `happys stack mobile:install <stack>`
103
+ already include `--prebuild`.
104
+ - Legacy alias: `happys mobile:prebuild` exists (hidden), but prefer `happys mobile --prebuild`.
105
+
106
+ ## Manual `happys mobile` usage (advanced)
107
+
108
+ If you want to work on the embedded Expo app directly (outside `happys dev --mobile`), `happys mobile` supports:
63
109
 
64
110
  ```bash
65
- happys mobile --host=localhost
111
+ # Start Metro (keeps running):
112
+ happys mobile --host=lan
113
+
114
+ # Build + install on iOS (and exit). If you omit --device, it will try to auto-pick a connected iPhone over USB:
115
+ happys mobile --prebuild --run-ios --device="Your iPhone"
116
+ happys mobile --prebuild --run-ios --configuration=Release --no-metro
66
117
  ```
67
118
 
68
- - **Real iPhone** (same Wi‑Fi as your Mac):
119
+ ## Notes / troubleshooting
120
+
121
+ - **QR opens the wrong app**:
122
+ - The dev-client QR uses the `HAPPY_STACKS_DEV_CLIENT_SCHEME` (default: `happystacks-dev`).
123
+ - Per-stack installs use a different per-stack scheme, so they should not intercept dev-client QR scans.
124
+
125
+ - **List connected devices** (for `--device=`):
69
126
 
70
127
  ```bash
71
- happys mobile --host=lan
128
+ happys mobile:devices
72
129
  ```
73
130
 
74
- Open the dev build and tap Reload. Scanning the QR should open the dev build (not the App Store app).
131
+ - **Code signing weirdness on a real iPhone**:
132
+ - Happy Stacks will try to “un-pin” signing fields in the generated `.pbxproj` so Expo/Xcode can reconfigure signing
133
+ (this avoids failures where automatic signing is disabled because `DEVELOPMENT_TEAM`/profiles were pinned).
134
+ - If you want to manage signing manually, pass `--no-signing-fix` to `happys mobile ...` / `happys stack mobile <stack> ...`.
75
135
 
76
136
  ## Bake the default server URL into the app (optional)
77
137
 
78
138
  If you want the built app to default to your happy-stacks server URL, set this **when building**:
79
139
 
80
140
  ```bash
81
- HAPPY_STACKS_SERVER_URL="https://<your-machine>.<tailnet>.ts.net" happys mobile:install
141
+ HAPPY_STACKS_SERVER_URL="https://<your-machine>.<tailnet>.ts.net" happys mobile-dev-client --install
82
142
  ```
83
143
 
84
- Note: changing `HAPPY_STACKS_SERVER_URL` requires rebuilding/reinstalling the Release app (`happys mobile:install`).
144
+ Note: changing `HAPPY_STACKS_SERVER_URL` requires rebuilding/reinstalling the app you care about.
85
145
 
86
- You can also set a custom bundle id (recommended for real devices):
146
+ Important:
87
147
 
88
- ```bash
89
- HAPPY_STACKS_IOS_BUNDLE_ID="com.yourname.happy.local.dev" HAPPY_STACKS_SERVER_URL="https://<your-machine>.<tailnet>.ts.net" happys mobile:install
90
- ```
148
+ - For **non-main stacks**, `HAPPY_STACKS_SERVER_URL` is only respected if it’s set **in that stack’s env file**
149
+ (safety: we ignore “global” URLs for non-main stacks to avoid accidentally repointing other stacks).
150
+
151
+ ## Customizing the app identity (optional / advanced)
152
+
153
+ Happy Stacks uses these identities:
91
154
 
92
- ## Customizing the app identity (optional)
155
+ - **Dev-client**: defaults to `Happy Stacks Dev` + bundle id `com.happystacks.dev.<user>`
156
+ - **Per-stack release**: defaults to `Happy (<stack>)` + bundle id `com.happystacks.stack.<user>.<stack>`
157
+
158
+ If you want to build/install *manually* (instead of `mobile-dev-client` / `stack mobile:install`), you can override:
93
159
 
94
160
  - **Bundle identifier (recommended for real iPhones)**:
95
- - You may *need* this if the default `com.slopus.happy.dev` can’t be registered on your Apple team.
161
+ - You may need this if the bundle id youre using isn’t available/owned by your Apple team.
96
162
 
97
163
  ```bash
98
- HAPPY_STACKS_IOS_BUNDLE_ID="com.yourname.happy.local.dev" happys mobile --run-ios
99
- HAPPY_STACKS_IOS_BUNDLE_ID="com.yourname.happy.local.dev" happys mobile:install
164
+ HAPPY_STACKS_IOS_BUNDLE_ID="com.yourname.happy.local.dev" happys mobile --prebuild --run-ios --no-metro
100
165
  ```
101
166
 
102
167
  - **App name (what shows on the home screen)**:
103
168
 
104
169
  ```bash
105
- HAPPY_STACKS_IOS_APP_NAME="Happy Local" happys mobile:install
170
+ HAPPY_STACKS_IOS_APP_NAME="Happy Local" happys mobile --prebuild --run-ios --no-metro
106
171
  ```
107
172
 
108
173
  ## Suggested env (recommended)
@@ -110,25 +175,18 @@ HAPPY_STACKS_IOS_APP_NAME="Happy Local" happys mobile:install
110
175
  Add these to your main stack env file (`~/.happy/stacks/main/env`) (or `~/.happy-stacks/env.local` for global overrides) so you don’t have to prefix every command:
111
176
 
112
177
  ```bash
113
- # Required if you want the Release app to default to your stack server:
114
- HAPPY_STACKS_SERVER_URL="https://<your-machine>.<tailnet>.ts.net"
178
+ # How the phone reaches Metro:
179
+ # - lan: recommended for real devices
180
+ # - localhost: OK for simulators
181
+ HAPPY_STACKS_MOBILE_HOST="lan"
115
182
 
116
- # Strongly recommended for real devices (needs to be unique + owned by your Apple team):
117
- HAPPY_STACKS_IOS_BUNDLE_ID="com.yourname.happy.local.dev"
183
+ # (optional) default scheme used in the dev-client QR / deep link
184
+ # (must match your installed dev-client app):
185
+ HAPPY_STACKS_DEV_CLIENT_SCHEME="happystacks-dev"
186
+
187
+ # Default public server URL for the stack (baked into the Expo app config):
188
+ HAPPY_STACKS_SERVER_URL="https://<your-machine>.<tailnet>.ts.net"
118
189
 
119
190
  # Optional: home screen name:
120
191
  HAPPY_STACKS_IOS_APP_NAME="Happy Local"
121
192
  ```
122
-
123
- ## Personal build on iPhone (EAS internal distribution)
124
-
125
- ```bash
126
- cd "$HOME/.happy-stacks/workspace/components/happy"
127
- eas build --profile development --platform ios
128
- ```
129
-
130
- Then keep Metro running from `happy-stacks`:
131
-
132
- ```bash
133
- happys mobile --host=lan
134
- ```
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "happy-stacks",
3
3
  "type": "module",
4
- "version": "0.3.0",
4
+ "version": "0.4.0",
5
5
  "packageManager": "pnpm@10.18.3",
6
6
  "bin": {
7
7
  "happys": "./bin/happys.mjs",
@@ -55,5 +55,9 @@
55
55
  "menubar:install": "node ./scripts/menubar.mjs install",
56
56
  "menubar:uninstall": "node ./scripts/menubar.mjs uninstall",
57
57
  "menubar:open": "bash -lc 'DIR=\"$(defaults read com.ameba.SwiftBar PluginDirectory 2>/dev/null)\"; if [[ -z \"$DIR\" ]]; then DIR=\"$HOME/Library/Application Support/SwiftBar/Plugins\"; fi; open \"$DIR\"'"
58
+ },
59
+ "dependencies": {
60
+ "qrcode": "^1.5.4",
61
+ "qrcode-terminal": "^0.12.0"
58
62
  }
59
63
  }
package/scripts/auth.mjs CHANGED
@@ -32,7 +32,7 @@ import {
32
32
  getServerLightDataDirFromEnvOrDefault,
33
33
  resolveCliHomeDir,
34
34
  } from './utils/stack/dirs.mjs';
35
- import { resolveLocalhostHost } from './utils/paths/localhost_host.mjs';
35
+ import { resolveLocalhostHost, preferStackLocalhostUrl } from './utils/paths/localhost_host.mjs';
36
36
 
37
37
  function getInternalServerUrlCompat() {
38
38
  const { port, internalServerUrl } = getInternalServerUrl({ env: process.env, defaultPort: 3005 });
@@ -45,9 +45,9 @@ async function resolveWebappUrlFromRunningExpo({ rootDir, stackName }) {
45
45
  const uiDir = getComponentDir(rootDir, 'happy');
46
46
  const uiPaths = getExpoStatePaths({
47
47
  baseDir,
48
- kind: 'ui-dev',
48
+ kind: 'expo-dev',
49
49
  projectDir: uiDir,
50
- stateFileName: 'ui.state.json',
50
+ stateFileName: 'expo.state.json',
51
51
  });
52
52
  const uiRunning = await isStateProcessRunning(uiPaths.statePath);
53
53
  if (!uiRunning.running) return null;
@@ -580,7 +580,7 @@ async function cmdCopyFrom({ argv, json }) {
580
580
  const managed = (targetEnv.HAPPY_STACKS_MANAGED_INFRA ?? targetEnv.HAPPY_LOCAL_MANAGED_INFRA ?? '1').toString().trim() !== '0';
581
581
  if (targetServerComponent === 'happy-server' && withInfra && managed) {
582
582
  const { port } = getInternalServerUrlCompat();
583
- const publicServerUrl = `http://localhost:${port}`;
583
+ const publicServerUrl = await preferStackLocalhostUrl(`http://localhost:${port}`, { stackName });
584
584
  const envPath = resolveStackEnvPath(stackName).envPath;
585
585
  const infra = await ensureHappyServerManagedInfra({
586
586
  stackName,
@@ -689,6 +689,7 @@ async function cmdStatus({ json }) {
689
689
  defaultPublicUrl,
690
690
  envPublicUrl,
691
691
  allowEnable: false,
692
+ stackName,
692
693
  });
693
694
 
694
695
  const cliHomeDir = resolveCliHomeDir();
@@ -770,7 +771,7 @@ async function cmdStatus({ json }) {
770
771
  async function cmdLogin({ argv, json }) {
771
772
  const rootDir = getRootDir(import.meta.url);
772
773
  const stackName = getStackName();
773
- const { kv } = parseArgs(argv);
774
+ const { flags, kv } = parseArgs(argv);
774
775
 
775
776
  const { port, url: internalServerUrl } = getInternalServerUrlCompat();
776
777
  const { defaultPublicUrl, envPublicUrl } = getPublicServerUrlEnvOverride({ env: process.env, serverPort: port, stackName });
@@ -779,10 +780,12 @@ async function cmdLogin({ argv, json }) {
779
780
  defaultPublicUrl,
780
781
  envPublicUrl,
781
782
  allowEnable: false,
783
+ stackName,
782
784
  });
783
785
  const { envWebappUrl } = getWebappUrlEnvOverride({ env: process.env, stackName });
784
786
  const expoWebappUrl = await resolveWebappUrlFromRunningExpo({ rootDir, stackName });
785
- const webappUrl = envWebappUrl || expoWebappUrl || publicServerUrl;
787
+ const webappUrlRaw = envWebappUrl || expoWebappUrl || publicServerUrl;
788
+ const webappUrl = await preferStackLocalhostUrl(webappUrlRaw, { stackName });
786
789
  const webappUrlSource = expoWebappUrl ? 'expo' : envWebappUrl ? 'stack env override' : 'server';
787
790
 
788
791
  const cliHomeDir = resolveCliHomeDir();
@@ -818,7 +821,8 @@ async function cmdLogin({ argv, json }) {
818
821
  return;
819
822
  }
820
823
 
821
- if (!json) {
824
+ const quietUx = flags.has('--quiet') || flags.has('--no-ux');
825
+ if (!json && !quietUx) {
822
826
  printAuthLoginInstructions({
823
827
  stackName,
824
828
  context,