stim 1.0.0 → 1.1.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 +22 -10
- package/dist/android-CC5bUer7.mjs +204 -0
- package/dist/android-DnZlT3aU.mjs +2279 -0
- package/dist/android-cas-DGlTStWu.mjs +1078 -0
- package/dist/android-cas-compiler.mjs +1 -1
- package/dist/android-xmyGBNy5.mjs +665 -0
- package/dist/app-install-oZIQRbdD.mjs +1141 -0
- package/dist/build-lock-vE45ZzJZ.mjs +645 -0
- package/dist/build-slots-CgacA9zQ.mjs +152 -0
- package/dist/{cache-manifest-4tH64LQ9.mjs → cache-manifest-oeH18ERr.mjs} +2 -2
- package/dist/cache-manifest.mjs +1 -1
- package/dist/cli.mjs +24 -27166
- package/dist/collector-run.mjs +6 -4
- package/dist/command-output-8wHMEde0.mjs +289 -0
- package/dist/{config-7kmtuhO2.mjs → config-D7uu8Gbq.mjs} +43 -12
- package/dist/deps-64wVnpZc.mjs +435 -0
- package/dist/dev-client-Bvky1hya.mjs +557 -0
- package/dist/device-DJKv5Qej.mjs +250 -0
- package/dist/device-lease-D-G3SgVe.mjs +335 -0
- package/dist/device-pool-BlHXnYYs.mjs +408 -0
- package/dist/device-remote-CCgkG82J.mjs +1209 -0
- package/dist/doctor-BRF9aAJ3.mjs +530 -0
- package/dist/doctor-DlVxTtMq.mjs +1137 -0
- package/dist/error-diagnostics-B5IJwv3-.mjs +636 -0
- package/dist/gc-DhXdgSdt.mjs +1651 -0
- package/dist/guide-DrhrDBo5.mjs +4319 -0
- package/dist/ios-BZxKPuqP.mjs +382 -0
- package/dist/ios-CK1Vrk6Q.mjs +2721 -0
- package/dist/ios-device-CYbuRNXR.mjs +81 -0
- package/dist/ios-device-CdSm0GDw.mjs +413 -0
- package/dist/logs-Dxoy16Qv.mjs +176 -0
- package/dist/logs-query-oqn47Ffx.mjs +273 -0
- package/dist/metro-B01H3EuD.mjs +184 -0
- package/dist/metro-store-CDDZHtPl.mjs +92 -0
- package/dist/ownership-B9QIdQOw.mjs +475 -0
- package/dist/ownership-D8HmSv2E.mjs +60 -0
- package/dist/ownership-claim-CeJLEpjD.mjs +569 -0
- package/dist/{project-Dit1CckW.mjs → project-COj6nzzh.mjs} +12 -3
- package/dist/reclaim-BPvN2HlP.mjs +347 -0
- package/dist/reload-DYZEBbZy.mjs +332 -0
- package/dist/remote-cache-DOi5-zLj.mjs +934 -0
- package/dist/{server-bare-BmTVVmBB.mjs → server-bare-BYHqEeyw.mjs} +3 -2
- package/dist/{server-expo-CHJe1r6j.mjs → server-expo-BEMr_m6p.mjs} +5 -4
- package/dist/server-expo-rb0k-0w7.mjs +2 -0
- package/dist/settings-BSwrY4FZ.mjs +687 -0
- package/dist/spawn-entry-nCE6m770.mjs +14 -0
- package/dist/start-ByCylYtB.mjs +822 -0
- package/dist/{state-2nush3MK.mjs → state-IYEY-zi8.mjs} +21 -22
- package/dist/stats-DZ4Q5J-7.mjs +62 -0
- package/dist/stats-wqxPm7Tv.mjs +307 -0
- package/dist/status-Dia0XTqG.mjs +363 -0
- package/dist/stop-BApyaF21.mjs +591 -0
- package/dist/stop-D6o5zAdm.mjs +3 -0
- package/dist/supervisor-run.d.mts +0 -1
- package/dist/supervisor-run.mjs +4 -4
- package/dist/workspace-process-lock-CxOO9-pf.mjs +47 -0
- package/dist/worktree-Dx3ZC5lS.mjs +539 -0
- package/dist/worktree-DxEmUI_n.mjs +1322 -0
- package/dist/xcode-CDB6fytw.mjs +1705 -0
- package/package.json +5 -5
- package/dist/android-jc8MnUyL.mjs +0 -514
- package/dist/metro-store-COLl1pOk.mjs +0 -1249
- package/dist/server-expo-BZJm--wA.mjs +0 -2
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
import { t as getExecutor } from "./exec-bsN9MJXb.mjs";
|
|
2
|
+
import { E as inspectProcessIdentity, O as waitForProcessExit, S as upsertProject, T as captureProcessToken, a as clearSupervisor, f as getProject } from "./config-D7uu8Gbq.mjs";
|
|
3
|
+
import { C as supervisorLogFile, D as workspaceLogsDir, _ as writeWorkspaceState, c as readMetroTunnel, i as clearManagedMetroTunnel, p as readWorkspaceState, s as clearWorkspaceSupervisor, y as ensureWorkspaceStorage } from "./state-IYEY-zi8.mjs";
|
|
4
|
+
import { g as repoRoot, s as gitCommonDir } from "./worktree-Dx3ZC5lS.mjs";
|
|
5
|
+
import { a as findProjectRoot, i as detectIsExpo, n as detectAndroidPackage, r as detectBundleId, t as appProjectProblem } from "./project-COj6nzzh.mjs";
|
|
6
|
+
import { n as supervisorError } from "./errors-B6W7FkxQ.mjs";
|
|
7
|
+
import { i as resolveProjectMetro, n as killMetroTree, o as reserveMetroPort, r as pidExists } from "./metro-B01H3EuD.mjs";
|
|
8
|
+
import { D as remoteIosSetting, E as remoteDeviceSettingError, F as unknownSettingKeys, I as PUBLIC_METRO_ENV, L as detectProviders, M as settingShapeErrors, O as resolveCacheProviderConfig, P as tunnelModeSetting, R as planMetroReach, T as remoteAndroidSetting, a as SETTING_SHAPE_REMEDY, b as metroTunnelSettingError, d as cacheProviderSettingError, k as resolveSettings, w as publicUrlSetting, x as ngrokUrlSetting } from "./settings-BSwrY4FZ.mjs";
|
|
9
|
+
import { g as stepTimer, l as phaseLine } from "./command-output-8wHMEde0.mjs";
|
|
10
|
+
import { n as workspaceProcessLockError, t as withWorkspaceProcessLock } from "./workspace-process-lock-CxOO9-pf.mjs";
|
|
11
|
+
import { n as expoMetroConfigPath } from "./metro-store-CDDZHtPl.mjs";
|
|
12
|
+
import { o as expoSdkMajor } from "./server-expo-BEMr_m6p.mjs";
|
|
13
|
+
import { t as spawnEntry } from "./spawn-entry-nCE6m770.mjs";
|
|
14
|
+
import { s as queryLogs } from "./logs-query-oqn47Ffx.mjs";
|
|
15
|
+
import { a as withManagedRemoteWorktreeLock, i as terminateChild, n as startTunnelSequence, r as stopTunnel, s as withManagedTunnelLock, t as resolveSupervisorTarget } from "./ownership-B9QIdQOw.mjs";
|
|
16
|
+
import { mkdirSync, openSync, readFileSync } from "node:fs";
|
|
17
|
+
import { randomUUID } from "node:crypto";
|
|
18
|
+
import { dirname } from "node:path";
|
|
19
|
+
import chalk from "chalk";
|
|
20
|
+
import { CACHE_PROVIDER_ENV, cacheProviderEnv } from "@stim-cli/cache";
|
|
21
|
+
//#region src/supervisor/cache-reset.ts
|
|
22
|
+
async function resetMetroCache(root) {
|
|
23
|
+
if (detectIsExpo(root) && ((expoSdkMajor(root) ?? 0) < 54 || !expoMetroConfigPath())) throw supervisorError("STIM_BAD_ARG", "An isolated Metro cache reset requires Expo SDK 54 or newer and the Stim Metro config adapter.", "Upgrade Expo or repair the Stim installation before retrying. The running server was not changed.");
|
|
24
|
+
const project = getProject(root);
|
|
25
|
+
const port = project?.metroPort;
|
|
26
|
+
const target = resolveSupervisorTarget({
|
|
27
|
+
state: readWorkspaceState(root)?.supervisor,
|
|
28
|
+
record: project?.supervisor,
|
|
29
|
+
reservedPort: port
|
|
30
|
+
});
|
|
31
|
+
if (target.status === "unverified") throw supervisorError("STIM_SUPERVISOR_EXITED", `Cannot reset Metro: ${target.reason}.`, "Stop it with the tool that started it, then retry. Stim leaves unverified processes alone.");
|
|
32
|
+
if (port && target.status !== "ours" && !(await resolveProjectMetro(port, root)).missing) throw supervisorError("STIM_SUPERVISOR_EXITED", "Cannot reset an externally started dev server.", "Stop it with the tool that started it, then retry `stim start --reset-cache`.");
|
|
33
|
+
if (target.status === "ours") {
|
|
34
|
+
const expected = {
|
|
35
|
+
pid: target.pid,
|
|
36
|
+
processToken: target.processToken
|
|
37
|
+
};
|
|
38
|
+
if (!killMetroTree(target.pid, target.processToken) || !await waitForProcessExit(expected, 1e4)) throw supervisorError("STIM_SUPERVISOR_EXITED", "The owned Metro supervisor could not be stopped for a cache reset.", "Inspect `stim status` and `stim logs`, then retry. No cache reset was recorded.");
|
|
39
|
+
if (port && !(await resolveProjectMetro(port, root)).missing) throw supervisorError("STIM_SUPERVISOR_EXITED", `Port ${port} is still occupied after stopping Metro.`, "Inspect the listener before retrying. Stim does not terminate unrecorded processes.");
|
|
40
|
+
clearWorkspaceSupervisor(root, expected);
|
|
41
|
+
clearSupervisor(root, expected);
|
|
42
|
+
}
|
|
43
|
+
writeWorkspaceState(root, { metroCacheGeneration: randomUUID() });
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/commands/start.ts
|
|
47
|
+
const DEFAULT_WAIT_SECONDS = 60;
|
|
48
|
+
const POLL_MS = 500;
|
|
49
|
+
const LOG_TAIL_LINES = 5;
|
|
50
|
+
function writeNote(line) {
|
|
51
|
+
console.error(line);
|
|
52
|
+
}
|
|
53
|
+
function supervisorEntry() {
|
|
54
|
+
return spawnEntry("supervisor-run");
|
|
55
|
+
}
|
|
56
|
+
function wantsExpoOwnTunnel({ isExpo, remote, mode, publicUrl }) {
|
|
57
|
+
return remote && isExpo && !publicUrl && mode === "expo";
|
|
58
|
+
}
|
|
59
|
+
function parseWait(value) {
|
|
60
|
+
if (value === void 0 || value === null) return { seconds: DEFAULT_WAIT_SECONDS };
|
|
61
|
+
const seconds = Number(value);
|
|
62
|
+
if (!Number.isFinite(seconds) || seconds <= 0) return { error: `Invalid --wait value ${JSON.stringify(value)}. Pass a number of seconds, e.g. --wait 90.` };
|
|
63
|
+
return { seconds };
|
|
64
|
+
}
|
|
65
|
+
function liveSupervisor({ state, project, port, isAlive = pidExists, inspectIdentity = inspectProcessIdentity } = {}) {
|
|
66
|
+
const target = resolveSupervisorTarget({
|
|
67
|
+
state: state?.supervisor,
|
|
68
|
+
record: project?.supervisor,
|
|
69
|
+
reservedPort: port,
|
|
70
|
+
isAlive,
|
|
71
|
+
inspectIdentity
|
|
72
|
+
});
|
|
73
|
+
return target.status === "ours" && target.port === port ? {
|
|
74
|
+
pid: target.pid,
|
|
75
|
+
port: target.port,
|
|
76
|
+
mode: target.mode ?? null,
|
|
77
|
+
startedAt: target.startedAt ?? null
|
|
78
|
+
} : null;
|
|
79
|
+
}
|
|
80
|
+
function startFacts({ port, supervisor, logsDir, alreadyRunning }) {
|
|
81
|
+
return {
|
|
82
|
+
port,
|
|
83
|
+
supervisorPid: supervisor?.pid ?? null,
|
|
84
|
+
mode: supervisor?.mode ?? null,
|
|
85
|
+
logsDir,
|
|
86
|
+
alreadyRunning: Boolean(alreadyRunning)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function startError({ code, message, remedy = null }) {
|
|
90
|
+
return {
|
|
91
|
+
code,
|
|
92
|
+
message,
|
|
93
|
+
remedy: remedy ?? null
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function tailLines(text, n = LOG_TAIL_LINES) {
|
|
97
|
+
return String(text || "").split("\n").filter((l) => l.trim() !== "").slice(-n);
|
|
98
|
+
}
|
|
99
|
+
function readLogTail(file, n = LOG_TAIL_LINES) {
|
|
100
|
+
try {
|
|
101
|
+
return tailLines(readFileSync(file, "utf-8"), n);
|
|
102
|
+
} catch {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
107
|
+
function startCommand(program) {
|
|
108
|
+
registerStart(program);
|
|
109
|
+
}
|
|
110
|
+
function providersOnPath() {
|
|
111
|
+
return detectProviders((bin) => {
|
|
112
|
+
try {
|
|
113
|
+
return Boolean(getExecutor().runQuiet(`command -v ${bin}`, { timeoutMs: 5e3 }));
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
const DEFAULT_START_DEPS = {
|
|
120
|
+
providers: providersOnPath,
|
|
121
|
+
startTunnelSequence,
|
|
122
|
+
isTunnelAlive: pidExists,
|
|
123
|
+
writeTunnelRecord: writeWorkspaceState,
|
|
124
|
+
stopTunnel,
|
|
125
|
+
writeSupervisorRecord: writeWorkspaceState,
|
|
126
|
+
terminateSupervisorChild: (child) => terminateChild(child, {
|
|
127
|
+
alreadyExited: false,
|
|
128
|
+
timeoutMs: 1e3,
|
|
129
|
+
now: Date.now,
|
|
130
|
+
sleep,
|
|
131
|
+
isAlive: pidExists
|
|
132
|
+
}),
|
|
133
|
+
withWorktreeLock: withManagedRemoteWorktreeLock,
|
|
134
|
+
withTunnelLock: withManagedTunnelLock,
|
|
135
|
+
clearTunnelRecord: clearManagedMetroTunnel
|
|
136
|
+
};
|
|
137
|
+
function normalizeManagedTunnelUrl(url) {
|
|
138
|
+
return url.trim().replace(/\/+$/, "");
|
|
139
|
+
}
|
|
140
|
+
function registerStart(program, overrides = {}) {
|
|
141
|
+
const d = {
|
|
142
|
+
...DEFAULT_START_DEPS,
|
|
143
|
+
...overrides
|
|
144
|
+
};
|
|
145
|
+
program.command("start").description("Start this workspace's dev server under a detached supervisor and wait until it verifies as this project's. Idempotent: a healthy dev server on the reserved port is a no-op. Structured logs land in the global workspace logs directory.").option("--json", "Emit the facts as a single JSON line on stdout; every other line goes to stderr").option("--wait <seconds>", `How long to wait for the dev server to answer (default ${DEFAULT_WAIT_SECONDS})`).option("--remote", "Prepare the dev server for a remote device").option("--reset-cache", "Restart owned Metro with fresh per-app transform and file-map cache state").action(async (opts) => {
|
|
146
|
+
const json = Boolean(opts.json);
|
|
147
|
+
const waitTimer = stepTimer();
|
|
148
|
+
const out = (line) => {
|
|
149
|
+
if (json) console.error(line);
|
|
150
|
+
else console.log(line);
|
|
151
|
+
};
|
|
152
|
+
const note = writeNote;
|
|
153
|
+
const fail = ({ code, message, remedy = null, lines = [] }) => {
|
|
154
|
+
note(chalk.red(message));
|
|
155
|
+
for (const line of lines) note(chalk.dim(` ${line}`));
|
|
156
|
+
if (remedy) note(chalk.dim(remedy));
|
|
157
|
+
note(chalk.red(`failed: ${code}`));
|
|
158
|
+
if (json) console.log(JSON.stringify(startError({
|
|
159
|
+
code,
|
|
160
|
+
message,
|
|
161
|
+
remedy
|
|
162
|
+
})));
|
|
163
|
+
process.exit(1);
|
|
164
|
+
};
|
|
165
|
+
const wait = parseWait(opts.wait);
|
|
166
|
+
if (wait.error) return fail({
|
|
167
|
+
code: "STIM_BAD_ARG",
|
|
168
|
+
message: wait.error,
|
|
169
|
+
remedy: "Pass a whole number of seconds, e.g. --wait 90."
|
|
170
|
+
});
|
|
171
|
+
const waitSeconds = wait.seconds;
|
|
172
|
+
const root = findProjectRoot(process.cwd());
|
|
173
|
+
if (!root) return fail({
|
|
174
|
+
code: "STIM_NO_PROJECT",
|
|
175
|
+
message: "Not in a React Native project (no package.json found).",
|
|
176
|
+
remedy: "Run this from the app directory -- the one holding package.json."
|
|
177
|
+
});
|
|
178
|
+
const projectProblem = appProjectProblem(root);
|
|
179
|
+
if (projectProblem) return fail({
|
|
180
|
+
code: "STIM_NO_PROJECT",
|
|
181
|
+
message: projectProblem.message,
|
|
182
|
+
remedy: projectProblem.remedy
|
|
183
|
+
});
|
|
184
|
+
try {
|
|
185
|
+
ensureWorkspaceStorage(root);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
return fail({
|
|
188
|
+
code: error?.code || "STIM_WORKSPACE_STATE",
|
|
189
|
+
message: `Could not prepare this workspace's Stim state: ${error?.message || error}`,
|
|
190
|
+
remedy: "Check that STIM_HOME is writable and has free space. An EPERM on a directory you can write is a sandbox: allow writes to STIM_HOME, or run Stim with the sandbox disabled (`stim guide errors sandbox`)."
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
const isExpo = detectIsExpo(root);
|
|
194
|
+
const worktreeRoot = repoRoot(root) ?? root;
|
|
195
|
+
const settingsContext = {
|
|
196
|
+
projectPath: root,
|
|
197
|
+
gitCommonDir: gitCommonDir(root),
|
|
198
|
+
repoRoot: worktreeRoot
|
|
199
|
+
};
|
|
200
|
+
const settings = resolveSettings(settingsContext);
|
|
201
|
+
const [shapeError, ...moreShapeErrors] = settingShapeErrors(settings);
|
|
202
|
+
if (shapeError) return fail({
|
|
203
|
+
code: "STIM_BAD_ARG",
|
|
204
|
+
message: shapeError,
|
|
205
|
+
lines: moreShapeErrors,
|
|
206
|
+
remedy: SETTING_SHAPE_REMEDY
|
|
207
|
+
});
|
|
208
|
+
const cacheProvider = resolveCacheProviderConfig(settingsContext);
|
|
209
|
+
for (const key of unknownSettingKeys(settings)) note(chalk.yellow(`Warning: setting "${key}" is not read by Stim and will be ignored.`));
|
|
210
|
+
const cacheProviderError = cacheProviderSettingError(settings);
|
|
211
|
+
if (cacheProviderError) note(chalk.yellow(`Warning: ${cacheProviderError} No cache provider is used.`));
|
|
212
|
+
const settingError = metroTunnelSettingError(settings);
|
|
213
|
+
if (settingError) return fail({
|
|
214
|
+
code: "STIM_BAD_ARG",
|
|
215
|
+
message: settingError,
|
|
216
|
+
remedy: `Set metro.tunnel to one of: auto, expo, ngrok, cloudflared, off.`
|
|
217
|
+
});
|
|
218
|
+
const remoteSettingError = remoteDeviceSettingError(settings);
|
|
219
|
+
if (remoteSettingError) return fail({
|
|
220
|
+
code: "STIM_BAD_ARG",
|
|
221
|
+
message: remoteSettingError,
|
|
222
|
+
remedy: "Set ios.remote and android.remote to either proxy or eas."
|
|
223
|
+
});
|
|
224
|
+
const remote = Boolean(opts.remote) || remoteIosSetting(settings) !== null || remoteAndroidSetting(settings) !== null;
|
|
225
|
+
const tunnelMode = tunnelModeSetting(settings) ?? "auto";
|
|
226
|
+
const publicUrl = publicUrlSetting(settings);
|
|
227
|
+
const tunnel = wantsExpoOwnTunnel({
|
|
228
|
+
isExpo,
|
|
229
|
+
remote,
|
|
230
|
+
mode: tunnelMode,
|
|
231
|
+
publicUrl
|
|
232
|
+
});
|
|
233
|
+
if (tunnel) note(chalk.dim("note requesting an Expo tunnel for this workspace's dev server"));
|
|
234
|
+
const managedRemote = remote && !tunnel && !publicUrl && tunnelMode !== "off";
|
|
235
|
+
const runStart = async () => {
|
|
236
|
+
upsertProject(root, {
|
|
237
|
+
bundleId: detectBundleId(root) ?? void 0,
|
|
238
|
+
androidPackage: detectAndroidPackage(root) ?? void 0,
|
|
239
|
+
isExpo
|
|
240
|
+
});
|
|
241
|
+
if (opts.resetCache) {
|
|
242
|
+
try {
|
|
243
|
+
await resetMetroCache(root);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
const failure = error;
|
|
246
|
+
return fail({
|
|
247
|
+
code: failure.code ?? "STIM_WORKSPACE_STATE",
|
|
248
|
+
message: failure.message,
|
|
249
|
+
remedy: failure.remedy
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
note(phaseLine("cache", "fresh Metro cache state for this app; shared cache entries and devices preserved"));
|
|
253
|
+
}
|
|
254
|
+
const logsDir = workspaceLogsDir(root);
|
|
255
|
+
const logFile = supervisorLogFile(root);
|
|
256
|
+
const port = await resolvePort(root, note);
|
|
257
|
+
let publicOrigin = remote ? publicUrl : null;
|
|
258
|
+
let resolution = await resolveProjectMetro(port, root);
|
|
259
|
+
let supervisor = liveSupervisor({
|
|
260
|
+
state: readWorkspaceState(root),
|
|
261
|
+
project: getProject(root),
|
|
262
|
+
port
|
|
263
|
+
});
|
|
264
|
+
const recordedTarget = resolveSupervisorTarget({
|
|
265
|
+
state: readWorkspaceState(root)?.supervisor,
|
|
266
|
+
record: getProject(root)?.supervisor,
|
|
267
|
+
reservedPort: port
|
|
268
|
+
});
|
|
269
|
+
if (recordedTarget.status === "unverified") return fail({
|
|
270
|
+
code: "STIM_SUPERVISOR_EXITED",
|
|
271
|
+
message: `Cannot reuse or replace the recorded supervisor: ${recordedTarget.reason}.`,
|
|
272
|
+
remedy: "Stop it with the tool that started it, then retry `stim start`. Stim leaves unverified processes alone."
|
|
273
|
+
});
|
|
274
|
+
let managedTunnel = null;
|
|
275
|
+
let spawnedChild = null;
|
|
276
|
+
let spawnedTs = null;
|
|
277
|
+
let childExit = null;
|
|
278
|
+
const spawnSupervisor = (origin) => {
|
|
279
|
+
mkdirSync(logsDir, { recursive: true });
|
|
280
|
+
const fd = openSync(logFile, "a");
|
|
281
|
+
spawnedTs = Date.now();
|
|
282
|
+
const supervisorArgs = [
|
|
283
|
+
supervisorEntry(),
|
|
284
|
+
"--root",
|
|
285
|
+
root,
|
|
286
|
+
"--port",
|
|
287
|
+
String(port),
|
|
288
|
+
...tunnel ? ["--tunnel"] : []
|
|
289
|
+
];
|
|
290
|
+
const childEnv = {
|
|
291
|
+
...process.env,
|
|
292
|
+
...origin ? {
|
|
293
|
+
[PUBLIC_METRO_ENV]: origin,
|
|
294
|
+
EXPO_PACKAGER_PROXY_URL: origin
|
|
295
|
+
} : {}
|
|
296
|
+
};
|
|
297
|
+
childEnv[CACHE_PROVIDER_ENV] = cacheProviderEnv(cacheProvider);
|
|
298
|
+
const child = getExecutor().spawn(process.execPath, supervisorArgs, {
|
|
299
|
+
cwd: root,
|
|
300
|
+
detached: true,
|
|
301
|
+
stdio: [
|
|
302
|
+
"ignore",
|
|
303
|
+
fd,
|
|
304
|
+
fd
|
|
305
|
+
],
|
|
306
|
+
env: childEnv
|
|
307
|
+
});
|
|
308
|
+
child.unref?.();
|
|
309
|
+
child.on?.("exit", (code, signal) => {
|
|
310
|
+
childExit = {
|
|
311
|
+
code,
|
|
312
|
+
signal
|
|
313
|
+
};
|
|
314
|
+
});
|
|
315
|
+
child.on?.("error", (err) => {
|
|
316
|
+
childExit = {
|
|
317
|
+
code: null,
|
|
318
|
+
signal: null,
|
|
319
|
+
error: err
|
|
320
|
+
};
|
|
321
|
+
});
|
|
322
|
+
out(chalk.dim(phaseLine("metro", `starting on port ${port} (${isExpo ? "expo-child" : "bare-inproc"}, supervisor pid ${child.pid})`)));
|
|
323
|
+
spawnedChild = child;
|
|
324
|
+
return child;
|
|
325
|
+
};
|
|
326
|
+
const waitForSupervisorHandoff = async (child) => {
|
|
327
|
+
const deadline = Date.now() + 5e3;
|
|
328
|
+
while (Date.now() < deadline) {
|
|
329
|
+
const found = liveSupervisor({
|
|
330
|
+
state: readWorkspaceState(root),
|
|
331
|
+
project: getProject(root),
|
|
332
|
+
port
|
|
333
|
+
});
|
|
334
|
+
if (found?.pid === child.pid) return found;
|
|
335
|
+
if (childExit !== null || (child.pid ? !pidExists(child.pid) : true)) return null;
|
|
336
|
+
await sleep(25);
|
|
337
|
+
}
|
|
338
|
+
return null;
|
|
339
|
+
};
|
|
340
|
+
if (remote && !tunnel && !publicUrl && tunnelMode !== "off") {
|
|
341
|
+
const available = d.providers();
|
|
342
|
+
const plan = planMetroReach({
|
|
343
|
+
mode: tunnelMode,
|
|
344
|
+
metroPort: port,
|
|
345
|
+
publicUrl,
|
|
346
|
+
isExpo,
|
|
347
|
+
available
|
|
348
|
+
});
|
|
349
|
+
if ("failed" in plan) return fail({
|
|
350
|
+
code: "STIM_REMOTE_METRO_UNREACHABLE",
|
|
351
|
+
message: plan.failed,
|
|
352
|
+
remedy: plan.remedy
|
|
353
|
+
});
|
|
354
|
+
if ("start" in plan) {
|
|
355
|
+
const candidates = tunnelMode === "auto" ? available : [plan.start];
|
|
356
|
+
const expectedStableUrl = ngrokUrlSetting(settings);
|
|
357
|
+
let acquisition;
|
|
358
|
+
try {
|
|
359
|
+
acquisition = await d.withTunnelLock(root, async () => {
|
|
360
|
+
let tracking;
|
|
361
|
+
let startedCleanup = null;
|
|
362
|
+
const recorded = readMetroTunnel(root);
|
|
363
|
+
if (recorded?.kind === "managed" && d.isTunnelAlive(recorded.pid)) {
|
|
364
|
+
if (!recorded.processToken) return { failed: {
|
|
365
|
+
code: "STIM_REMOTE_START_REQUIRED",
|
|
366
|
+
message: "The recorded managed Metro tunnel has no process identity token.",
|
|
367
|
+
remedy: "Inspect the process, stop it with the provider tooling, remove the stale metroTunnel state, and retry."
|
|
368
|
+
} };
|
|
369
|
+
if (!(recorded.port === port && candidates.includes(recorded.provider) && (!expectedStableUrl || normalizeManagedTunnelUrl(recorded.url) === expectedStableUrl))) return { failed: {
|
|
370
|
+
code: "STIM_REMOTE_START_REQUIRED",
|
|
371
|
+
message: `A different managed Metro tunnel is already running for this workspace.`,
|
|
372
|
+
remedy: "Run `stim stop`, then `stim start --remote`."
|
|
373
|
+
} };
|
|
374
|
+
tracking = {
|
|
375
|
+
record: {
|
|
376
|
+
provider: recorded.provider,
|
|
377
|
+
pid: recorded.pid,
|
|
378
|
+
url: normalizeManagedTunnelUrl(recorded.url),
|
|
379
|
+
port: recorded.port,
|
|
380
|
+
startedAt: recorded.startedAt,
|
|
381
|
+
processToken: recorded.processToken,
|
|
382
|
+
logFile: recorded.logFile
|
|
383
|
+
},
|
|
384
|
+
startedHere: false
|
|
385
|
+
};
|
|
386
|
+
} else {
|
|
387
|
+
const currentResolution = await resolveProjectMetro(port, root);
|
|
388
|
+
const currentSupervisor = liveSupervisor({
|
|
389
|
+
state: readWorkspaceState(root),
|
|
390
|
+
project: getProject(root),
|
|
391
|
+
port
|
|
392
|
+
});
|
|
393
|
+
if (currentResolution.metro || currentSupervisor) return { failed: {
|
|
394
|
+
code: "STIM_REMOTE_START_REQUIRED",
|
|
395
|
+
message: `The dev server on port ${port} is local-only and cannot gain a managed tunnel while it is running.`,
|
|
396
|
+
remedy: "Run `stim stop`, then `stim start --remote`."
|
|
397
|
+
} };
|
|
398
|
+
const started = await d.startTunnelSequence({
|
|
399
|
+
providers: candidates,
|
|
400
|
+
port,
|
|
401
|
+
ngrokUrl: expectedStableUrl,
|
|
402
|
+
requireReachable: false
|
|
403
|
+
});
|
|
404
|
+
if ("failed" in started) return { failed: {
|
|
405
|
+
code: "STIM_REMOTE_METRO_UNREACHABLE",
|
|
406
|
+
message: `Could not start a managed Metro tunnel for port ${port}.`,
|
|
407
|
+
remedy: started.reason
|
|
408
|
+
} };
|
|
409
|
+
const record = {
|
|
410
|
+
provider: started.provider,
|
|
411
|
+
pid: started.pid,
|
|
412
|
+
url: normalizeManagedTunnelUrl(started.url),
|
|
413
|
+
port,
|
|
414
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
415
|
+
processToken: started.processToken,
|
|
416
|
+
logFile: started.logFile
|
|
417
|
+
};
|
|
418
|
+
startedCleanup = started.cleanup;
|
|
419
|
+
try {
|
|
420
|
+
d.writeTunnelRecord(root, { metroTunnel: {
|
|
421
|
+
kind: "managed",
|
|
422
|
+
...record
|
|
423
|
+
} });
|
|
424
|
+
} catch (err) {
|
|
425
|
+
const stopped = await started.cleanup();
|
|
426
|
+
return { failed: {
|
|
427
|
+
code: "STIM_REMOTE_METRO_UNREACHABLE",
|
|
428
|
+
message: `Could not record the managed Metro tunnel: ${err?.message || err}`,
|
|
429
|
+
remedy: stopped.status === "failed" ? `Cleanup failed. Unmanaged pid ${record.pid} may still be running: ${stopped.reason ?? "unknown error"}` : "The tunnel process was stopped. Fix the workspace write error, then retry `stim start --remote`."
|
|
430
|
+
} };
|
|
431
|
+
}
|
|
432
|
+
tracking = {
|
|
433
|
+
record,
|
|
434
|
+
startedHere: true
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
const currentResolution = await resolveProjectMetro(port, root);
|
|
438
|
+
const currentSupervisor = liveSupervisor({
|
|
439
|
+
state: readWorkspaceState(root),
|
|
440
|
+
project: getProject(root),
|
|
441
|
+
port
|
|
442
|
+
});
|
|
443
|
+
if (currentResolution.metro || currentSupervisor) {
|
|
444
|
+
if (tracking.startedHere && startedCleanup) {
|
|
445
|
+
const stopped = await startedCleanup();
|
|
446
|
+
if (stopped.status === "failed") return { failed: {
|
|
447
|
+
code: "STIM_REMOTE_START_REQUIRED",
|
|
448
|
+
message: `The dev server on port ${port} started before the managed tunnel was ready.`,
|
|
449
|
+
remedy: `Tunnel cleanup failed for pid ${tracking.record.pid}: ${stopped.reason ?? "unknown error"}. The tunnel record remains available to \`stim stop\`.`
|
|
450
|
+
} };
|
|
451
|
+
d.clearTunnelRecord(root, tracking.record);
|
|
452
|
+
return { failed: {
|
|
453
|
+
code: "STIM_REMOTE_START_REQUIRED",
|
|
454
|
+
message: `The dev server on port ${port} started before the managed tunnel was ready.`,
|
|
455
|
+
remedy: "Run `stim stop`, then `stim start --remote`."
|
|
456
|
+
} };
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
origin: tracking.record.url,
|
|
460
|
+
tunnel: tracking
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
if (!d.isTunnelAlive(tracking.record.pid)) {
|
|
464
|
+
if (tracking.startedHere && startedCleanup) {
|
|
465
|
+
const stopped = await startedCleanup();
|
|
466
|
+
if (stopped.status === "failed") return { failed: {
|
|
467
|
+
code: "STIM_REMOTE_METRO_UNREACHABLE",
|
|
468
|
+
message: `The managed ${tracking.record.provider} tunnel exited before the supervisor started.`,
|
|
469
|
+
remedy: `Cleanup failed. Unmanaged pid ${tracking.record.pid} may still be running: ${stopped.reason ?? "unknown error"}. The tunnel record remains available to \`stim stop\`.`
|
|
470
|
+
} };
|
|
471
|
+
}
|
|
472
|
+
d.clearTunnelRecord(root, tracking.record);
|
|
473
|
+
return { failed: {
|
|
474
|
+
code: "STIM_REMOTE_METRO_UNREACHABLE",
|
|
475
|
+
message: `The managed ${tracking.record.provider} tunnel exited before the supervisor started.`,
|
|
476
|
+
remedy: "Retry `stim start --remote`."
|
|
477
|
+
} };
|
|
478
|
+
}
|
|
479
|
+
let child;
|
|
480
|
+
try {
|
|
481
|
+
child = spawnSupervisor(tracking.record.url);
|
|
482
|
+
} catch (err) {
|
|
483
|
+
return { failed: {
|
|
484
|
+
code: "STIM_SUPERVISOR_EXITED",
|
|
485
|
+
message: `Could not spawn the dev server supervisor: ${err?.message || err}`,
|
|
486
|
+
remedy: "Fix the spawn error, then retry `stim start --remote`."
|
|
487
|
+
} };
|
|
488
|
+
}
|
|
489
|
+
const handoffRecord = {
|
|
490
|
+
pid: child.pid,
|
|
491
|
+
processToken: child.pid ? captureProcessToken(child.pid) : null,
|
|
492
|
+
port,
|
|
493
|
+
mode: isExpo ? "expo-child" : "bare-inproc",
|
|
494
|
+
startedAt: new Date(spawnedTs ?? Date.now()).toISOString()
|
|
495
|
+
};
|
|
496
|
+
try {
|
|
497
|
+
d.writeSupervisorRecord(root, { supervisor: handoffRecord });
|
|
498
|
+
} catch {
|
|
499
|
+
if (!await waitForSupervisorHandoff(child)) {
|
|
500
|
+
const stopped = await d.terminateSupervisorChild(child);
|
|
501
|
+
return { failed: {
|
|
502
|
+
code: "STIM_SUPERVISOR_EXITED",
|
|
503
|
+
message: `Could not record supervisor pid ${child.pid ?? "unknown"} before releasing the managed start lock.`,
|
|
504
|
+
remedy: stopped ? "The supervisor process was stopped. Retry `stim start --remote`." : `Cleanup failed. Unmanaged supervisor pid ${child.pid ?? "unknown"} may still be running. Stop it before retrying.`
|
|
505
|
+
} };
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return {
|
|
509
|
+
origin: tracking.record.url,
|
|
510
|
+
tunnel: tracking
|
|
511
|
+
};
|
|
512
|
+
});
|
|
513
|
+
} catch (err) {
|
|
514
|
+
return fail({
|
|
515
|
+
code: "STIM_REMOTE_METRO_UNREACHABLE",
|
|
516
|
+
message: `Could not acquire the managed Metro tunnel lock: ${err?.message || err}`,
|
|
517
|
+
remedy: "Retry `stim start --remote` after the other start command finishes."
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
if ("failed" in acquisition) return fail(acquisition.failed);
|
|
521
|
+
publicOrigin = acquisition.origin;
|
|
522
|
+
managedTunnel = acquisition.tunnel;
|
|
523
|
+
resolution = await resolveProjectMetro(port, root);
|
|
524
|
+
supervisor = liveSupervisor({
|
|
525
|
+
state: readWorkspaceState(root),
|
|
526
|
+
project: getProject(root),
|
|
527
|
+
port
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
const managedTunnelExited = () => managedTunnel !== null && !d.isTunnelAlive(managedTunnel.record.pid);
|
|
532
|
+
const failExitedManagedTunnel = async () => {
|
|
533
|
+
const tracked = managedTunnel;
|
|
534
|
+
const record = tracked.record;
|
|
535
|
+
const stopped = tracked.startedHere ? await d.stopTunnel(record) : { status: "missing" };
|
|
536
|
+
if (stopped.status !== "failed") d.clearTunnelRecord(root, record);
|
|
537
|
+
return fail({
|
|
538
|
+
code: "STIM_REMOTE_METRO_UNREACHABLE",
|
|
539
|
+
message: `The managed ${record.provider} tunnel exited before the dev server became ready.`,
|
|
540
|
+
remedy: stopped.status === "failed" ? `The tunnel cleanup also failed: ${stopped.reason ?? "unknown error"}. Run \`stim stop\`, then retry.` : "Run `stim stop`, then `stim start --remote`."
|
|
541
|
+
});
|
|
542
|
+
};
|
|
543
|
+
const requireExpoTunnel = () => {
|
|
544
|
+
if (tunnel && (!supervisor || readMetroTunnel(root)?.kind !== "expo")) fail({
|
|
545
|
+
code: "STIM_REMOTE_START_REQUIRED",
|
|
546
|
+
message: `The Expo dev server on port ${port} is local-only and cannot gain a tunnel while it is running.`,
|
|
547
|
+
remedy: "Run `stim stop`, then `stim start --remote`."
|
|
548
|
+
});
|
|
549
|
+
};
|
|
550
|
+
if (!spawnedChild && resolution.metro) {
|
|
551
|
+
if (managedTunnelExited()) return failExitedManagedTunnel();
|
|
552
|
+
if (tunnel && supervisor) {
|
|
553
|
+
if (!await waitForExpoTunnel({
|
|
554
|
+
root,
|
|
555
|
+
seconds: waitSeconds,
|
|
556
|
+
aborted: () => !liveSupervisor({
|
|
557
|
+
state: readWorkspaceState(root),
|
|
558
|
+
project: getProject(root),
|
|
559
|
+
port
|
|
560
|
+
})
|
|
561
|
+
})) {
|
|
562
|
+
const stillLive = liveSupervisor({
|
|
563
|
+
state: readWorkspaceState(root),
|
|
564
|
+
project: getProject(root),
|
|
565
|
+
port
|
|
566
|
+
});
|
|
567
|
+
return fail({
|
|
568
|
+
code: "STIM_REMOTE_START_REQUIRED",
|
|
569
|
+
message: stillLive ? `The Expo dev server on port ${port} is local-only and cannot gain a tunnel while it is running.` : `The Expo dev server on port ${port} stopped before its tunnel became ready.`,
|
|
570
|
+
remedy: stillLive ? "Run `stim stop`, then `stim start --remote`." : "Run `stim start --remote` again."
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
supervisor = liveSupervisor({
|
|
574
|
+
state: readWorkspaceState(root),
|
|
575
|
+
project: getProject(root),
|
|
576
|
+
port
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
requireExpoTunnel();
|
|
580
|
+
if (!supervisor) {
|
|
581
|
+
note(chalk.dim(`A dev server for this project already answers on port ${port}, started outside Stim.`));
|
|
582
|
+
note(chalk.dim("Leaving it alone: Stim will not start a second bundler over a working one."));
|
|
583
|
+
}
|
|
584
|
+
if (managedTunnelExited()) return failExitedManagedTunnel();
|
|
585
|
+
report({
|
|
586
|
+
json,
|
|
587
|
+
out,
|
|
588
|
+
port,
|
|
589
|
+
supervisor,
|
|
590
|
+
logsDir,
|
|
591
|
+
alreadyRunning: true,
|
|
592
|
+
waited: waitTimer()
|
|
593
|
+
});
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (!spawnedChild && supervisor) {
|
|
597
|
+
note(chalk.dim(`Supervisor pid ${supervisor.pid} is already running for this workspace; waiting for it to answer on port ${port}...`));
|
|
598
|
+
if (!await waitForMetro({
|
|
599
|
+
root,
|
|
600
|
+
port,
|
|
601
|
+
seconds: waitSeconds,
|
|
602
|
+
aborted: managedTunnel ? managedTunnelExited : void 0
|
|
603
|
+
})) {
|
|
604
|
+
if (managedTunnelExited()) return failExitedManagedTunnel();
|
|
605
|
+
return fail({
|
|
606
|
+
code: "STIM_METRO_TIMEOUT",
|
|
607
|
+
message: `Supervisor pid ${supervisor.pid} did not serve port ${port} within ${waitSeconds}s.`,
|
|
608
|
+
lines: logTailLines(logFile),
|
|
609
|
+
remedy: "Run `stim stop` to halt it, then `stim start` again."
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
if (tunnel) {
|
|
613
|
+
if (!await waitForExpoTunnel({
|
|
614
|
+
root,
|
|
615
|
+
seconds: waitSeconds,
|
|
616
|
+
aborted: () => !liveSupervisor({
|
|
617
|
+
state: readWorkspaceState(root),
|
|
618
|
+
project: getProject(root),
|
|
619
|
+
port
|
|
620
|
+
})
|
|
621
|
+
})) {
|
|
622
|
+
const stillLive = liveSupervisor({
|
|
623
|
+
state: readWorkspaceState(root),
|
|
624
|
+
project: getProject(root),
|
|
625
|
+
port
|
|
626
|
+
});
|
|
627
|
+
return fail({
|
|
628
|
+
code: "STIM_REMOTE_START_REQUIRED",
|
|
629
|
+
message: stillLive ? `The Expo dev server on port ${port} is local-only and cannot gain a tunnel while it is running.` : `The Expo dev server on port ${port} stopped before its tunnel became ready.`,
|
|
630
|
+
remedy: stillLive ? "Run `stim stop`, then `stim start --remote`." : "Run `stim start --remote` again."
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
supervisor = liveSupervisor({
|
|
635
|
+
state: readWorkspaceState(root),
|
|
636
|
+
project: getProject(root),
|
|
637
|
+
port
|
|
638
|
+
}) || (tunnel ? null : supervisor);
|
|
639
|
+
requireExpoTunnel();
|
|
640
|
+
if (managedTunnelExited()) return failExitedManagedTunnel();
|
|
641
|
+
report({
|
|
642
|
+
json,
|
|
643
|
+
out,
|
|
644
|
+
port,
|
|
645
|
+
supervisor,
|
|
646
|
+
logsDir,
|
|
647
|
+
alreadyRunning: true,
|
|
648
|
+
waited: waitTimer()
|
|
649
|
+
});
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (managedTunnelExited()) return failExitedManagedTunnel();
|
|
653
|
+
const child = spawnedChild ?? spawnSupervisor(publicOrigin);
|
|
654
|
+
const attemptStartedTs = spawnedTs ?? Date.now();
|
|
655
|
+
if (!await waitForMetro({
|
|
656
|
+
root,
|
|
657
|
+
port,
|
|
658
|
+
seconds: waitSeconds,
|
|
659
|
+
aborted: () => childExit !== null || (child.pid ? !pidExists(child.pid) : false) || managedTunnelExited()
|
|
660
|
+
})) {
|
|
661
|
+
if (managedTunnelExited()) return failExitedManagedTunnel();
|
|
662
|
+
const gone = childExit !== null || (child.pid ? !pidExists(child.pid) : false);
|
|
663
|
+
const exitInfo = childExit;
|
|
664
|
+
const how = exitInfo ? exitInfo.signal ? `signal ${exitInfo.signal}` : `code ${exitInfo.code}` : "without being observed";
|
|
665
|
+
return fail(gone ? {
|
|
666
|
+
code: "STIM_SUPERVISOR_EXITED",
|
|
667
|
+
message: `The supervisor exited (${how}) before the dev server came up on port ${port}.`,
|
|
668
|
+
lines: failureEvidence({
|
|
669
|
+
logFile,
|
|
670
|
+
logsDir,
|
|
671
|
+
sinceTs: attemptStartedTs
|
|
672
|
+
}),
|
|
673
|
+
remedy: "Fix the error above and run `stim start` again; `stim logs --errors` has the full records."
|
|
674
|
+
} : {
|
|
675
|
+
code: "STIM_METRO_TIMEOUT",
|
|
676
|
+
message: `The dev server did not answer on port ${port} within ${waitSeconds}s.`,
|
|
677
|
+
lines: failureEvidence({
|
|
678
|
+
logFile,
|
|
679
|
+
logsDir,
|
|
680
|
+
sinceTs: attemptStartedTs
|
|
681
|
+
}),
|
|
682
|
+
remedy: "It may still be starting. Run `stim stop` to halt it, or `stim logs` to follow along."
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
if (managedTunnelExited()) return failExitedManagedTunnel();
|
|
686
|
+
if (tunnel) {
|
|
687
|
+
if (!await waitForExpoTunnel({
|
|
688
|
+
root,
|
|
689
|
+
seconds: waitSeconds,
|
|
690
|
+
aborted: () => childExit !== null || (child.pid ? !pidExists(child.pid) : false)
|
|
691
|
+
})) {
|
|
692
|
+
const gone = childExit !== null || (child.pid ? !pidExists(child.pid) : false);
|
|
693
|
+
return fail({
|
|
694
|
+
code: gone ? "STIM_SUPERVISOR_EXITED" : "STIM_METRO_TIMEOUT",
|
|
695
|
+
message: gone ? `The supervisor exited before the Expo tunnel became ready on port ${port}.` : `Expo did not report a tunnel URL within ${waitSeconds}s.`,
|
|
696
|
+
remedy: "Run `stim stop`, then `stim start --remote`."
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
supervisor = liveSupervisor({
|
|
701
|
+
state: readWorkspaceState(root),
|
|
702
|
+
project: getProject(root),
|
|
703
|
+
port
|
|
704
|
+
}) || {
|
|
705
|
+
pid: child.pid,
|
|
706
|
+
port,
|
|
707
|
+
mode: null,
|
|
708
|
+
startedAt: null
|
|
709
|
+
};
|
|
710
|
+
report({
|
|
711
|
+
json,
|
|
712
|
+
out,
|
|
713
|
+
port,
|
|
714
|
+
supervisor,
|
|
715
|
+
logsDir,
|
|
716
|
+
alreadyRunning: false,
|
|
717
|
+
waited: waitTimer()
|
|
718
|
+
});
|
|
719
|
+
};
|
|
720
|
+
const startLocked = async () => {
|
|
721
|
+
try {
|
|
722
|
+
return await withWorkspaceProcessLock(dirname(workspaceLogsDir(root)), "metro-start", runStart, { external: true });
|
|
723
|
+
} catch (error) {
|
|
724
|
+
if (workspaceProcessLockError(error) !== "timeout") throw error;
|
|
725
|
+
return fail({
|
|
726
|
+
code: "STIM_METRO_TIMEOUT",
|
|
727
|
+
message: "Another Metro start or reset is still running for this app.",
|
|
728
|
+
remedy: "Wait for it to finish, then retry `stim start`."
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
if (!managedRemote) return startLocked();
|
|
733
|
+
try {
|
|
734
|
+
return await d.withWorktreeLock(worktreeRoot, startLocked);
|
|
735
|
+
} catch (err) {
|
|
736
|
+
const lockError = workspaceProcessLockError(err);
|
|
737
|
+
if (lockError === "refused") return fail({
|
|
738
|
+
code: "STIM_WORKTREE_REMOVAL_IN_PROGRESS",
|
|
739
|
+
message: `The worktree at ${worktreeRoot} is being removed.`,
|
|
740
|
+
remedy: "Retry `stim start --remote` after `stim worktree remove` finishes."
|
|
741
|
+
});
|
|
742
|
+
if (lockError === "timeout") return fail({
|
|
743
|
+
code: "STIM_REMOTE_METRO_UNREACHABLE",
|
|
744
|
+
message: `Could not acquire the managed remote worktree lock: ${err?.message || err}`,
|
|
745
|
+
remedy: "Retry `stim start --remote` after the other remote start command finishes."
|
|
746
|
+
});
|
|
747
|
+
throw err;
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
async function resolvePort(root, note) {
|
|
752
|
+
const recorded = getProject(root)?.metroPort;
|
|
753
|
+
if (!recorded) return await reserveMetroPort(root);
|
|
754
|
+
const held = await resolveProjectMetro(recorded, root);
|
|
755
|
+
if (!held.notOurs) return recorded;
|
|
756
|
+
const fresh = await reserveMetroPort(root);
|
|
757
|
+
if (fresh !== recorded) {
|
|
758
|
+
note(chalk.yellow(`Port ${recorded} is held by something else (${held.notOurs}).`));
|
|
759
|
+
note(chalk.dim(`Reserved port ${fresh} for this project instead.`));
|
|
760
|
+
}
|
|
761
|
+
return fresh;
|
|
762
|
+
}
|
|
763
|
+
async function waitForMetro({ root, port, seconds, aborted = () => false, probe = resolveProjectMetro }) {
|
|
764
|
+
const deadline = Date.now() + seconds * 1e3;
|
|
765
|
+
while (Date.now() < deadline) {
|
|
766
|
+
if ((await probe(port, root)).metro) return true;
|
|
767
|
+
if (aborted()) return false;
|
|
768
|
+
await sleep(POLL_MS);
|
|
769
|
+
}
|
|
770
|
+
const last = await probe(port, root);
|
|
771
|
+
return Boolean(last.metro);
|
|
772
|
+
}
|
|
773
|
+
async function waitForExpoTunnel({ root, seconds, aborted = () => false }) {
|
|
774
|
+
const deadline = Date.now() + seconds * 1e3;
|
|
775
|
+
while (Date.now() < deadline) {
|
|
776
|
+
if (aborted()) return false;
|
|
777
|
+
if (readMetroTunnel(root)?.kind === "expo") return true;
|
|
778
|
+
await sleep(POLL_MS);
|
|
779
|
+
}
|
|
780
|
+
return !aborted() && readMetroTunnel(root)?.kind === "expo";
|
|
781
|
+
}
|
|
782
|
+
function logTailLines(logFile) {
|
|
783
|
+
return [...readLogTail(logFile), `Supervisor log: ${logFile}`];
|
|
784
|
+
}
|
|
785
|
+
function failureEvidence({ logFile, logsDir, sinceTs }) {
|
|
786
|
+
const supTail = readLogTail(logFile);
|
|
787
|
+
const lines = supTail.length > 0 ? [...supTail, `Supervisor log: ${logFile}`] : [];
|
|
788
|
+
let errors = [];
|
|
789
|
+
let all = [];
|
|
790
|
+
try {
|
|
791
|
+
errors = queryLogs({
|
|
792
|
+
dir: logsDir,
|
|
793
|
+
minLevel: "error"
|
|
794
|
+
});
|
|
795
|
+
all = queryLogs({ dir: logsDir });
|
|
796
|
+
} catch {}
|
|
797
|
+
const since = (rs) => rs.filter((r) => typeof r.ts === "number" && r.ts >= sinceTs).slice(-8);
|
|
798
|
+
let recent = since(errors);
|
|
799
|
+
const fellBack = recent.length === 0;
|
|
800
|
+
if (fellBack) recent = since(all);
|
|
801
|
+
for (const r of recent) lines.push(`${String(r.src ?? "?")}: ${String(r.msg ?? "").split("\n")[0] ?? ""}`);
|
|
802
|
+
if (recent.length > 0) lines.push(fellBack ? "Full records: `stim logs`" : "Full records: `stim logs --errors`");
|
|
803
|
+
return lines;
|
|
804
|
+
}
|
|
805
|
+
function report({ json, out, port, supervisor, logsDir, alreadyRunning, waited }) {
|
|
806
|
+
const facts = startFacts({
|
|
807
|
+
port,
|
|
808
|
+
supervisor,
|
|
809
|
+
logsDir,
|
|
810
|
+
alreadyRunning
|
|
811
|
+
});
|
|
812
|
+
if (json) {
|
|
813
|
+
console.log(JSON.stringify(facts));
|
|
814
|
+
return facts;
|
|
815
|
+
}
|
|
816
|
+
const who = facts.supervisorPid ? `supervisor pid ${facts.supervisorPid}${facts.mode ? ` (${facts.mode})` : ""}` : "started outside Stim";
|
|
817
|
+
out(chalk.green(`OK: dev server on port ${port}, ${who}${alreadyRunning ? " (already running)" : ""} ${waited}`));
|
|
818
|
+
out(chalk.dim(phaseLine("logs", logsDir)));
|
|
819
|
+
return facts;
|
|
820
|
+
}
|
|
821
|
+
//#endregion
|
|
822
|
+
export { startCommand as default, failureEvidence, liveSupervisor, parseWait, readLogTail, registerStart, startFacts, supervisorEntry, tailLines, wantsExpoOwnTunnel };
|