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.
- package/README.md +29 -7
- package/bin/happys.mjs +114 -15
- package/docs/happy-development.md +2 -2
- package/docs/isolated-linux-vm.md +82 -0
- package/docs/mobile-ios.md +112 -54
- package/package.json +5 -1
- package/scripts/auth.mjs +11 -7
- package/scripts/build.mjs +54 -7
- package/scripts/daemon.mjs +166 -10
- package/scripts/dev.mjs +181 -46
- package/scripts/edison.mjs +4 -2
- package/scripts/init.mjs +3 -1
- package/scripts/install.mjs +112 -16
- package/scripts/lint.mjs +24 -4
- package/scripts/mobile.mjs +88 -104
- package/scripts/mobile_dev_client.mjs +83 -0
- package/scripts/provision/linux-ubuntu-review-pr.sh +51 -0
- package/scripts/review.mjs +217 -0
- package/scripts/review_pr.mjs +368 -0
- package/scripts/run.mjs +83 -9
- package/scripts/service.mjs +2 -2
- package/scripts/setup.mjs +42 -43
- package/scripts/setup_pr.mjs +591 -34
- package/scripts/stack.mjs +503 -45
- package/scripts/tailscale.mjs +37 -1
- package/scripts/test.mjs +45 -8
- package/scripts/tui.mjs +309 -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 +24 -0
- package/scripts/utils/cli/cwd_scope.mjs +82 -0
- package/scripts/utils/cli/cwd_scope.test.mjs +77 -0
- package/scripts/utils/cli/log_forwarder.mjs +157 -0
- package/scripts/utils/cli/prereqs.mjs +72 -0
- package/scripts/utils/cli/progress.mjs +126 -0
- package/scripts/utils/cli/verbosity.mjs +12 -0
- package/scripts/utils/dev/daemon.mjs +47 -3
- package/scripts/utils/dev/expo_dev.mjs +246 -0
- package/scripts/utils/dev/expo_dev.test.mjs +76 -0
- package/scripts/utils/dev/server.mjs +15 -25
- package/scripts/utils/dev_auth_key.mjs +169 -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 +24 -20
- 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/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 +42 -38
- package/scripts/utils/proc/parallel.mjs +25 -0
- package/scripts/utils/proc/pm.mjs +69 -12
- package/scripts/utils/proc/proc.mjs +76 -2
- package/scripts/utils/review/base_ref.mjs +74 -0
- package/scripts/utils/review/base_ref.test.mjs +54 -0
- package/scripts/utils/review/runners/coderabbit.mjs +19 -0
- package/scripts/utils/review/runners/codex.mjs +51 -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/mobile_api_url.mjs +61 -0
- package/scripts/utils/server/mobile_api_url.test.mjs +41 -0
- package/scripts/utils/server/urls.mjs +14 -4
- 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 +2 -2
- 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 +7 -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/ui/qr.mjs +17 -0
- package/scripts/utils/validate.mjs +88 -0
- package/scripts/worktrees.mjs +141 -55
- 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:
|
|
236
|
+
Optional: enable Expo dev-client for mobile reviewers (works with both default `--dev` and `--start`):
|
|
230
237
|
|
|
231
238
|
```bash
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
320
|
-
happys mobile --
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
+
|
package/docs/mobile-ios.md
CHANGED
|
@@ -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
|
-
##
|
|
11
|
+
## Two supported modes
|
|
12
12
|
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
24
|
+
Install the dedicated Happy Stacks dev-client app on your iPhone (USB).
|
|
20
25
|
|
|
21
|
-
|
|
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
|
|
30
|
+
happys mobile-dev-client --install
|
|
25
31
|
```
|
|
26
32
|
|
|
27
|
-
-
|
|
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
|
|
37
|
+
happys stack mobile-dev-client <stack> --install
|
|
31
38
|
```
|
|
32
39
|
|
|
33
|
-
|
|
40
|
+
Optional:
|
|
34
41
|
|
|
35
42
|
```bash
|
|
36
|
-
happys mobile --
|
|
43
|
+
happys mobile-dev-client --install --device="Your iPhone"
|
|
44
|
+
happys mobile-dev-client --install --clean
|
|
37
45
|
```
|
|
38
46
|
|
|
39
|
-
|
|
47
|
+
Then run any stack with mobile enabled:
|
|
40
48
|
|
|
41
49
|
```bash
|
|
42
|
-
happys mobile
|
|
50
|
+
happys stack dev <stack> --mobile
|
|
51
|
+
# or:
|
|
52
|
+
happys dev --mobile
|
|
43
53
|
```
|
|
44
54
|
|
|
45
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
+
Run:
|
|
55
82
|
|
|
56
83
|
```bash
|
|
57
|
-
happys mobile
|
|
84
|
+
happys mobile --prebuild
|
|
85
|
+
# (optional) fully regenerate ios/:
|
|
86
|
+
happys mobile --prebuild --clean
|
|
58
87
|
```
|
|
59
88
|
|
|
60
|
-
|
|
89
|
+
What this does today:
|
|
61
90
|
|
|
62
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
128
|
+
happys mobile:devices
|
|
72
129
|
```
|
|
73
130
|
|
|
74
|
-
|
|
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
|
|
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
|
|
144
|
+
Note: changing `HAPPY_STACKS_SERVER_URL` requires rebuilding/reinstalling the app you care about.
|
|
85
145
|
|
|
86
|
-
|
|
146
|
+
Important:
|
|
87
147
|
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
|
161
|
+
- You may need this if the bundle id you’re 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
|
|
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
|
-
#
|
|
114
|
-
|
|
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
|
-
#
|
|
117
|
-
|
|
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.
|
|
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: '
|
|
48
|
+
kind: 'expo-dev',
|
|
49
49
|
projectDir: uiDir,
|
|
50
|
-
stateFileName: '
|
|
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
|
|
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
|
-
|
|
824
|
+
const quietUx = flags.has('--quiet') || flags.has('--no-ux');
|
|
825
|
+
if (!json && !quietUx) {
|
|
822
826
|
printAuthLoginInstructions({
|
|
823
827
|
stackName,
|
|
824
828
|
context,
|