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
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join, resolve } from 'node:path';
|
|
7
|
+
import { getRootDir } from './utils/paths/paths.mjs';
|
|
8
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
9
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
10
|
+
import { getVerbosityLevel } from './utils/cli/verbosity.mjs';
|
|
11
|
+
import { createStepPrinter } from './utils/cli/progress.mjs';
|
|
12
|
+
import { prompt, promptSelect, withRl } from './utils/cli/wizard.mjs';
|
|
13
|
+
import { assertCliPrereqs } from './utils/cli/prereqs.mjs';
|
|
14
|
+
import { randomToken } from './utils/crypto/tokens.mjs';
|
|
15
|
+
import { inferPrStackBaseName } from './utils/stack/pr_stack_name.mjs';
|
|
16
|
+
import { sanitizeStackName } from './utils/stack/names.mjs';
|
|
17
|
+
import { listReviewPrSandboxes, reviewPrSandboxPrefixPath, writeReviewPrSandboxMeta } from './utils/sandbox/review_pr_sandbox.mjs';
|
|
18
|
+
import { bold, cyan, dim } from './utils/ui/ansi.mjs';
|
|
19
|
+
|
|
20
|
+
function usage() {
|
|
21
|
+
return [
|
|
22
|
+
'[review-pr] usage:',
|
|
23
|
+
' happys review-pr --happy=<pr-url|number> [--happy-server-light=<pr-url|number>] [--name=<stack>] [--dev|--start] [--mobile|--no-mobile] [--forks|--upstream] [--seed-auth|--no-seed-auth] [--copy-auth-from=<stack>] [--link-auth|--copy-auth] [--update] [--force] [--keep-sandbox] [--json] [-- <stack dev/start args...>]',
|
|
24
|
+
'',
|
|
25
|
+
'What it does:',
|
|
26
|
+
'- creates a temporary sandbox dir',
|
|
27
|
+
'- runs `happys setup-pr ...` inside that sandbox (fully isolated state)',
|
|
28
|
+
'- on exit (including Ctrl+C): stops sandbox processes and deletes the sandbox dir',
|
|
29
|
+
'',
|
|
30
|
+
'legacy note:',
|
|
31
|
+
'- `--happy-cli` / `--happy-server` are legacy split-repo flags; in monorepo mode, use `--happy` only.',
|
|
32
|
+
].join('\n');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function waitForExit(child) {
|
|
36
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
37
|
+
child.on('error', rejectPromise);
|
|
38
|
+
child.on('close', (code, signal) => resolvePromise({ code: code ?? 1, signal: signal ?? null }));
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function tryStopSandbox({ rootDir, sandboxDir }) {
|
|
43
|
+
const bin = join(rootDir, 'bin', 'happys.mjs');
|
|
44
|
+
const child = spawn(process.execPath, [bin, '--sandbox-dir', sandboxDir, 'stop', '--yes', '--aggressive', '--sweep-owned', '--no-service'], {
|
|
45
|
+
cwd: rootDir,
|
|
46
|
+
env: process.env,
|
|
47
|
+
stdio: 'ignore',
|
|
48
|
+
});
|
|
49
|
+
await waitForExit(child);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function argvHasFlag(argv, names) {
|
|
53
|
+
for (const n of names) {
|
|
54
|
+
if (argv.includes(n)) return true;
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function kvValue(argv, names) {
|
|
60
|
+
for (const a of argv) {
|
|
61
|
+
for (const n of names) {
|
|
62
|
+
if (a === n) {
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
if (a.startsWith(`${n}=`)) {
|
|
66
|
+
return a.slice(`${n}=`.length);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function main() {
|
|
74
|
+
const rootDir = getRootDir(import.meta.url);
|
|
75
|
+
const argv = process.argv.slice(2);
|
|
76
|
+
const { flags } = parseArgs(argv);
|
|
77
|
+
const json = wantsJson(argv, { flags });
|
|
78
|
+
const verbosity = getVerbosityLevel(process.env);
|
|
79
|
+
const steps = createStepPrinter({ enabled: Boolean(process.stdout.isTTY && !json && verbosity === 0) });
|
|
80
|
+
|
|
81
|
+
if (wantsHelp(argv, { flags })) {
|
|
82
|
+
printResult({ json, data: { usage: usage() }, text: usage() });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await assertCliPrereqs({ git: true, pnpm: true });
|
|
87
|
+
|
|
88
|
+
// Determine a stable base stack name from PR inputs (used for sandbox discovery),
|
|
89
|
+
// and a per-run unique stack name by default (prevents browser storage collisions across deleted sandboxes).
|
|
90
|
+
const prHappy = (kvValue(argv, ['--happy']) ?? '').trim();
|
|
91
|
+
const prCli = (kvValue(argv, ['--happy-cli']) ?? '').trim();
|
|
92
|
+
const prServer = (kvValue(argv, ['--happy-server']) ?? '').trim();
|
|
93
|
+
const prServerLight = (kvValue(argv, ['--happy-server-light']) ?? '').trim();
|
|
94
|
+
const explicitName = (kvValue(argv, ['--name']) ?? '').trim();
|
|
95
|
+
|
|
96
|
+
const baseStackName = explicitName
|
|
97
|
+
? sanitizeStackName(explicitName, { fallback: 'pr', maxLen: 64 })
|
|
98
|
+
: inferPrStackBaseName({ happy: prHappy, happyCli: prCli, server: prServer, serverLight: prServerLight, fallback: 'pr' });
|
|
99
|
+
|
|
100
|
+
const shouldAutoSuffix = !explicitName;
|
|
101
|
+
const uniqueSuffix = randomToken(4); // short, URL-safe-ish
|
|
102
|
+
const newStackName = shouldAutoSuffix
|
|
103
|
+
? sanitizeStackName(`${baseStackName}-${uniqueSuffix}`, { fallback: baseStackName, maxLen: 64 })
|
|
104
|
+
: baseStackName;
|
|
105
|
+
|
|
106
|
+
// Look for leftover sandboxes for the same PR base name (typically due to --keep-sandbox / failures).
|
|
107
|
+
const canPrompt = Boolean(process.stdout.isTTY && process.stdin.isTTY && !json);
|
|
108
|
+
const existingSandboxes = canPrompt ? await listReviewPrSandboxes({ baseStackName }) : [];
|
|
109
|
+
|
|
110
|
+
if (process.stdout.isTTY && !json) {
|
|
111
|
+
const intro = [
|
|
112
|
+
'',
|
|
113
|
+
'',
|
|
114
|
+
bold(`✨ ${cyan('Happy Stacks')} review-pr ✨`),
|
|
115
|
+
'',
|
|
116
|
+
'It will help you review a PR for Happy in a completely isolated environment.',
|
|
117
|
+
dim('Uses `happy-server-light` (no Redis, no Postgres, no Docker).'),
|
|
118
|
+
dim('Desktop browser + optional mobile review (Expo dev-client).'),
|
|
119
|
+
'',
|
|
120
|
+
bold('What will happen:'),
|
|
121
|
+
`- ${cyan('sandbox')}: temporary isolated Happy install`,
|
|
122
|
+
`- ${cyan('components')}: clone/install (inside the sandbox only)`,
|
|
123
|
+
`- ${cyan('start')}: start the Happy stack in sandbox (server, daemon, web, mobile)`,
|
|
124
|
+
`- ${cyan('login')}: guide you through Happy login for this sandbox`,
|
|
125
|
+
`- ${cyan('browser')}: open the Happy web app`,
|
|
126
|
+
`- ${cyan('mobile')}: start Expo dev-client (optional)`,
|
|
127
|
+
`- ${cyan('cleanup')}: stop processes + delete sandbox on exit`,
|
|
128
|
+
'',
|
|
129
|
+
dim('Everything is deleted automatically when you exit.'),
|
|
130
|
+
dim('Your main Happy installation remains untouched.'),
|
|
131
|
+
'',
|
|
132
|
+
dim('Tips:'),
|
|
133
|
+
dim('- Add `-v` / `-vv` / `-vvv` to show the full logs'),
|
|
134
|
+
dim('- Add `--keep-sandbox` to keep the sandbox directory between runs'),
|
|
135
|
+
'',
|
|
136
|
+
existingSandboxes.length
|
|
137
|
+
? bold('Choose how to proceed') + dim(' (or Ctrl+C to cancel).')
|
|
138
|
+
: bold('Press Enter to proceed') + dim(' (or Ctrl+C to cancel).'),
|
|
139
|
+
].join('\n');
|
|
140
|
+
// eslint-disable-next-line no-console
|
|
141
|
+
console.log(intro);
|
|
142
|
+
if (!existingSandboxes.length) {
|
|
143
|
+
await withRl(async (rl) => {
|
|
144
|
+
await prompt(rl, '', { defaultValue: '' });
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let sandboxDir = '';
|
|
150
|
+
let createdNewSandbox = false;
|
|
151
|
+
let reusedSandboxMeta = null;
|
|
152
|
+
|
|
153
|
+
if (existingSandboxes.length) {
|
|
154
|
+
const picked = await withRl(async (rl) => {
|
|
155
|
+
const options = [
|
|
156
|
+
{ label: 'Create a new sandbox (recommended)', value: 'new' },
|
|
157
|
+
...existingSandboxes.map((s) => {
|
|
158
|
+
const stackLabel = s.stackName ? `stack=${s.stackName}` : 'stack=?';
|
|
159
|
+
return { label: `Reuse existing sandbox (${stackLabel}) — ${s.dir}`, value: s.dir };
|
|
160
|
+
}),
|
|
161
|
+
];
|
|
162
|
+
return await promptSelect(rl, {
|
|
163
|
+
title: 'Review-pr sandbox:',
|
|
164
|
+
options,
|
|
165
|
+
defaultIndex: 0,
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
if (picked === 'new') {
|
|
169
|
+
steps.start('create temporary sandbox');
|
|
170
|
+
const prefix = reviewPrSandboxPrefixPath(baseStackName);
|
|
171
|
+
sandboxDir = resolve(await mkdtemp(prefix));
|
|
172
|
+
createdNewSandbox = true;
|
|
173
|
+
steps.stop('✓', 'create temporary sandbox');
|
|
174
|
+
} else {
|
|
175
|
+
sandboxDir = resolve(String(picked));
|
|
176
|
+
reusedSandboxMeta = existingSandboxes.find((s) => resolve(s.dir) === sandboxDir) ?? null;
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
steps.start('create temporary sandbox');
|
|
180
|
+
const prefix = reviewPrSandboxPrefixPath(baseStackName);
|
|
181
|
+
sandboxDir = resolve(await mkdtemp(prefix));
|
|
182
|
+
createdNewSandbox = true;
|
|
183
|
+
steps.stop('✓', 'create temporary sandbox');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// If we're reusing a sandbox, prefer the stack name recorded in its meta file (keeps hostname stable),
|
|
187
|
+
// but only when the user did not explicitly pass --name.
|
|
188
|
+
const effectiveStackName =
|
|
189
|
+
!explicitName && reusedSandboxMeta?.stackName
|
|
190
|
+
? sanitizeStackName(reusedSandboxMeta.stackName, { fallback: baseStackName, maxLen: 64 })
|
|
191
|
+
: newStackName;
|
|
192
|
+
|
|
193
|
+
// Safety marker to ensure we only delete what we created.
|
|
194
|
+
const markerPath = join(sandboxDir, '.happy-stacks-sandbox-marker');
|
|
195
|
+
// Always ensure the marker exists for safety; write meta only for new sandboxes.
|
|
196
|
+
try {
|
|
197
|
+
if (!existsSync(markerPath)) {
|
|
198
|
+
await writeFile(markerPath, 'review-pr\n', 'utf-8');
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
// ignore; deletion guard will fail closed later if marker is missing
|
|
202
|
+
}
|
|
203
|
+
if (createdNewSandbox && existsSync(markerPath)) {
|
|
204
|
+
try {
|
|
205
|
+
await writeReviewPrSandboxMeta({ sandboxDir, baseStackName, stackName: effectiveStackName, argv });
|
|
206
|
+
} catch {
|
|
207
|
+
// ignore
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const bin = join(rootDir, 'bin', 'happys.mjs');
|
|
212
|
+
|
|
213
|
+
let child = null;
|
|
214
|
+
let gotSignal = null;
|
|
215
|
+
let childExitCode = null;
|
|
216
|
+
|
|
217
|
+
const forwardSignal = (sig) => {
|
|
218
|
+
const first = gotSignal == null;
|
|
219
|
+
gotSignal = gotSignal ?? sig;
|
|
220
|
+
if (first && process.stdout.isTTY && !json) {
|
|
221
|
+
// eslint-disable-next-line no-console
|
|
222
|
+
console.log('\n[review-pr] received Ctrl+C — cleaning up sandbox, please wait...');
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
child?.kill(sig);
|
|
226
|
+
} catch {
|
|
227
|
+
// ignore
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const onSigInt = () => forwardSignal('SIGINT');
|
|
232
|
+
const onSigTerm = () => forwardSignal('SIGTERM');
|
|
233
|
+
process.on('SIGINT', onSigInt);
|
|
234
|
+
process.on('SIGTERM', onSigTerm);
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const wantsStart = flags.has('--start') || flags.has('--prod');
|
|
238
|
+
const hasMobileFlag = argv.includes('--mobile') || argv.includes('--with-mobile') || argv.includes('--no-mobile');
|
|
239
|
+
const argvWithDefaults =
|
|
240
|
+
process.stdout.isTTY && !json && !wantsStart && !hasMobileFlag ? [...argv, '--mobile'] : argv;
|
|
241
|
+
|
|
242
|
+
// If the caller did not explicitly name the stack, make it unique per run.
|
|
243
|
+
// This prevents browser storage collisions when sandboxes are deleted between runs.
|
|
244
|
+
const hasNameFlag = argvWithDefaults.some((a) => a === '--name' || a.startsWith('--name='));
|
|
245
|
+
const argvFinal = hasNameFlag ? argvWithDefaults : [...argvWithDefaults, `--name=${effectiveStackName}`];
|
|
246
|
+
|
|
247
|
+
child = spawn(process.execPath, [bin, '--sandbox-dir', sandboxDir, 'setup-pr', ...argvFinal], {
|
|
248
|
+
cwd: rootDir,
|
|
249
|
+
env: process.env,
|
|
250
|
+
stdio: 'inherit',
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const { code } = await waitForExit(child);
|
|
254
|
+
childExitCode = code;
|
|
255
|
+
process.exitCode = code;
|
|
256
|
+
} finally {
|
|
257
|
+
process.off('SIGINT', onSigInt);
|
|
258
|
+
process.off('SIGTERM', onSigTerm);
|
|
259
|
+
|
|
260
|
+
steps.start('stop sandbox processes (best-effort)');
|
|
261
|
+
try {
|
|
262
|
+
// Best-effort stop before deleting the sandbox.
|
|
263
|
+
await tryStopSandbox({ rootDir, sandboxDir });
|
|
264
|
+
steps.stop('✓', 'stop sandbox processes (best-effort)');
|
|
265
|
+
} catch {
|
|
266
|
+
steps.stop('x', 'stop sandbox processes (best-effort)');
|
|
267
|
+
// eslint-disable-next-line no-console
|
|
268
|
+
console.warn(`[review-pr] warning: failed to stop all sandbox processes. Attempting cleanup anyway.`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// On failure, offer to keep the sandbox for inspection (TTY only).
|
|
272
|
+
// - `--keep-sandbox` always wins (no prompt)
|
|
273
|
+
// - on signals, don't prompt (just follow the normal cleanup rules)
|
|
274
|
+
const keepSandbox = flags.has('--keep-sandbox');
|
|
275
|
+
const failed = !json && (childExitCode ?? 0) !== 0;
|
|
276
|
+
const canPromptKeep =
|
|
277
|
+
failed &&
|
|
278
|
+
!keepSandbox &&
|
|
279
|
+
!gotSignal &&
|
|
280
|
+
Boolean(process.stdout.isTTY && process.stdin.isTTY) &&
|
|
281
|
+
!json;
|
|
282
|
+
|
|
283
|
+
let keepOnFail = false;
|
|
284
|
+
if (failed && !keepSandbox && !gotSignal) {
|
|
285
|
+
if (canPromptKeep) {
|
|
286
|
+
// Default: keep in verbose mode, delete otherwise.
|
|
287
|
+
const defaultKeep = getVerbosityLevel(process.env) > 0;
|
|
288
|
+
keepOnFail = await withRl(async (rl) => {
|
|
289
|
+
return await promptSelect(rl, {
|
|
290
|
+
title: 'Review-pr failed. Keep the sandbox for inspection?',
|
|
291
|
+
options: [
|
|
292
|
+
{ label: 'yes (keep sandbox directory)', value: true },
|
|
293
|
+
{ label: 'no (delete sandbox directory)', value: false },
|
|
294
|
+
],
|
|
295
|
+
defaultIndex: defaultKeep ? 0 : 1,
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
} else {
|
|
299
|
+
// Non-interactive: keep old behavior (verbose keeps, otherwise delete).
|
|
300
|
+
keepOnFail = getVerbosityLevel(process.env) > 0;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const shouldDeleteSandbox = !keepSandbox && !(failed && keepOnFail);
|
|
305
|
+
|
|
306
|
+
steps.start('delete sandbox directory');
|
|
307
|
+
// Only delete if marker exists (paranoia guard).
|
|
308
|
+
// Note: if marker is missing, we intentionally leave the sandbox dir on disk.
|
|
309
|
+
try {
|
|
310
|
+
if (!existsSync(markerPath)) {
|
|
311
|
+
throw new Error('missing marker');
|
|
312
|
+
}
|
|
313
|
+
if (!shouldDeleteSandbox) {
|
|
314
|
+
steps.stop('!', 'delete sandbox directory');
|
|
315
|
+
// eslint-disable-next-line no-console
|
|
316
|
+
console.warn(`[review-pr] sandbox preserved at: ${sandboxDir}`);
|
|
317
|
+
if (!json && (childExitCode ?? 0) !== 0) {
|
|
318
|
+
// eslint-disable-next-line no-console
|
|
319
|
+
console.warn(`[review-pr] tip: inspect stack wiring with:`);
|
|
320
|
+
// eslint-disable-next-line no-console
|
|
321
|
+
console.warn(` npx happy-stacks --sandbox-dir "${sandboxDir}" stack info ${effectiveStackName}`);
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
await rm(markerPath, { force: false });
|
|
325
|
+
await rm(sandboxDir, { recursive: true, force: true });
|
|
326
|
+
steps.stop('✓', 'delete sandbox directory');
|
|
327
|
+
}
|
|
328
|
+
} catch {
|
|
329
|
+
steps.stop('x', 'delete sandbox directory');
|
|
330
|
+
// eslint-disable-next-line no-console
|
|
331
|
+
console.warn(`[review-pr] warning: failed to delete sandbox directory: ${sandboxDir}`);
|
|
332
|
+
// eslint-disable-next-line no-console
|
|
333
|
+
console.warn(`[review-pr] you can remove it manually after stopping any remaining processes.`);
|
|
334
|
+
// Preserve conventional exit codes on signals.
|
|
335
|
+
if (gotSignal) {
|
|
336
|
+
const code = gotSignal === 'SIGINT' ? 130 : gotSignal === 'SIGTERM' ? 143 : 1;
|
|
337
|
+
process.exitCode = process.exitCode ?? code;
|
|
338
|
+
}
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
// Preserve conventional exit codes on signals.
|
|
342
|
+
if (gotSignal) {
|
|
343
|
+
const code = gotSignal === 'SIGINT' ? 130 : gotSignal === 'SIGTERM' ? 143 : 1;
|
|
344
|
+
process.exitCode = process.exitCode ?? code;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
main().catch((err) => {
|
|
350
|
+
console.error('[review-pr] failed:', err);
|
|
351
|
+
process.exit(1);
|
|
352
|
+
});
|
|
353
|
+
|
package/scripts/run.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import './utils/env/env.mjs';
|
|
|
2
2
|
import { parseArgs } from './utils/cli/args.mjs';
|
|
3
3
|
import { pathExists } from './utils/fs/fs.mjs';
|
|
4
4
|
import { killProcessTree, runCapture, spawnProc } from './utils/proc/proc.mjs';
|
|
5
|
-
import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths/paths.mjs';
|
|
5
|
+
import { componentDirEnvKey, getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths/paths.mjs';
|
|
6
6
|
import { killPortListeners } from './utils/net/ports.mjs';
|
|
7
7
|
import { getServerComponentName, isHappyServerRunning, waitForServerReady } from './utils/server/server.mjs';
|
|
8
8
|
import { ensureCliBuilt, ensureDepsInstalled, pmExecBin, pmSpawnScript, requireDir } from './utils/proc/pm.mjs';
|
|
@@ -13,21 +13,27 @@ import { maybeResetTailscaleServe } from './tailscale.mjs';
|
|
|
13
13
|
import { isDaemonRunning, startLocalDaemonWithAuth, stopLocalDaemon } from './daemon.mjs';
|
|
14
14
|
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
15
15
|
import { assertServerComponentDirMatches, assertServerPrismaProviderMatches } from './utils/server/validate.mjs';
|
|
16
|
+
import { resolveServerStartScript } from './utils/server/flavor_scripts.mjs';
|
|
16
17
|
import { applyHappyServerMigrations, ensureHappyServerManagedInfra } from './utils/server/infra/happy_server_infra.mjs';
|
|
17
|
-
import { getAccountCountForServerComponent, prepareDaemonAuthSeedIfNeeded } from './utils/stack/startup.mjs';
|
|
18
|
+
import { getAccountCountForServerComponent, prepareDaemonAuthSeedIfNeeded, resolveAutoCopyFromMainEnabled } from './utils/stack/startup.mjs';
|
|
18
19
|
import { recordStackRuntimeStart, recordStackRuntimeUpdate } from './utils/stack/runtime_state.mjs';
|
|
19
20
|
import { resolveStackContext } from './utils/stack/context.mjs';
|
|
20
21
|
import { getPublicServerUrlEnvOverride, resolveServerPortFromEnv, resolveServerUrls } from './utils/server/urls.mjs';
|
|
21
|
-
import {
|
|
22
|
+
import { preferStackLocalhostUrl } from './utils/paths/localhost_host.mjs';
|
|
22
23
|
import { openUrlInBrowser } from './utils/ui/browser.mjs';
|
|
24
|
+
import { ensureDevExpoServer, resolveExpoTailscaleEnabled } from './utils/dev/expo_dev.mjs';
|
|
25
|
+
import { maybeRunInteractiveStackAuthSetup } from './utils/auth/interactive_stack_auth.mjs';
|
|
26
|
+
import { getInvokedCwd, inferComponentFromCwd } from './utils/cli/cwd_scope.mjs';
|
|
27
|
+
import { daemonStartGate, formatDaemonAuthRequiredError } from './utils/auth/daemon_gate.mjs';
|
|
28
|
+
import { resolveServerUiEnv } from './utils/server/ui_env.mjs';
|
|
23
29
|
|
|
24
30
|
/**
|
|
25
31
|
* Run the local stack in "production-like" mode:
|
|
26
|
-
* - happy-server-light
|
|
32
|
+
* - server (happy-server-light by default)
|
|
27
33
|
* - happy-cli daemon
|
|
28
|
-
* - serve prebuilt UI via
|
|
34
|
+
* - optionally serve prebuilt UI (via server or gateway)
|
|
29
35
|
*
|
|
30
|
-
*
|
|
36
|
+
* Optional: Expo dev-client Metro for mobile reviewers (`--mobile`).
|
|
31
37
|
*/
|
|
32
38
|
|
|
33
39
|
async function main() {
|
|
@@ -37,12 +43,17 @@ async function main() {
|
|
|
37
43
|
if (wantsHelp(argv, { flags })) {
|
|
38
44
|
printResult({
|
|
39
45
|
json,
|
|
40
|
-
data: { flags: ['--server=happy-server|happy-server-light', '--no-ui', '--no-daemon', '--restart', '--no-browser'], json: true },
|
|
46
|
+
data: { flags: ['--server=happy-server|happy-server-light', '--no-ui', '--no-daemon', '--restart', '--no-browser', '--mobile', '--expo-tailscale'], json: true },
|
|
41
47
|
text: [
|
|
42
48
|
'[start] usage:',
|
|
43
49
|
' happys start [--server=happy-server|happy-server-light] [--restart] [--json]',
|
|
50
|
+
' happys start --mobile # also start Expo dev-client Metro for mobile',
|
|
51
|
+
' happys start --expo-tailscale # forward Expo to Tailscale interface for remote access',
|
|
44
52
|
' (legacy in a cloned repo): pnpm start [-- --server=happy-server|happy-server-light] [--json]',
|
|
45
53
|
' note: --json prints the resolved config (dry-run) and exits.',
|
|
54
|
+
'',
|
|
55
|
+
'note:',
|
|
56
|
+
' If run from inside a component checkout/worktree, that checkout is used for this run (without requiring `happys wt use`).',
|
|
46
57
|
].join('\n'),
|
|
47
58
|
});
|
|
48
59
|
return;
|
|
@@ -50,6 +61,20 @@ async function main() {
|
|
|
50
61
|
|
|
51
62
|
const rootDir = getRootDir(import.meta.url);
|
|
52
63
|
|
|
64
|
+
const inferred = inferComponentFromCwd({
|
|
65
|
+
rootDir,
|
|
66
|
+
invokedCwd: getInvokedCwd(process.env),
|
|
67
|
+
components: ['happy', 'happy-cli', 'happy-server-light', 'happy-server'],
|
|
68
|
+
});
|
|
69
|
+
if (inferred) {
|
|
70
|
+
const stacksKey = componentDirEnvKey(inferred.component);
|
|
71
|
+
const legacyKey = stacksKey.replace(/^HAPPY_STACKS_/, 'HAPPY_LOCAL_');
|
|
72
|
+
// Stack env should win. Only infer from CWD when the component dir isn't already configured.
|
|
73
|
+
if (!(process.env[stacksKey] ?? '').toString().trim() && !(process.env[legacyKey] ?? '').toString().trim()) {
|
|
74
|
+
process.env[stacksKey] = inferred.repoDir;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
53
78
|
const serverPort = resolveServerPortFromEnv({ defaultPort: 3005 });
|
|
54
79
|
|
|
55
80
|
// Internal URL used by local processes on this machine.
|
|
@@ -67,6 +92,8 @@ async function main() {
|
|
|
67
92
|
const startDaemon = !flags.has('--no-daemon') && (process.env.HAPPY_LOCAL_DAEMON ?? '1') !== '0';
|
|
68
93
|
const serveUiWanted = !flags.has('--no-ui') && (process.env.HAPPY_LOCAL_SERVE_UI ?? '1') !== '0';
|
|
69
94
|
const serveUi = serveUiWanted;
|
|
95
|
+
const startMobile = flags.has('--mobile') || flags.has('--with-mobile');
|
|
96
|
+
const expoTailscale = flags.has('--expo-tailscale') || resolveExpoTailscaleEnabled({ env: process.env });
|
|
70
97
|
const noBrowser = flags.has('--no-browser') || (process.env.HAPPY_STACKS_NO_BROWSER ?? process.env.HAPPY_LOCAL_NO_BROWSER ?? '').toString().trim() === '1';
|
|
71
98
|
const uiPrefix = process.env.HAPPY_LOCAL_UI_PREFIX?.trim() ? process.env.HAPPY_LOCAL_UI_PREFIX.trim() : '/';
|
|
72
99
|
const autostart = getDefaultAutostartPaths();
|
|
@@ -78,12 +105,17 @@ async function main() {
|
|
|
78
105
|
|
|
79
106
|
const serverDir = getComponentDir(rootDir, serverComponentName);
|
|
80
107
|
const cliDir = getComponentDir(rootDir, 'happy-cli');
|
|
108
|
+
const uiDir = getComponentDir(rootDir, 'happy');
|
|
109
|
+
const serverStartScript = resolveServerStartScript({ serverComponentName, serverDir });
|
|
81
110
|
|
|
82
111
|
assertServerComponentDirMatches({ rootDir, serverComponentName, serverDir });
|
|
83
112
|
assertServerPrismaProviderMatches({ serverComponentName, serverDir });
|
|
84
113
|
|
|
85
114
|
await requireDir(serverComponentName, serverDir);
|
|
86
115
|
await requireDir('happy-cli', cliDir);
|
|
116
|
+
if (startMobile) {
|
|
117
|
+
await requireDir('happy', uiDir);
|
|
118
|
+
}
|
|
87
119
|
|
|
88
120
|
const cliBin = join(cliDir, 'bin', 'happy.mjs');
|
|
89
121
|
|
|
@@ -99,12 +131,14 @@ async function main() {
|
|
|
99
131
|
mode: 'start',
|
|
100
132
|
serverComponentName,
|
|
101
133
|
serverDir,
|
|
134
|
+
uiDir,
|
|
102
135
|
cliDir,
|
|
103
136
|
serverPort,
|
|
104
137
|
internalServerUrl,
|
|
105
138
|
publicServerUrl,
|
|
106
139
|
startDaemon,
|
|
107
140
|
serveUi,
|
|
141
|
+
startMobile,
|
|
108
142
|
uiPrefix,
|
|
109
143
|
uiBuildDir,
|
|
110
144
|
cliHomeDir,
|
|
@@ -113,19 +147,20 @@ async function main() {
|
|
|
113
147
|
return;
|
|
114
148
|
}
|
|
115
149
|
|
|
116
|
-
|
|
150
|
+
const uiBuildDirExists = await pathExists(uiBuildDir);
|
|
151
|
+
if (serveUi && !uiBuildDirExists) {
|
|
117
152
|
if (serverComponentName === 'happy-server-light') {
|
|
118
153
|
throw new Error(`[local] UI build directory not found at ${uiBuildDir}. Run: happys build (legacy in a cloned repo: pnpm build)`);
|
|
119
154
|
}
|
|
120
|
-
// For happy-server, UI serving is optional
|
|
121
|
-
console.log(`[local] UI build directory not found at ${uiBuildDir}; UI
|
|
155
|
+
// For happy-server, UI serving is optional.
|
|
156
|
+
console.log(`[local] UI build directory not found at ${uiBuildDir}; UI serving will be disabled`);
|
|
122
157
|
}
|
|
123
158
|
|
|
124
159
|
const children = [];
|
|
125
160
|
let shuttingDown = false;
|
|
126
161
|
const baseEnv = { ...process.env };
|
|
127
162
|
const stackCtx = resolveStackContext({ env: baseEnv, autostart });
|
|
128
|
-
const { stackMode, runtimeStatePath, stackName, ephemeral } = stackCtx;
|
|
163
|
+
const { stackMode, runtimeStatePath, stackName, envPath, ephemeral } = stackCtx;
|
|
129
164
|
|
|
130
165
|
// Ensure happy-cli is install+build ready before starting the daemon.
|
|
131
166
|
const buildCli = (baseEnv.HAPPY_STACKS_CLI_BUILD ?? baseEnv.HAPPY_LOCAL_CLI_BUILD ?? '1').toString().trim() !== '0';
|
|
@@ -133,12 +168,18 @@ async function main() {
|
|
|
133
168
|
|
|
134
169
|
// Ensure server deps exist before any Prisma/docker work.
|
|
135
170
|
await ensureDepsInstalled(serverDir, serverComponentName);
|
|
171
|
+
if (startMobile) {
|
|
172
|
+
await ensureDepsInstalled(uiDir, 'happy');
|
|
173
|
+
}
|
|
136
174
|
|
|
137
175
|
// Public URL automation:
|
|
138
176
|
// - Only the main stack should ever auto-enable Tailscale Serve by default.
|
|
139
177
|
// - Non-main stacks default to localhost unless the user explicitly configured a public URL
|
|
140
178
|
// OR Tailscale Serve is already configured for this stack's internal URL (status matches).
|
|
141
|
-
const allowEnableTailscale =
|
|
179
|
+
const allowEnableTailscale =
|
|
180
|
+
!stackMode ||
|
|
181
|
+
stackName === 'main' ||
|
|
182
|
+
(baseEnv.HAPPY_STACKS_TAILSCALE_SERVE ?? baseEnv.HAPPY_LOCAL_TAILSCALE_SERVE ?? '0').toString().trim() === '1';
|
|
142
183
|
const resolvedUrls = await resolveServerUrls({ env: baseEnv, serverPort, allowEnable: allowEnableTailscale });
|
|
143
184
|
if (stackMode && stackName !== 'main' && !resolvedUrls.envPublicUrl) {
|
|
144
185
|
const src = String(resolvedUrls.publicServerUrlSource ?? '');
|
|
@@ -181,12 +222,7 @@ async function main() {
|
|
|
181
222
|
// Avoid noisy failures if a previous run left the metrics port busy.
|
|
182
223
|
// You can override with METRICS_ENABLED=true if you want it.
|
|
183
224
|
METRICS_ENABLED: baseEnv.METRICS_ENABLED ?? 'false',
|
|
184
|
-
...(serveUi
|
|
185
|
-
? {
|
|
186
|
-
HAPPY_SERVER_LIGHT_UI_DIR: uiBuildDir,
|
|
187
|
-
HAPPY_SERVER_LIGHT_UI_PREFIX: uiPrefix,
|
|
188
|
-
}
|
|
189
|
-
: {}),
|
|
225
|
+
...resolveServerUiEnv({ serveUi, uiBuildDir, uiPrefix, uiBuildDirExists }),
|
|
190
226
|
};
|
|
191
227
|
let serverLightAccountCount = null;
|
|
192
228
|
let happyServerAccountCount = null;
|
|
@@ -285,7 +321,7 @@ async function main() {
|
|
|
285
321
|
// Default server start (happy-server-light, or happy-server without managed infra).
|
|
286
322
|
if (!(serverComponentName === 'happy-server' && (baseEnv.HAPPY_STACKS_MANAGED_INFRA ?? baseEnv.HAPPY_LOCAL_MANAGED_INFRA ?? '1') !== '0')) {
|
|
287
323
|
if (!serverAlreadyRunning || restart) {
|
|
288
|
-
const server = await pmSpawnScript({ label: 'server', dir: serverDir, script:
|
|
324
|
+
const server = await pmSpawnScript({ label: 'server', dir: serverDir, script: serverStartScript, env: serverEnv });
|
|
289
325
|
children.push(server);
|
|
290
326
|
if (stackMode && runtimeStatePath) {
|
|
291
327
|
await recordStackRuntimeUpdate(runtimeStatePath, { processes: { serverPid: server.pid } }).catch(() => {});
|
|
@@ -331,9 +367,8 @@ async function main() {
|
|
|
331
367
|
// Auto-open UI (interactive only) using the stack-scoped hostname when applicable.
|
|
332
368
|
const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
333
369
|
if (isInteractive && !noBrowser) {
|
|
334
|
-
const host = resolveLocalhostHost({ stackMode, stackName: autostart.stackName });
|
|
335
370
|
const prefix = uiPrefix.startsWith('/') ? uiPrefix : `/${uiPrefix}`;
|
|
336
|
-
const openUrl = `http
|
|
371
|
+
const openUrl = await preferStackLocalhostUrl(`http://localhost:${serverPort}${prefix}`, { stackName: autostart.stackName });
|
|
337
372
|
const res = await openUrlInBrowser(openUrl);
|
|
338
373
|
if (!res.ok) {
|
|
339
374
|
console.warn(`[local] ui: failed to open browser automatically (${res.error}).`);
|
|
@@ -343,6 +378,16 @@ async function main() {
|
|
|
343
378
|
|
|
344
379
|
// Daemon
|
|
345
380
|
if (startDaemon) {
|
|
381
|
+
const gate = daemonStartGate({ env: baseEnv, cliHomeDir });
|
|
382
|
+
if (!gate.ok) {
|
|
383
|
+
const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
384
|
+
// In orchestrated auth flows, keep server/UI up and let the orchestrator start daemon post-auth.
|
|
385
|
+
if (gate.reason === 'auth_flow_missing_credentials') {
|
|
386
|
+
console.log('[local] auth flow: skipping daemon start until credentials exist');
|
|
387
|
+
} else if (!isInteractive) {
|
|
388
|
+
throw new Error(formatDaemonAuthRequiredError({ stackName: autostart.stackName, cliHomeDir }));
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
346
391
|
const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
347
392
|
if (serverComponentName === 'happy-server' && happyServerAccountCount == null) {
|
|
348
393
|
const acct = await getAccountCountForServerComponent({
|
|
@@ -355,6 +400,16 @@ async function main() {
|
|
|
355
400
|
}
|
|
356
401
|
const accountCount =
|
|
357
402
|
serverComponentName === 'happy-server-light' ? serverLightAccountCount : happyServerAccountCount;
|
|
403
|
+
const autoSeedEnabled = resolveAutoCopyFromMainEnabled({ env: baseEnv, stackName: autostart.stackName, isInteractive });
|
|
404
|
+
await maybeRunInteractiveStackAuthSetup({
|
|
405
|
+
rootDir,
|
|
406
|
+
env: baseEnv,
|
|
407
|
+
stackName: autostart.stackName,
|
|
408
|
+
cliHomeDir,
|
|
409
|
+
accountCount,
|
|
410
|
+
isInteractive,
|
|
411
|
+
autoSeedEnabled,
|
|
412
|
+
});
|
|
358
413
|
await prepareDaemonAuthSeedIfNeeded({
|
|
359
414
|
rootDir,
|
|
360
415
|
env: baseEnv,
|
|
@@ -372,7 +427,32 @@ async function main() {
|
|
|
372
427
|
publicServerUrl,
|
|
373
428
|
isShuttingDown: () => shuttingDown,
|
|
374
429
|
forceRestart: restart,
|
|
430
|
+
env: baseEnv,
|
|
431
|
+
stackName: autostart.stackName,
|
|
375
432
|
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Optional: start Expo dev-client Metro for mobile reviewers.
|
|
437
|
+
if (startMobile) {
|
|
438
|
+
const expoRes = await ensureDevExpoServer({
|
|
439
|
+
startUi: false,
|
|
440
|
+
startMobile: true,
|
|
441
|
+
uiDir,
|
|
442
|
+
autostart,
|
|
443
|
+
baseEnv,
|
|
444
|
+
apiServerUrl: publicServerUrl,
|
|
445
|
+
restart,
|
|
446
|
+
stackMode,
|
|
447
|
+
runtimeStatePath,
|
|
448
|
+
stackName,
|
|
449
|
+
envPath,
|
|
450
|
+
children,
|
|
451
|
+
expoTailscale,
|
|
452
|
+
});
|
|
453
|
+
if (expoRes?.tailscale?.ok && expoRes.tailscale.tailscaleIp && expoRes.port) {
|
|
454
|
+
console.log(`[local] expo tailscale: http://${expoRes.tailscale.tailscaleIp}:${expoRes.port}`);
|
|
455
|
+
}
|
|
376
456
|
}
|
|
377
457
|
|
|
378
458
|
const shutdown = async () => {
|
package/scripts/service.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import './utils/env/env.mjs';
|
|
2
2
|
import { run, runCapture } from './utils/proc/proc.mjs';
|
|
3
|
-
import { getDefaultAutostartPaths, getRootDir, resolveStackEnvPath } from './utils/paths/paths.mjs';
|
|
3
|
+
import { getComponentDir, getDefaultAutostartPaths, getRootDir, resolveStackEnvPath } from './utils/paths/paths.mjs';
|
|
4
4
|
import { getInternalServerUrl, getPublicServerUrlEnvOverride } from './utils/server/urls.mjs';
|
|
5
5
|
import { ensureMacAutostartDisabled, ensureMacAutostartEnabled } from './utils/service/autostart_darwin.mjs';
|
|
6
6
|
import { getCanonicalHomeDir } from './utils/env/config.mjs';
|
|
@@ -278,7 +278,7 @@ async function postStartDiagnostics() {
|
|
|
278
278
|
}
|
|
279
279
|
const { publicServerUrl: publicUrl } = getPublicServerUrlEnvOverride({ env: process.env, serverPort: port });
|
|
280
280
|
|
|
281
|
-
const cliDir =
|
|
281
|
+
const cliDir = getComponentDir(rootDir, 'happy-cli');
|
|
282
282
|
const cliBin = join(cliDir, 'bin', 'happy.mjs');
|
|
283
283
|
|
|
284
284
|
const accessKey = join(cliHomeDir, 'access.key');
|