happy-stacks 0.2.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 +84 -25
- package/bin/happys.mjs +116 -17
- 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 +59 -208
- package/scripts/build.mjs +58 -12
- package/scripts/cli-link.mjs +3 -3
- package/scripts/completion.mjs +5 -5
- package/scripts/daemon.mjs +168 -20
- package/scripts/dev.mjs +196 -70
- package/scripts/doctor.mjs +20 -36
- package/scripts/edison.mjs +105 -78
- package/scripts/happy.mjs +8 -19
- package/scripts/init.mjs +8 -14
- package/scripts/install.mjs +119 -23
- package/scripts/lint.mjs +31 -32
- package/scripts/menubar.mjs +6 -13
- package/scripts/migrate.mjs +11 -21
- package/scripts/mobile.mjs +93 -108
- 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 +95 -21
- package/scripts/self.mjs +11 -29
- package/scripts/server_flavor.mjs +4 -4
- package/scripts/service.mjs +19 -29
- package/scripts/setup.mjs +63 -160
- package/scripts/setup_pr.mjs +592 -52
- package/scripts/stack.mjs +608 -200
- package/scripts/stop.mjs +3 -3
- package/scripts/tailscale.mjs +44 -11
- package/scripts/test.mjs +52 -36
- package/scripts/tui.mjs +314 -74
- package/scripts/typecheck.mjs +31 -32
- package/scripts/ui_gateway.mjs +1 -1
- package/scripts/uninstall.mjs +6 -6
- package/scripts/utils/auth/daemon_gate.mjs +55 -0
- package/scripts/utils/auth/daemon_gate.test.mjs +37 -0
- package/scripts/utils/auth/dev_key.mjs +163 -0
- package/scripts/utils/{auth_files.mjs → auth/files.mjs} +2 -4
- 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/handy_master_secret.mjs +68 -0
- package/scripts/utils/auth/interactive_stack_auth.mjs +72 -0
- package/scripts/utils/{auth_login_ux.mjs → auth/login_ux.mjs} +32 -13
- package/scripts/utils/auth/sources.mjs +38 -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/flags.mjs +17 -0
- package/scripts/utils/cli/log_forwarder.mjs +157 -0
- package/scripts/utils/cli/normalize.mjs +16 -0
- package/scripts/utils/cli/prereqs.mjs +72 -0
- package/scripts/utils/cli/progress.mjs +126 -0
- package/scripts/utils/cli/smoke_help.mjs +2 -2
- package/scripts/utils/cli/verbosity.mjs +12 -0
- package/scripts/utils/cli/wizard.mjs +1 -1
- package/scripts/utils/crypto/tokens.mjs +14 -0
- package/scripts/utils/{dev_daemon.mjs → dev/daemon.mjs} +51 -7
- 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 → dev/server.mjs} +22 -32
- package/scripts/utils/dev_auth_key.mjs +1 -1
- package/scripts/utils/{config.mjs → env/config.mjs} +3 -2
- package/scripts/utils/{dotenv.mjs → env/dotenv.mjs} +3 -0
- package/scripts/utils/{env.mjs → env/env.mjs} +5 -3
- package/scripts/utils/{env_file.mjs → env/env_file.mjs} +2 -1
- package/scripts/utils/{env_local.mjs → env/env_local.mjs} +1 -0
- package/scripts/utils/env/read.mjs +30 -0
- package/scripts/utils/env/values.mjs +13 -0
- package/scripts/utils/expo/command.mjs +52 -0
- package/scripts/utils/{expo.mjs → expo/expo.mjs} +23 -10
- package/scripts/utils/expo/metro_ports.mjs +114 -0
- package/scripts/utils/fs/json.mjs +25 -0
- package/scripts/utils/fs/ops.mjs +29 -0
- package/scripts/utils/fs/package_json.mjs +8 -0
- package/scripts/utils/fs/tail.mjs +12 -0
- package/scripts/utils/git/git.mjs +67 -0
- package/scripts/utils/git/refs.mjs +26 -0
- package/scripts/utils/{worktrees.mjs → git/worktrees.mjs} +27 -23
- package/scripts/utils/handy_master_secret.mjs +2 -2
- 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/dns.mjs +10 -0
- package/scripts/utils/net/lan_ip.mjs +24 -0
- package/scripts/utils/{ports.mjs → net/ports.mjs} +12 -6
- package/scripts/utils/net/url.mjs +30 -0
- package/scripts/utils/net/url.test.mjs +20 -0
- package/scripts/utils/paths/localhost_host.mjs +56 -0
- package/scripts/utils/{paths.mjs → paths/paths.mjs} +52 -45
- package/scripts/utils/{runtime.mjs → paths/runtime.mjs} +3 -1
- package/scripts/utils/proc/commands.mjs +34 -0
- package/scripts/utils/{ownership.mjs → proc/ownership.mjs} +1 -1
- package/scripts/utils/proc/package_scripts.mjs +31 -0
- package/scripts/utils/proc/parallel.mjs +25 -0
- package/scripts/utils/proc/pids.mjs +11 -0
- package/scripts/utils/{pm.mjs → proc/pm.mjs} +128 -158
- package/scripts/utils/{proc.mjs → proc/proc.mjs} +77 -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/{happy_server_infra.mjs → server/infra/happy_server_infra.mjs} +10 -49
- 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/port.mjs +68 -0
- package/scripts/utils/{server.mjs → server/server.mjs} +12 -0
- package/scripts/utils/server/urls.mjs +101 -0
- package/scripts/utils/server/validate.mjs +88 -0
- package/scripts/utils/service/autostart_darwin.mjs +182 -0
- package/scripts/utils/service/autostart_darwin.test.mjs +50 -0
- package/scripts/utils/stack/context.mjs +23 -0
- package/scripts/utils/stack/dirs.mjs +27 -0
- package/scripts/utils/stack/editor_workspace.mjs +152 -0
- package/scripts/utils/stack/names.mjs +12 -0
- package/scripts/utils/stack/pr_stack_name.mjs +16 -0
- package/scripts/utils/stack/runtime_state.mjs +88 -0
- package/scripts/utils/stack/stacks.mjs +45 -0
- package/scripts/utils/{stack_startup.mjs → stack/startup.mjs} +9 -2
- package/scripts/utils/{stack_stop.mjs → stack/stop.mjs} +24 -19
- package/scripts/utils/stack_context.mjs +3 -3
- package/scripts/utils/stack_runtime_state.mjs +1 -1
- package/scripts/utils/stacks.mjs +2 -2
- package/scripts/utils/{browser.mjs → ui/browser.mjs} +1 -1
- package/scripts/utils/ui/qr.mjs +17 -0
- package/scripts/utils/ui/text.mjs +16 -0
- package/scripts/utils/validate.mjs +1 -1
- package/scripts/where.mjs +6 -6
- package/scripts/worktrees.mjs +171 -113
- package/scripts/utils/auth_sources.mjs +0 -12
- package/scripts/utils/dev_expo_web.mjs +0 -112
- package/scripts/utils/localhost_host.mjs +0 -17
- package/scripts/utils/server_port.mjs +0 -9
- package/scripts/utils/server_urls.mjs +0 -54
- /package/scripts/utils/{sandbox.mjs → env/sandbox.mjs} +0 -0
- /package/scripts/utils/{fs.mjs → fs/fs.mjs} +0 -0
- /package/scripts/utils/{canonical_home.mjs → paths/canonical_home.mjs} +0 -0
- /package/scripts/utils/{watch.mjs → proc/watch.mjs} +0 -0
package/scripts/setup_pr.mjs
CHANGED
|
@@ -1,59 +1,207 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
2
|
import { parseArgs } from './utils/cli/args.mjs';
|
|
3
3
|
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { isTty } from './utils/cli/wizard.mjs';
|
|
5
|
+
import { getVerbosityLevel } from './utils/cli/verbosity.mjs';
|
|
6
|
+
import { createStepPrinter, runCommandLogged } from './utils/cli/progress.mjs';
|
|
7
|
+
import { createFileLogForwarder } from './utils/cli/log_forwarder.mjs';
|
|
8
|
+
import { assertCliPrereqs } from './utils/cli/prereqs.mjs';
|
|
9
|
+
import { decidePrAuthPlan } from './utils/auth/guided_pr_auth.mjs';
|
|
10
|
+
import { assertExpoWebappBundlesOrThrow, guidedStackAuthLoginNow, resolveStackWebappUrlForAuth } from './utils/auth/stack_guided_login.mjs';
|
|
11
|
+
import { preferStackLocalhostUrl } from './utils/paths/localhost_host.mjs';
|
|
12
|
+
import { getRootDir, resolveStackEnvPath } from './utils/paths/paths.mjs';
|
|
13
|
+
import { run } from './utils/proc/proc.mjs';
|
|
14
|
+
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/env/sandbox.mjs';
|
|
15
|
+
import { sanitizeStackName } from './utils/stack/names.mjs';
|
|
16
|
+
import { getStackRuntimeStatePath, readStackRuntimeStateFile } from './utils/stack/runtime_state.mjs';
|
|
17
|
+
import { readEnvObjectFromFile } from './utils/env/read.mjs';
|
|
18
|
+
import { checkDaemonState, startLocalDaemonWithAuth } from './daemon.mjs';
|
|
7
19
|
import { existsSync } from 'node:fs';
|
|
8
|
-
import { homedir } from 'node:os';
|
|
9
20
|
import { join } from 'node:path';
|
|
21
|
+
import { homedir } from 'node:os';
|
|
22
|
+
import { resolveMobileQrPayload } from './utils/mobile/dev_client_links.mjs';
|
|
23
|
+
import { renderQrAscii } from './utils/ui/qr.mjs';
|
|
24
|
+
import { inferPrStackBaseName } from './utils/stack/pr_stack_name.mjs';
|
|
25
|
+
|
|
26
|
+
function supportsAnsi() {
|
|
27
|
+
if (!process.stdout.isTTY) return false;
|
|
28
|
+
if (process.env.NO_COLOR) return false;
|
|
29
|
+
if ((process.env.TERM ?? '').toLowerCase() === 'dumb') return false;
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function bold(s) {
|
|
34
|
+
return supportsAnsi() ? `\x1b[1m${s}\x1b[0m` : String(s);
|
|
35
|
+
}
|
|
10
36
|
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
-
.trim()
|
|
14
|
-
.toLowerCase()
|
|
15
|
-
.replace(/[^a-z0-9-]+/g, '-')
|
|
16
|
-
.replace(/-+/g, '-')
|
|
17
|
-
.replace(/^-+/, '')
|
|
18
|
-
.replace(/-+$/, '');
|
|
19
|
-
return s || 'pr';
|
|
37
|
+
function dim(s) {
|
|
38
|
+
return supportsAnsi() ? `\x1b[2m${s}\x1b[0m` : String(s);
|
|
20
39
|
}
|
|
21
40
|
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return
|
|
41
|
+
function cyan(s) {
|
|
42
|
+
return supportsAnsi() ? `\x1b[36m${s}\x1b[0m` : String(s);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function green(s) {
|
|
46
|
+
return supportsAnsi() ? `\x1b[32m${s}\x1b[0m` : String(s);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function pickReviewerMobileSchemeEnv(env) {
|
|
50
|
+
// For review-pr flows, reviewers typically have the standard Happy dev build on their phone,
|
|
51
|
+
// so default to the canonical `happy://` scheme unless the user explicitly configured one.
|
|
52
|
+
// If the user explicitly set a review-specific override, honor it.
|
|
53
|
+
const reviewOverride = (env.HAPPY_STACKS_REVIEW_MOBILE_SCHEME ?? env.HAPPY_LOCAL_REVIEW_MOBILE_SCHEME ?? '').toString().trim();
|
|
54
|
+
if (reviewOverride) {
|
|
55
|
+
return { ...env, HAPPY_STACKS_MOBILE_SCHEME: reviewOverride, HAPPY_LOCAL_MOBILE_SCHEME: reviewOverride };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// In sandbox review flows, prefer the standard Happy dev build scheme even if the user's global
|
|
59
|
+
// dev-client scheme is configured for Happy Stacks.
|
|
60
|
+
if (isSandboxed()) {
|
|
61
|
+
return { ...env, HAPPY_STACKS_MOBILE_SCHEME: 'happy', HAPPY_LOCAL_MOBILE_SCHEME: 'happy' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Non-sandbox: keep existing behavior unless nothing is configured at all.
|
|
65
|
+
const explicit =
|
|
66
|
+
(env.HAPPY_STACKS_MOBILE_SCHEME ??
|
|
67
|
+
env.HAPPY_LOCAL_MOBILE_SCHEME ??
|
|
68
|
+
env.HAPPY_STACKS_DEV_CLIENT_SCHEME ??
|
|
69
|
+
env.HAPPY_LOCAL_DEV_CLIENT_SCHEME ??
|
|
70
|
+
'')
|
|
71
|
+
.toString()
|
|
72
|
+
.trim();
|
|
73
|
+
if (explicit) return env;
|
|
74
|
+
return { ...env, HAPPY_STACKS_MOBILE_SCHEME: 'happy', HAPPY_LOCAL_MOBILE_SCHEME: 'happy' };
|
|
28
75
|
}
|
|
29
76
|
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
77
|
+
async function printReviewerStackSummary({ rootDir, stackName, env, wantsMobile }) {
|
|
78
|
+
try {
|
|
79
|
+
const runtimeStatePath = getStackRuntimeStatePath(stackName);
|
|
80
|
+
// Wait briefly for Expo metadata to land in stack.runtime.json (it can be published slightly
|
|
81
|
+
// after the server /health check passes, especially after a restart).
|
|
82
|
+
const deadline = Date.now() + 20_000;
|
|
83
|
+
let st = await readStackRuntimeStateFile(runtimeStatePath);
|
|
84
|
+
while (Date.now() < deadline) {
|
|
85
|
+
const hasExpo = Boolean(st?.expo && typeof st.expo === 'object' && Number(st.expo.port) > 0);
|
|
86
|
+
if (hasExpo) break;
|
|
87
|
+
// eslint-disable-next-line no-await-in-loop
|
|
88
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
89
|
+
// eslint-disable-next-line no-await-in-loop
|
|
90
|
+
st = await readStackRuntimeStateFile(runtimeStatePath);
|
|
91
|
+
}
|
|
92
|
+
const baseDir = resolveStackEnvPath(stackName, env).baseDir;
|
|
93
|
+
const envPath = resolveStackEnvPath(stackName, env).envPath;
|
|
94
|
+
|
|
95
|
+
const serverPort = Number(st?.ports?.server);
|
|
96
|
+
const backendPort = Number(st?.ports?.backend);
|
|
97
|
+
const uiPort = Number(st?.expo?.webPort ?? st?.expo?.port);
|
|
98
|
+
const mobilePort = Number(st?.expo?.mobilePort ?? st?.expo?.port);
|
|
99
|
+
const runnerLog = String(st?.logs?.runner ?? '').trim();
|
|
100
|
+
const runnerPid = Number(st?.ownerPid);
|
|
101
|
+
const serverPid = Number(st?.processes?.serverPid);
|
|
102
|
+
const expoPid = Number(st?.processes?.expoPid);
|
|
103
|
+
|
|
104
|
+
const internalServerUrl = Number.isFinite(serverPort) && serverPort > 0 ? `http://127.0.0.1:${serverPort}` : '';
|
|
105
|
+
const uiUrlRaw = Number.isFinite(uiPort) && uiPort > 0 ? `http://localhost:${uiPort}` : '';
|
|
106
|
+
const uiUrl = uiUrlRaw ? await preferStackLocalhostUrl(uiUrlRaw, { stackName, env }) : '';
|
|
107
|
+
|
|
108
|
+
// eslint-disable-next-line no-console
|
|
109
|
+
console.log('');
|
|
110
|
+
// eslint-disable-next-line no-console
|
|
111
|
+
console.log(bold('Review details'));
|
|
112
|
+
// eslint-disable-next-line no-console
|
|
113
|
+
console.log(`${dim('Stack:')} ${cyan(stackName)}`);
|
|
114
|
+
// eslint-disable-next-line no-console
|
|
115
|
+
console.log(`${dim('Env:')} ${envPath}`);
|
|
116
|
+
// eslint-disable-next-line no-console
|
|
117
|
+
console.log(`${dim('Dir:')} ${baseDir}`);
|
|
118
|
+
if (Number.isFinite(runnerPid) && runnerPid > 1) {
|
|
119
|
+
// eslint-disable-next-line no-console
|
|
120
|
+
console.log(`${dim('Runner:')} pid=${runnerPid}${Number.isFinite(serverPid) && serverPid > 1 ? ` serverPid=${serverPid}` : ''}${Number.isFinite(expoPid) && expoPid > 1 ? ` expoPid=${expoPid}` : ''}`);
|
|
121
|
+
}
|
|
122
|
+
if (runnerLog) {
|
|
123
|
+
// eslint-disable-next-line no-console
|
|
124
|
+
console.log(`${dim('Logs:')} ${runnerLog}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// eslint-disable-next-line no-console
|
|
128
|
+
console.log('');
|
|
129
|
+
// eslint-disable-next-line no-console
|
|
130
|
+
console.log(bold('Ports'));
|
|
131
|
+
if (Number.isFinite(serverPort) && serverPort > 0) {
|
|
132
|
+
// eslint-disable-next-line no-console
|
|
133
|
+
console.log(`- ${dim('server')}: ${serverPort}${internalServerUrl ? ` (${internalServerUrl})` : ''}`);
|
|
134
|
+
}
|
|
135
|
+
if (Number.isFinite(backendPort) && backendPort > 0) {
|
|
136
|
+
// eslint-disable-next-line no-console
|
|
137
|
+
console.log(`- ${dim('backend')}: ${backendPort}`);
|
|
138
|
+
}
|
|
139
|
+
if (Number.isFinite(uiPort) && uiPort > 0) {
|
|
140
|
+
// eslint-disable-next-line no-console
|
|
141
|
+
console.log(`- ${dim('web UI')}: ${uiPort}${uiUrl ? ` (${uiUrl})` : ''}`);
|
|
142
|
+
}
|
|
143
|
+
if (wantsMobile && Number.isFinite(mobilePort) && mobilePort > 0) {
|
|
144
|
+
// eslint-disable-next-line no-console
|
|
145
|
+
console.log(`- ${dim('mobile')}: ${mobilePort} (Metro)`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Prefer the Metro port recorded by Expo; fall back to the web UI port if needed.
|
|
149
|
+
const metroPort = Number.isFinite(mobilePort) && mobilePort > 0 ? mobilePort : Number.isFinite(uiPort) && uiPort > 0 ? uiPort : null;
|
|
150
|
+
|
|
151
|
+
if (wantsMobile && Number.isFinite(metroPort) && metroPort > 0) {
|
|
152
|
+
const payload = resolveMobileQrPayload({ env, port: metroPort });
|
|
153
|
+
const qr = await renderQrAscii(payload.payload, { small: true });
|
|
154
|
+
|
|
155
|
+
// eslint-disable-next-line no-console
|
|
156
|
+
console.log('');
|
|
157
|
+
// eslint-disable-next-line no-console
|
|
158
|
+
console.log(bold('Mobile (Expo dev-client)'));
|
|
159
|
+
if (payload.metroUrl) {
|
|
160
|
+
// eslint-disable-next-line no-console
|
|
161
|
+
console.log(`- ${dim('Metro')}: ${payload.metroUrl}`);
|
|
162
|
+
}
|
|
163
|
+
if (payload.scheme) {
|
|
164
|
+
// eslint-disable-next-line no-console
|
|
165
|
+
console.log(`- ${dim('Scheme')}: ${payload.scheme}://`);
|
|
166
|
+
}
|
|
167
|
+
if (payload.deepLink) {
|
|
168
|
+
// eslint-disable-next-line no-console
|
|
169
|
+
console.log(`- ${dim('Link')}: ${payload.deepLink}`);
|
|
170
|
+
}
|
|
171
|
+
if (qr.ok && qr.lines.length) {
|
|
172
|
+
// eslint-disable-next-line no-console
|
|
173
|
+
console.log('');
|
|
174
|
+
// eslint-disable-next-line no-console
|
|
175
|
+
console.log(bold('Scan this QR code with your Happy dev build:'));
|
|
176
|
+
// eslint-disable-next-line no-console
|
|
177
|
+
console.log(qr.lines.join('\n'));
|
|
178
|
+
} else if (!qr.ok) {
|
|
179
|
+
// eslint-disable-next-line no-console
|
|
180
|
+
console.log(dim(`(QR unavailable: ${qr.error || 'unknown error'})`));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// eslint-disable-next-line no-console
|
|
185
|
+
console.log('');
|
|
186
|
+
// eslint-disable-next-line no-console
|
|
187
|
+
console.log(green('✓ Ready'));
|
|
188
|
+
// eslint-disable-next-line no-console
|
|
189
|
+
console.log(dim('Tip: press Ctrl+C when you’re done to stop the stack and clean up the sandbox.'));
|
|
190
|
+
} catch {
|
|
191
|
+
// best-effort
|
|
192
|
+
}
|
|
41
193
|
}
|
|
42
194
|
|
|
43
195
|
function detectBestAuthSource() {
|
|
44
196
|
const devAuthEnvExists = existsSync(resolveStackEnvPath('dev-auth').envPath);
|
|
45
197
|
const devAuthAccessKey = join(resolveStackEnvPath('dev-auth').baseDir, 'cli', 'access.key');
|
|
46
198
|
const mainAccessKey = join(resolveStackEnvPath('main').baseDir, 'cli', 'access.key');
|
|
47
|
-
const allowGlobal = sandboxAllowsGlobalSideEffects();
|
|
48
|
-
const legacyAccessKey = join(homedir(), '.happy', 'cli', 'access.key');
|
|
49
199
|
|
|
50
200
|
const hasDevAuth = devAuthEnvExists && existsSync(devAuthAccessKey);
|
|
51
201
|
const hasMain = existsSync(mainAccessKey);
|
|
52
|
-
const hasLegacy = (!isSandboxed() || allowGlobal) && existsSync(legacyAccessKey);
|
|
53
202
|
|
|
54
203
|
if (hasDevAuth) return { from: 'dev-auth', hasAny: true };
|
|
55
204
|
if (hasMain) return { from: 'main', hasAny: true };
|
|
56
|
-
if (hasLegacy) return { from: 'legacy', hasAny: true };
|
|
57
205
|
return { from: 'main', hasAny: false };
|
|
58
206
|
}
|
|
59
207
|
|
|
@@ -79,13 +227,16 @@ async function main() {
|
|
|
79
227
|
|
|
80
228
|
const { flags, kv } = parseArgs(argv);
|
|
81
229
|
const json = wantsJson(argv, { flags });
|
|
230
|
+
const interactive = isTty() && !json;
|
|
231
|
+
const verbosity = getVerbosityLevel(process.env);
|
|
232
|
+
const quietUi = interactive && verbosity === 0;
|
|
82
233
|
|
|
83
234
|
if (wantsHelp(argv, { flags })) {
|
|
84
235
|
printResult({
|
|
85
236
|
json,
|
|
86
237
|
data: {
|
|
87
238
|
usage:
|
|
88
|
-
'happys setup-pr --happy=<pr-url|number> [--happy-cli=<pr-url|number>] [--happy-server=<pr-url|number>|--happy-server-light=<pr-url|number>] [--name=<stack>] [--dev|--start] [--seed-auth|--no-seed-auth] [--copy-auth-from=<stack
|
|
239
|
+
'happys setup-pr --happy=<pr-url|number> [--happy-cli=<pr-url|number>] [--happy-server=<pr-url|number>|--happy-server-light=<pr-url|number>] [--name=<stack>] [--dev|--start] [--mobile] [--deps=none|link|install|link-or-install] [--forks|--upstream] [--seed-auth|--no-seed-auth] [--copy-auth-from=<stack>] [--link-auth|--copy-auth] [--update] [--force] [--json] [-- <stack dev/start args...>]',
|
|
89
240
|
},
|
|
90
241
|
text: [
|
|
91
242
|
'[setup-pr] usage:',
|
|
@@ -96,7 +247,7 @@ async function main() {
|
|
|
96
247
|
'- ensures happy-stacks home exists (init)',
|
|
97
248
|
'- bootstraps/clones missing components (upstream by default)',
|
|
98
249
|
'- creates or reuses a PR stack and checks out PR worktrees',
|
|
99
|
-
'- optionally seeds auth (best available source: dev-auth → main
|
|
250
|
+
'- optionally seeds auth (best available source: dev-auth → main)',
|
|
100
251
|
'- starts the stack (dev by default)',
|
|
101
252
|
'',
|
|
102
253
|
'Updating when the PR changes:',
|
|
@@ -112,6 +263,8 @@ async function main() {
|
|
|
112
263
|
return;
|
|
113
264
|
}
|
|
114
265
|
|
|
266
|
+
await assertCliPrereqs({ git: true, pnpm: true });
|
|
267
|
+
|
|
115
268
|
const prHappy = (kv.get('--happy') ?? '').trim();
|
|
116
269
|
const prCli = (kv.get('--happy-cli') ?? '').trim();
|
|
117
270
|
const prServer = (kv.get('--happy-server') ?? '').trim();
|
|
@@ -128,33 +281,190 @@ async function main() {
|
|
|
128
281
|
if (wantsDev && wantsStart) {
|
|
129
282
|
throw new Error('[setup-pr] choose either --dev or --start (not both)');
|
|
130
283
|
}
|
|
284
|
+
const repoSourceFlag = flags.has('--upstream') ? '--upstream' : flags.has('--forks') ? '--forks' : null;
|
|
285
|
+
const wantsMobile = (flags.has('--mobile') || flags.has('--with-mobile')) && !flags.has('--no-mobile');
|
|
286
|
+
// Worktree dependency strategy:
|
|
287
|
+
// - For dev flows (review-pr/setup-pr), prefer reusing base checkout node_modules to avoid reinstalling in worktrees.
|
|
288
|
+
// - Allow override via --deps=none|link|install|link-or-install.
|
|
289
|
+
const depsModeArg = (kv.get('--deps') ?? '').trim();
|
|
290
|
+
const depsMode = depsModeArg || (wantsDev ? 'link-or-install' : 'none');
|
|
131
291
|
|
|
132
292
|
const stackNameRaw = (kv.get('--name') ?? '').trim();
|
|
133
|
-
const stackName = stackNameRaw
|
|
293
|
+
const stackName = stackNameRaw
|
|
294
|
+
? sanitizeStackName(stackNameRaw)
|
|
295
|
+
: inferPrStackBaseName({ happy: prHappy, happyCli: prCli, server: prServer, serverLight: prServerLight, fallback: 'pr' });
|
|
134
296
|
|
|
135
297
|
// Determine server flavor for bootstrap and stack creation.
|
|
136
298
|
const serverComponent = (kv.get('--server') ?? '').trim() || (prServer ? 'happy-server' : 'happy-server-light');
|
|
137
299
|
const bootstrapServer = prServer || serverComponent === 'happy-server' ? 'both' : 'happy-server-light';
|
|
138
300
|
|
|
139
301
|
// Auth defaults (avoid prompts; setup-pr should be low-friction).
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
302
|
+
// Note: these may be updated below (sandbox prompt), so keep them mutable.
|
|
303
|
+
let seedAuthFlag = flags.has('--seed-auth') ? true : flags.has('--no-seed-auth') ? false : null;
|
|
304
|
+
let authFrom = (kv.get('--copy-auth-from') ?? '').trim();
|
|
305
|
+
let linkAuth = flags.has('--link-auth') ? true : flags.has('--copy-auth') ? false : null;
|
|
306
|
+
|
|
307
|
+
// Disallow "legacy" auth seeding in setup-pr flows:
|
|
308
|
+
// We can't reliably seed local DB Account rows from a remote/production Happy install,
|
|
309
|
+
// so this leads to broken stacks. Use guided login instead.
|
|
310
|
+
if (authFrom && authFrom.toLowerCase() === 'legacy') {
|
|
311
|
+
throw new Error('[setup-pr] --copy-auth-from=legacy is not supported. Use guided login (no seeding) instead.');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Re-read flags after optional prompt mutation.
|
|
315
|
+
seedAuthFlag = flags.has('--seed-auth') ? true : flags.has('--no-seed-auth') ? false : null;
|
|
316
|
+
authFrom = (kv.get('--copy-auth-from') ?? '').trim();
|
|
317
|
+
linkAuth = flags.has('--link-auth') ? true : flags.has('--copy-auth') ? false : null;
|
|
318
|
+
|
|
319
|
+
// If this PR stack already has credentials, do not prompt or override it.
|
|
320
|
+
const stackAlreadyAuthed = (() => {
|
|
321
|
+
try {
|
|
322
|
+
const { baseDir, envPath } = resolveStackEnvPath(stackName);
|
|
323
|
+
if (!existsSync(envPath)) return false;
|
|
324
|
+
return existsSync(join(baseDir, 'cli', 'access.key'));
|
|
325
|
+
} catch {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
})();
|
|
329
|
+
|
|
330
|
+
// Centralized guided auth decision (prompt early, before noisy install logs).
|
|
331
|
+
// In non-sandbox mode we still guide: offer reusing dev-auth/main first, otherwise guided login.
|
|
332
|
+
const sandboxNoGlobal = isSandboxed() && !sandboxAllowsGlobalSideEffects();
|
|
333
|
+
if (sandboxNoGlobal && (seedAuthFlag === true || authFrom)) {
|
|
334
|
+
throw new Error(
|
|
335
|
+
'[setup-pr] auth seeding is disabled in sandbox mode.\n' +
|
|
336
|
+
'Reason: it reuses global machine state (other stacks) and breaks sandbox isolation.\n' +
|
|
337
|
+
'Use guided login instead, or set: HAPPY_STACKS_SANDBOX_ALLOW_GLOBAL=1'
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
let plan = stackAlreadyAuthed
|
|
342
|
+
? { mode: 'existing' }
|
|
343
|
+
: await decidePrAuthPlan({
|
|
344
|
+
interactive,
|
|
345
|
+
seedAuthFlag,
|
|
346
|
+
explicitFrom: authFrom,
|
|
347
|
+
defaultLoginNow: true,
|
|
348
|
+
});
|
|
349
|
+
if (sandboxNoGlobal && plan?.mode === 'seed') {
|
|
350
|
+
// Keep sandbox runs isolated by default.
|
|
351
|
+
plan = { mode: 'login', loginNow: true, reason: 'sandbox_no_global' };
|
|
352
|
+
}
|
|
143
353
|
|
|
144
354
|
const best = detectBestAuthSource();
|
|
145
|
-
const effectiveSeedAuth =
|
|
146
|
-
|
|
147
|
-
|
|
355
|
+
const effectiveSeedAuth =
|
|
356
|
+
plan.mode === 'existing'
|
|
357
|
+
? false
|
|
358
|
+
: plan.mode === 'seed'
|
|
359
|
+
? true
|
|
360
|
+
: plan.mode === 'login'
|
|
361
|
+
? false
|
|
362
|
+
: seedAuthFlag != null
|
|
363
|
+
? seedAuthFlag
|
|
364
|
+
: best.hasAny;
|
|
365
|
+
const effectiveAuthFrom = plan.mode === 'seed' ? plan.from : authFrom || best.from;
|
|
366
|
+
const effectiveLinkAuth = plan.mode === 'seed' ? Boolean(plan.link) : linkAuth != null ? linkAuth : detectLinkDefault();
|
|
148
367
|
|
|
149
|
-
//
|
|
150
|
-
|
|
368
|
+
// Sandbox default: no cross-stack auth reuse unless explicitly allowed.
|
|
369
|
+
const sandboxEffectiveSeedAuth = sandboxNoGlobal ? false : effectiveSeedAuth;
|
|
151
370
|
|
|
371
|
+
// If we're going to guide the user through login, start in background first (even in verbose mode)
|
|
372
|
+
// so auth prompts aren't buried in runner logs.
|
|
373
|
+
const needsAuthFlow = interactive && !stackAlreadyAuthed && !sandboxEffectiveSeedAuth && plan.mode === 'login' && plan.loginNow;
|
|
374
|
+
let stackStartEnv = needsAuthFlow
|
|
375
|
+
? {
|
|
376
|
+
...process.env,
|
|
377
|
+
// Hint to the dev runner that it should start the Expo web UI early (before daemon auth),
|
|
378
|
+
// so guided login can open the correct UI origin (not the server port).
|
|
379
|
+
HAPPY_STACKS_AUTH_FLOW: '1',
|
|
380
|
+
HAPPY_LOCAL_AUTH_FLOW: '1',
|
|
381
|
+
}
|
|
382
|
+
: process.env;
|
|
383
|
+
if (wantsMobile) {
|
|
384
|
+
stackStartEnv = pickReviewerMobileSchemeEnv(stackStartEnv);
|
|
385
|
+
}
|
|
386
|
+
// (No extra messaging here; review-pr prints the up-front explanation + enter-to-proceed gate.)
|
|
387
|
+
|
|
388
|
+
// 1) Ensure happy-stacks home is initialized (idempotent).
|
|
152
389
|
// 2) Bootstrap component repos and deps (idempotent; clones only if missing).
|
|
153
|
-
|
|
390
|
+
if (quietUi) {
|
|
391
|
+
const baseLogDir = join(process.env.HAPPY_STACKS_HOME_DIR ?? join(homedir(), '.happy-stacks'), 'logs', 'setup-pr');
|
|
392
|
+
const initLog = join(baseLogDir, `init.${Date.now()}.log`);
|
|
393
|
+
const installLog = join(baseLogDir, `install.${Date.now()}.log`);
|
|
394
|
+
try {
|
|
395
|
+
await runCommandLogged({
|
|
396
|
+
label: `init happy-stacks home${isSandboxed() ? ' (sandbox)' : ''}`,
|
|
397
|
+
cmd: process.execPath,
|
|
398
|
+
args: [join(rootDir, 'scripts', 'init.mjs'), '--no-bootstrap'],
|
|
399
|
+
cwd: rootDir,
|
|
400
|
+
env: process.env,
|
|
401
|
+
logPath: initLog,
|
|
402
|
+
quiet: true,
|
|
403
|
+
showSteps: true,
|
|
404
|
+
});
|
|
405
|
+
await runCommandLogged({
|
|
406
|
+
label: `install/clone components${isSandboxed() ? ' (sandbox)' : ''}`,
|
|
407
|
+
cmd: process.execPath,
|
|
408
|
+
args: [
|
|
409
|
+
join(rootDir, 'scripts', 'install.mjs'),
|
|
410
|
+
...(repoSourceFlag ? [repoSourceFlag] : []),
|
|
411
|
+
'--clone',
|
|
412
|
+
`--server=${bootstrapServer}`,
|
|
413
|
+
...(wantsDev ? ['--no-ui-build'] : []),
|
|
414
|
+
// Sandbox dev: avoid wasting time installing base deps we won't run directly.
|
|
415
|
+
...(isSandboxed() && wantsDev ? ['--no-ui-deps'] : []),
|
|
416
|
+
// If the caller provided a happy-cli PR, the PR stack is guaranteed (fail-closed) to pin
|
|
417
|
+
// HAPPY_STACKS_COMPONENT_DIR_HAPPY_CLI to that worktree before starting dev, so building the
|
|
418
|
+
// base checkout is wasted work.
|
|
419
|
+
...(isSandboxed() && wantsDev && prCli ? ['--no-cli-deps', '--no-cli-build'] : []),
|
|
420
|
+
],
|
|
421
|
+
cwd: rootDir,
|
|
422
|
+
env: process.env,
|
|
423
|
+
logPath: installLog,
|
|
424
|
+
quiet: true,
|
|
425
|
+
showSteps: true,
|
|
426
|
+
});
|
|
427
|
+
} catch (e) {
|
|
428
|
+
const logPath = e?.logPath ? String(e.logPath) : null;
|
|
429
|
+
console.error('[setup-pr] failed during setup.');
|
|
430
|
+
if (logPath) {
|
|
431
|
+
console.error(`[setup-pr] log: ${logPath}`);
|
|
432
|
+
}
|
|
433
|
+
if (e?.stderr) {
|
|
434
|
+
console.error(String(e.stderr).trim());
|
|
435
|
+
} else if (e instanceof Error) {
|
|
436
|
+
console.error(e.message);
|
|
437
|
+
} else {
|
|
438
|
+
console.error(String(e));
|
|
439
|
+
}
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
} else {
|
|
443
|
+
await runNodeScript({ rootDir, rel: 'scripts/init.mjs', args: ['--no-bootstrap'] });
|
|
444
|
+
await runNodeScript({
|
|
445
|
+
rootDir,
|
|
446
|
+
rel: 'scripts/install.mjs',
|
|
447
|
+
args: [
|
|
448
|
+
...(repoSourceFlag ? [repoSourceFlag] : []),
|
|
449
|
+
'--clone',
|
|
450
|
+
`--server=${bootstrapServer}`,
|
|
451
|
+
...(wantsDev ? ['--no-ui-build'] : []),
|
|
452
|
+
...(isSandboxed() && wantsDev ? ['--no-ui-deps'] : []),
|
|
453
|
+
...(isSandboxed() && wantsDev && prCli ? ['--no-cli-deps', '--no-cli-build'] : []),
|
|
454
|
+
],
|
|
455
|
+
});
|
|
456
|
+
}
|
|
154
457
|
|
|
155
458
|
// 3) Create/reuse the PR stack and wire worktrees.
|
|
459
|
+
// Start Expo with all requested capabilities from the beginning to avoid stop/restart churn.
|
|
460
|
+
const startMobileNow = wantsMobile;
|
|
461
|
+
const userDisabledDaemon = forwarded.includes('--no-daemon');
|
|
462
|
+
const forwardedEffective =
|
|
463
|
+
needsAuthFlow && !userDisabledDaemon && !forwarded.includes('--no-daemon')
|
|
464
|
+
? [...forwarded, '--no-daemon']
|
|
465
|
+
: forwarded;
|
|
466
|
+
const injectedNoDaemon = needsAuthFlow && !userDisabledDaemon && forwardedEffective.includes('--no-daemon');
|
|
156
467
|
const stackArgs = [
|
|
157
|
-
'stack',
|
|
158
468
|
'pr',
|
|
159
469
|
stackName,
|
|
160
470
|
...(prHappy ? [`--happy=${prHappy}`] : []),
|
|
@@ -163,16 +473,246 @@ async function main() {
|
|
|
163
473
|
...(prServerLight ? [`--happy-server-light=${prServerLight}`] : []),
|
|
164
474
|
`--server=${serverComponent}`,
|
|
165
475
|
'--reuse',
|
|
476
|
+
...(depsMode ? [`--deps=${depsMode}`] : []),
|
|
166
477
|
...(flags.has('--update') ? ['--update'] : []),
|
|
167
478
|
...(flags.has('--force') ? ['--force'] : []),
|
|
168
|
-
...(
|
|
479
|
+
...(sandboxEffectiveSeedAuth
|
|
480
|
+
? ['--seed-auth', `--copy-auth-from=${effectiveAuthFrom}`, ...(effectiveLinkAuth ? ['--link-auth'] : [])]
|
|
481
|
+
: ['--no-seed-auth']),
|
|
169
482
|
...(wantsDev ? ['--dev'] : ['--start']),
|
|
483
|
+
...(startMobileNow ? ['--mobile'] : []),
|
|
484
|
+
...(((quietUi && !json) || needsAuthFlow) ? ['--background'] : []),
|
|
170
485
|
...(json ? ['--json'] : []),
|
|
171
486
|
];
|
|
172
|
-
if (
|
|
173
|
-
stackArgs.push('--', ...
|
|
487
|
+
if (forwardedEffective.length) {
|
|
488
|
+
stackArgs.push('--', ...forwardedEffective);
|
|
489
|
+
}
|
|
490
|
+
if (quietUi) {
|
|
491
|
+
const baseLogDir = join(process.env.HAPPY_STACKS_HOME_DIR ?? join(homedir(), '.happy-stacks'), 'logs', 'setup-pr');
|
|
492
|
+
const stackLog = join(baseLogDir, `stack-pr.${Date.now()}.log`);
|
|
493
|
+
await runCommandLogged({
|
|
494
|
+
label: `start PR stack${isSandboxed() ? ' (sandbox)' : ''}`,
|
|
495
|
+
cmd: process.execPath,
|
|
496
|
+
args: [join(rootDir, 'scripts', 'stack.mjs'), ...stackArgs],
|
|
497
|
+
cwd: rootDir,
|
|
498
|
+
env: stackStartEnv,
|
|
499
|
+
logPath: stackLog,
|
|
500
|
+
quiet: true,
|
|
501
|
+
showSteps: true,
|
|
502
|
+
}).catch((e) => {
|
|
503
|
+
const logPath = e?.logPath ? String(e.logPath) : stackLog;
|
|
504
|
+
console.error('[setup-pr] failed to start PR stack.');
|
|
505
|
+
console.error(`[setup-pr] log: ${logPath}`);
|
|
506
|
+
process.exit(1);
|
|
507
|
+
});
|
|
508
|
+
} else {
|
|
509
|
+
await runNodeScript({ rootDir, rel: 'scripts/stack.mjs', args: stackArgs, env: stackStartEnv });
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Sandbox UX: if we won't run the guided login flow, explicitly tell the user we're now in "keepalive"
|
|
513
|
+
// mode and how to exit/cleanup. Otherwise it can look like the command "hung".
|
|
514
|
+
if (isSandboxed() && interactive && !json && !needsAuthFlow) {
|
|
515
|
+
// eslint-disable-next-line no-console
|
|
516
|
+
console.log('');
|
|
517
|
+
// eslint-disable-next-line no-console
|
|
518
|
+
console.log('[setup-pr] Stack is running in the sandbox.');
|
|
519
|
+
// eslint-disable-next-line no-console
|
|
520
|
+
console.log('[setup-pr] Press Ctrl+C when you’re done to stop and delete the sandbox.');
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Guided auth flow:
|
|
524
|
+
// If the user chose "login now", we start in background (quiet mode) then perform login in the foreground.
|
|
525
|
+
// Sandbox: keep this process alive so review-pr can clean up on exit.
|
|
526
|
+
// Non-sandbox: after login, restart dev/start in the foreground so logs follow as usual.
|
|
527
|
+
if (needsAuthFlow) {
|
|
528
|
+
// eslint-disable-next-line no-console
|
|
529
|
+
console.log('');
|
|
530
|
+
if (interactive) {
|
|
531
|
+
// In verbose mode, tail the runner log so users can debug Expo/auth issues,
|
|
532
|
+
// but pause forwarding during the guided login prompts (keeps instructions readable).
|
|
533
|
+
let forwarder = null;
|
|
534
|
+
if (!json && verbosity > 0) {
|
|
535
|
+
try {
|
|
536
|
+
const runtimeStatePath = getStackRuntimeStatePath(stackName);
|
|
537
|
+
const deadline = Date.now() + 10_000;
|
|
538
|
+
let logPath = '';
|
|
539
|
+
while (Date.now() < deadline) {
|
|
540
|
+
// eslint-disable-next-line no-await-in-loop
|
|
541
|
+
const st = await readStackRuntimeStateFile(runtimeStatePath);
|
|
542
|
+
logPath = String(st?.logs?.runner ?? '').trim();
|
|
543
|
+
if (logPath) break;
|
|
544
|
+
// eslint-disable-next-line no-await-in-loop
|
|
545
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
546
|
+
}
|
|
547
|
+
if (logPath) {
|
|
548
|
+
forwarder = createFileLogForwarder({
|
|
549
|
+
path: logPath,
|
|
550
|
+
enabled: true,
|
|
551
|
+
label: 'stack',
|
|
552
|
+
startFromEnd: false,
|
|
553
|
+
});
|
|
554
|
+
await forwarder.start();
|
|
555
|
+
}
|
|
556
|
+
} catch {
|
|
557
|
+
forwarder = null;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const steps = createStepPrinter({ enabled: Boolean(process.stdout.isTTY && !json) });
|
|
562
|
+
const label = 'prepare login (waiting for web UI)';
|
|
563
|
+
steps.start(label);
|
|
564
|
+
let webappUrl = '';
|
|
565
|
+
try {
|
|
566
|
+
// Use the same env overlay we used to start the stack in background (includes auth-flow markers).
|
|
567
|
+
webappUrl = await resolveStackWebappUrlForAuth({ rootDir, stackName, env: stackStartEnv });
|
|
568
|
+
// This can take a moment (first bundle compile / resolver errors).
|
|
569
|
+
await assertExpoWebappBundlesOrThrow({ rootDir, stackName, webappUrl });
|
|
570
|
+
steps.stop('✓', label);
|
|
571
|
+
} catch (e) {
|
|
572
|
+
// For guided login, failing to resolve the UI origin should fail closed (server URL fallback is misleading).
|
|
573
|
+
steps.stop('x', label);
|
|
574
|
+
try {
|
|
575
|
+
await forwarder?.stop();
|
|
576
|
+
} catch {
|
|
577
|
+
// ignore
|
|
578
|
+
}
|
|
579
|
+
throw e;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
try {
|
|
583
|
+
forwarder?.pause();
|
|
584
|
+
// We've already checked the web UI bundle above; skip repeating it here.
|
|
585
|
+
await guidedStackAuthLoginNow({
|
|
586
|
+
rootDir,
|
|
587
|
+
stackName,
|
|
588
|
+
env: { ...stackStartEnv, HAPPY_STACKS_AUTH_SKIP_BUNDLE_CHECK: '1', HAPPY_LOCAL_AUTH_SKIP_BUNDLE_CHECK: '1' },
|
|
589
|
+
webappUrl,
|
|
590
|
+
});
|
|
591
|
+
} finally {
|
|
592
|
+
try {
|
|
593
|
+
forwarder?.resume();
|
|
594
|
+
} catch {
|
|
595
|
+
// ignore
|
|
596
|
+
}
|
|
597
|
+
try {
|
|
598
|
+
await forwarder?.stop();
|
|
599
|
+
} catch {
|
|
600
|
+
// ignore
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
// `guidedStackAuthLoginNow` already ran `stack auth <name> login` in interactive mode.
|
|
605
|
+
if (!interactive) {
|
|
606
|
+
await runNodeScript({ rootDir, rel: 'scripts/stack.mjs', args: ['auth', stackName, '--', 'login'] });
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// After guided login, start daemon now (unless the user explicitly disabled it).
|
|
610
|
+
// This ensures the machine is registered and appears in the UI.
|
|
611
|
+
if (injectedNoDaemon && !userDisabledDaemon) {
|
|
612
|
+
const steps = createStepPrinter({ enabled: Boolean(process.stdout.isTTY && !json) });
|
|
613
|
+
const label = 'start daemon (post-auth)';
|
|
614
|
+
steps.start(label);
|
|
615
|
+
try {
|
|
616
|
+
const { envPath, baseDir } = resolveStackEnvPath(stackName, stackStartEnv);
|
|
617
|
+
const stackEnv = await readEnvObjectFromFile(envPath);
|
|
618
|
+
const mergedEnv = { ...process.env, ...stackEnv };
|
|
619
|
+
|
|
620
|
+
const cliHomeDir =
|
|
621
|
+
(mergedEnv.HAPPY_STACKS_CLI_HOME_DIR ?? mergedEnv.HAPPY_LOCAL_CLI_HOME_DIR ?? '').toString().trim() ||
|
|
622
|
+
join(baseDir, 'cli');
|
|
623
|
+
const cliDir =
|
|
624
|
+
(mergedEnv.HAPPY_STACKS_COMPONENT_DIR_HAPPY_CLI ?? mergedEnv.HAPPY_LOCAL_COMPONENT_DIR_HAPPY_CLI ?? '').toString().trim();
|
|
625
|
+
if (!cliDir) {
|
|
626
|
+
throw new Error('[setup-pr] post-auth daemon start failed: HAPPY_STACKS_COMPONENT_DIR_HAPPY_CLI is not set');
|
|
627
|
+
}
|
|
628
|
+
const cliBin = join(cliDir, 'bin', 'happy.mjs');
|
|
629
|
+
|
|
630
|
+
const runtimeStatePath = getStackRuntimeStatePath(stackName);
|
|
631
|
+
const st = await readStackRuntimeStateFile(runtimeStatePath);
|
|
632
|
+
const serverPort = Number(st?.ports?.server);
|
|
633
|
+
if (!Number.isFinite(serverPort) || serverPort <= 0) {
|
|
634
|
+
throw new Error('[setup-pr] post-auth daemon start failed: could not resolve server port from stack.runtime.json');
|
|
635
|
+
}
|
|
636
|
+
const internalServerUrl = `http://127.0.0.1:${serverPort}`;
|
|
637
|
+
const publicServerUrl = internalServerUrl;
|
|
638
|
+
|
|
639
|
+
await startLocalDaemonWithAuth({
|
|
640
|
+
cliBin,
|
|
641
|
+
cliHomeDir,
|
|
642
|
+
internalServerUrl,
|
|
643
|
+
publicServerUrl,
|
|
644
|
+
isShuttingDown: () => false,
|
|
645
|
+
forceRestart: true,
|
|
646
|
+
env: mergedEnv,
|
|
647
|
+
stackName,
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
// Verify: daemon wrote state (best-effort wait).
|
|
651
|
+
const deadline = Date.now() + 10_000;
|
|
652
|
+
while (Date.now() < deadline) {
|
|
653
|
+
const s = checkDaemonState(cliHomeDir);
|
|
654
|
+
if (s.status === 'running') break;
|
|
655
|
+
// eslint-disable-next-line no-await-in-loop
|
|
656
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
657
|
+
}
|
|
658
|
+
steps.stop('✓', label);
|
|
659
|
+
} catch (e) {
|
|
660
|
+
steps.stop('x', label);
|
|
661
|
+
throw e;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (isSandboxed()) {
|
|
666
|
+
// Fall through to sandbox keepalive below.
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Re-attach logs in the foreground for the chosen mode.
|
|
670
|
+
const restartArgs = [
|
|
671
|
+
wantsDev ? 'dev' : 'start',
|
|
672
|
+
stackName,
|
|
673
|
+
'--restart',
|
|
674
|
+
...(wantsMobile ? ['--mobile'] : []),
|
|
675
|
+
...(forwarded.length ? ['--', ...forwarded] : []),
|
|
676
|
+
];
|
|
677
|
+
// If the user explicitly asked for verbose, reattach; otherwise keep things quiet.
|
|
678
|
+
if (verbosity > 0) {
|
|
679
|
+
await runNodeScript({ rootDir, rel: 'scripts/stack.mjs', args: restartArgs });
|
|
680
|
+
}
|
|
681
|
+
// Mobile is started up-front (in the initial stack pr start) so we don't need to restart here.
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// After login (and after the optional mobile Metro start), print a clear summary so reviewers
|
|
685
|
+
// have everything they need (URLs/ports/logs + QR) without needing verbose logs.
|
|
686
|
+
if (interactive && !json) {
|
|
687
|
+
await printReviewerStackSummary({ rootDir, stackName, env: stackStartEnv, wantsMobile });
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Sandbox: keep this process alive so review-pr stays running and can clean up on exit.
|
|
691
|
+
// The stack runner continues in the background; `review-pr` will stop it on Ctrl+C.
|
|
692
|
+
//
|
|
693
|
+
// IMPORTANT:
|
|
694
|
+
// Waiting on a Promise that only resolves on signals is NOT enough to keep Node alive; pending
|
|
695
|
+
// Promises and signal handlers do not keep the event loop open. We must keep a ref'd handle.
|
|
696
|
+
if (isSandboxed() && interactive && !json) {
|
|
697
|
+
// eslint-disable-next-line no-console
|
|
698
|
+
console.log('');
|
|
699
|
+
// eslint-disable-next-line no-console
|
|
700
|
+
console.log('[setup-pr] Stack is running in the sandbox.');
|
|
701
|
+
// eslint-disable-next-line no-console
|
|
702
|
+
console.log('[setup-pr] Press Ctrl+C when you’re done to stop and delete the sandbox.');
|
|
703
|
+
|
|
704
|
+
await new Promise((resolvePromise) => {
|
|
705
|
+
const interval = setInterval(() => {}, 1_000);
|
|
706
|
+
const done = () => {
|
|
707
|
+
clearInterval(interval);
|
|
708
|
+
process.off('SIGINT', done);
|
|
709
|
+
process.off('SIGTERM', done);
|
|
710
|
+
resolvePromise();
|
|
711
|
+
};
|
|
712
|
+
process.on('SIGINT', done);
|
|
713
|
+
process.on('SIGTERM', done);
|
|
714
|
+
});
|
|
174
715
|
}
|
|
175
|
-
await runNodeScript({ rootDir, rel: 'scripts/stack.mjs', args: stackArgs });
|
|
176
716
|
}
|
|
177
717
|
|
|
178
718
|
main().catch((err) => {
|