stim 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -10
- package/dist/android-BW8i_YxU.mjs +204 -0
- package/dist/android-Bcmev2s2.mjs +2334 -0
- package/dist/android-K63_0UfF.mjs +727 -0
- package/dist/android-cas-D6HPZ6UQ.mjs +1078 -0
- package/dist/android-cas-compiler.mjs +2 -2
- package/dist/app-install-NFxUahnH.mjs +1149 -0
- package/dist/build-lock-BsVoLDaq.mjs +645 -0
- package/dist/build-slots-D0b6sJyo.mjs +152 -0
- package/dist/{cache-manifest-4tH64LQ9.mjs → cache-manifest-Cu4_Znw9.mjs} +2 -2
- package/dist/cache-manifest.mjs +1 -1
- package/dist/cli.mjs +24 -27166
- package/dist/collector-run.d.mts +1 -0
- package/dist/collector-run.mjs +7 -5
- package/dist/command-output-8wHMEde0.mjs +289 -0
- package/dist/{config-7kmtuhO2.mjs → config-CMtIaTk9.mjs} +44 -13
- package/dist/deps-BLBsTNDS.mjs +435 -0
- package/dist/dev-client-RpvXxCXV.mjs +620 -0
- package/dist/device-CoCPmazS.mjs +250 -0
- package/dist/device-lease-Dpik41RY.mjs +335 -0
- package/dist/device-pool-B5pEVa3t.mjs +408 -0
- package/dist/device-remote-E81KAdXf.mjs +1209 -0
- package/dist/doctor-CqPeMsSm.mjs +1130 -0
- package/dist/doctor-D_CX7ib5.mjs +530 -0
- package/dist/error-diagnostics-Dw8Gn3yw.mjs +636 -0
- package/dist/{exec-bsN9MJXb.mjs → exec-CyylIdq9.mjs} +4 -2
- package/dist/gc-B7KkDinF.mjs +1665 -0
- package/dist/guide-BS2Xc24o.mjs +4503 -0
- package/dist/ios-B7KLgIp5.mjs +2733 -0
- package/dist/ios-D1h1Di39.mjs +451 -0
- package/dist/ios-device-D3pzTKQE.mjs +81 -0
- package/dist/ios-device-xNt0Lg6v.mjs +413 -0
- package/dist/logs-DcX9AvRF.mjs +176 -0
- package/dist/logs-query-oqn47Ffx.mjs +273 -0
- package/dist/metro-COslRK_F.mjs +184 -0
- package/dist/metro-store-CDDZHtPl.mjs +92 -0
- package/dist/ownership-CGPart6T.mjs +60 -0
- package/dist/ownership-CYOyqxUo.mjs +475 -0
- package/dist/ownership-claim-7zRxxTfZ.mjs +569 -0
- package/dist/{project-Dit1CckW.mjs → project-BBTVABUz.mjs} +12 -3
- package/dist/reclaim-C5Jmuahx.mjs +348 -0
- package/dist/reload-Z8nKifAk.mjs +332 -0
- package/dist/remote-cache-DDmJ1Roq.mjs +994 -0
- package/dist/{server-bare-BmTVVmBB.mjs → server-bare-DEk37Mz2.mjs} +8 -2
- package/dist/{server-expo-CHJe1r6j.mjs → server-expo-C3ktAwUG.mjs} +6 -5
- package/dist/server-expo-Drd7ksHu.mjs +2 -0
- package/dist/settings-DwU1_U_q.mjs +708 -0
- package/dist/spawn-entry-nCE6m770.mjs +14 -0
- package/dist/start-BrY4EuIg.mjs +822 -0
- package/dist/{state-2nush3MK.mjs → state-CZ-ER-8d.mjs} +21 -22
- package/dist/stats-BCWEVLrM.mjs +307 -0
- package/dist/stats-BsF15Bti.mjs +62 -0
- package/dist/status-DFI1Hfpg.mjs +393 -0
- package/dist/stop-B7DR4awK.mjs +3 -0
- package/dist/stop-Btg770vV.mjs +591 -0
- package/dist/supervisor-run.d.mts +0 -1
- package/dist/supervisor-run.mjs +4 -4
- package/dist/workspace-process-lock-BKfdys1q.mjs +47 -0
- package/dist/worktree-BPTbI-LN.mjs +1322 -0
- package/dist/worktree-C79xx_kM.mjs +543 -0
- package/dist/xcode-DeKCribl.mjs +1832 -0
- package/package.json +5 -5
- package/shim/bundle-response.cjs +5 -0
- 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,348 @@
|
|
|
1
|
+
import { D as sameProcessRecord, O as waitForProcessExit, f as getProject, v as removeProject } from "./config-CMtIaTk9.mjs";
|
|
2
|
+
import { T as workspaceDir, a as clearRemoteSession, c as readMetroTunnel, d as readRemoteSessionId, i as clearManagedMetroTunnel, p as readWorkspaceState } from "./state-CZ-ER-8d.mjs";
|
|
3
|
+
import { i as resolveProjectMetro, n as killMetroTree, r as pidExists } from "./metro-COslRK_F.mjs";
|
|
4
|
+
import { n as verifyCollectorOwnership } from "./ownership-CGPart6T.mjs";
|
|
5
|
+
import { d as resolveEasCliBin, h as teardownOwnedIosSim, m as teardownOwnedAvd, x as parkedMaxSetting } from "./remote-cache-DDmJ1Roq.mjs";
|
|
6
|
+
import { l as releaseWorkspaceLeases } from "./device-lease-Dpik41RY.mjs";
|
|
7
|
+
import { i as endRecordedSession } from "./device-remote-E81KAdXf.mjs";
|
|
8
|
+
import { r as stopTunnel, t as resolveSupervisorTarget } from "./ownership-CYOyqxUo.mjs";
|
|
9
|
+
import { existsSync, rmSync } from "node:fs";
|
|
10
|
+
//#region src/reclaim.ts
|
|
11
|
+
async function reapCollectors(root, { verify = verifyCollectorOwnership, collectors }) {
|
|
12
|
+
const skippedDevices = [];
|
|
13
|
+
const failedDevices = [];
|
|
14
|
+
for (const [platform, record] of Object.entries(collectors)) {
|
|
15
|
+
const rec = record;
|
|
16
|
+
const pid = rec?.pid;
|
|
17
|
+
if (typeof pid !== "number" || pid <= 0 || pid === process.pid || !pidExists(pid)) continue;
|
|
18
|
+
const ownership = verify({
|
|
19
|
+
pid,
|
|
20
|
+
platform,
|
|
21
|
+
root,
|
|
22
|
+
expected: rec
|
|
23
|
+
});
|
|
24
|
+
if (ownership.status === "gone") continue;
|
|
25
|
+
if (ownership.status === "unverified") {
|
|
26
|
+
const name = `${platform} log collector (pid ${pid})`;
|
|
27
|
+
const entry = {
|
|
28
|
+
platform: platform === "android" ? "android" : "ios",
|
|
29
|
+
name,
|
|
30
|
+
reason: `${ownership.reason}; keeping the record without signalling the process`
|
|
31
|
+
};
|
|
32
|
+
skippedDevices.push(entry);
|
|
33
|
+
failedDevices.push(entry);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
process.kill(pid, "SIGTERM");
|
|
38
|
+
if (await waitForProcessExit(rec, 5e3)) continue;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error.code === "ESRCH") continue;
|
|
41
|
+
}
|
|
42
|
+
const entry = {
|
|
43
|
+
platform: platform === "android" ? "android" : "ios",
|
|
44
|
+
name: `${platform} log collector (pid ${pid})`,
|
|
45
|
+
reason: "Collector exit could not be confirmed; keeping its ownership record."
|
|
46
|
+
};
|
|
47
|
+
skippedDevices.push(entry);
|
|
48
|
+
failedDevices.push(entry);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
skippedDevices,
|
|
52
|
+
failedDevices
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function reclaimRemoteSession(root, { stopSession = defaultStopSession } = {}) {
|
|
56
|
+
const sessionId = readRemoteSessionId(root);
|
|
57
|
+
if (!sessionId) return {
|
|
58
|
+
stopped: null,
|
|
59
|
+
failed: null
|
|
60
|
+
};
|
|
61
|
+
let result;
|
|
62
|
+
try {
|
|
63
|
+
result = stopSession(root, sessionId);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
result = {
|
|
66
|
+
status: "failed",
|
|
67
|
+
reason: String(err?.message ?? err)
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (result.status === "torn-down") {
|
|
71
|
+
if (result.reason) return {
|
|
72
|
+
stopped: sessionId,
|
|
73
|
+
failed: {
|
|
74
|
+
platform: "ios",
|
|
75
|
+
name: `remote session ${sessionId}`,
|
|
76
|
+
reason: `${result.reason} The session is stopped. Re-run cleanup to reconcile its retained ownership claim.`
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
clearRemoteSession(root, sessionId);
|
|
80
|
+
return {
|
|
81
|
+
stopped: sessionId,
|
|
82
|
+
failed: null
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
stopped: null,
|
|
87
|
+
failed: {
|
|
88
|
+
platform: "ios",
|
|
89
|
+
name: `remote session ${sessionId}`,
|
|
90
|
+
reason: `${result.reason ?? "stop failed"} -- it keeps billing until its max duration. Stop it by hand from any directory of this project: eas simulator:stop --id ${sessionId}`
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function defaultStopSession(root, sessionId) {
|
|
95
|
+
return endRecordedSession({
|
|
96
|
+
root,
|
|
97
|
+
sessionId,
|
|
98
|
+
easBin: resolveEasCliBin(root)?.file ?? null
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function defaultStopMetroTunnel(record) {
|
|
102
|
+
return stopTunnel(record);
|
|
103
|
+
}
|
|
104
|
+
async function reclaimMetroTunnel(root, { stopMetroTunnel = defaultStopMetroTunnel } = {}) {
|
|
105
|
+
const record = readMetroTunnel(root);
|
|
106
|
+
if (record?.kind !== "managed") return {
|
|
107
|
+
stopped: null,
|
|
108
|
+
failed: null
|
|
109
|
+
};
|
|
110
|
+
let result;
|
|
111
|
+
try {
|
|
112
|
+
result = await stopMetroTunnel(record);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
result = {
|
|
115
|
+
status: "failed",
|
|
116
|
+
reason: String(err?.message ?? err)
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (result.status === "failed") return {
|
|
120
|
+
stopped: null,
|
|
121
|
+
failed: {
|
|
122
|
+
platform: "ios",
|
|
123
|
+
name: `${record.provider} tunnel (pid ${record.pid})`,
|
|
124
|
+
reason: `${result.reason ?? "stop failed"} The process identity could not be verified or the stop could not be confirmed. The ownership record is kept. Inspect the process and retry \`stim worktree remove\`.`
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
if (!clearManagedMetroTunnel(root, record)) return {
|
|
128
|
+
stopped: null,
|
|
129
|
+
failed: {
|
|
130
|
+
platform: "ios",
|
|
131
|
+
name: "replacement managed tunnel",
|
|
132
|
+
reason: "A replacement managed tunnel record appeared during cleanup and is retained for a later stop."
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
return {
|
|
136
|
+
stopped: record.provider,
|
|
137
|
+
failed: null
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function describeDereferenced(project) {
|
|
141
|
+
const devices = [];
|
|
142
|
+
const ios = project?.platforms?.ios;
|
|
143
|
+
if (ios?.deviceUdid) devices.push(`ios sim ${ios.deviceUdid}`);
|
|
144
|
+
const android = project?.platforms?.android;
|
|
145
|
+
if (android?.avdName) devices.push(`android avd ${android.avdName}`);
|
|
146
|
+
else if (android?.serial) devices.push(`android device ${android.serial}`);
|
|
147
|
+
return devices;
|
|
148
|
+
}
|
|
149
|
+
function parkedIosCacheKey(lastBuild) {
|
|
150
|
+
if (lastBuild === null || typeof lastBuild !== "object" || Array.isArray(lastBuild)) return null;
|
|
151
|
+
const build = lastBuild;
|
|
152
|
+
return build.platform === "ios" && typeof build.cacheKey === "string" ? build.cacheKey : null;
|
|
153
|
+
}
|
|
154
|
+
function parkRequest(project, projectPath) {
|
|
155
|
+
const { max, error } = parkedMaxSetting("ios");
|
|
156
|
+
if (error || max <= 0) return void 0;
|
|
157
|
+
const cacheKey = parkedIosCacheKey(readWorkspaceState(projectPath)?.lastBuild);
|
|
158
|
+
return {
|
|
159
|
+
projectPath,
|
|
160
|
+
max,
|
|
161
|
+
bundleId: typeof project?.bundleId === "string" ? project.bundleId : null,
|
|
162
|
+
cacheKey,
|
|
163
|
+
simslimManaged: Boolean(project?.platforms?.ios?.simslimManaged)
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function reclaimOwnedDevices(project, projectPath, { park = false } = {}) {
|
|
167
|
+
const deletedDevices = [];
|
|
168
|
+
const parkedDevices = [];
|
|
169
|
+
const evictedDevices = [];
|
|
170
|
+
const poolNotes = [];
|
|
171
|
+
const skippedDevices = [];
|
|
172
|
+
const failedDevices = [];
|
|
173
|
+
const ios = project?.platforms?.ios;
|
|
174
|
+
if (ios?.owned && ios.deviceUdid) {
|
|
175
|
+
const udid = ios.deviceUdid;
|
|
176
|
+
const label = ios.deviceName || udid;
|
|
177
|
+
const r = teardownOwnedIosSim(udid, {
|
|
178
|
+
del: true,
|
|
179
|
+
label,
|
|
180
|
+
...park ? { park: parkRequest(project, projectPath) } : {}
|
|
181
|
+
});
|
|
182
|
+
if (r.parkFallback) poolNotes.push(`could not park ${label}: ${r.parkFallback} -- deleted it instead`);
|
|
183
|
+
for (const failure of r.evictionFailures ?? []) poolNotes.push(failure);
|
|
184
|
+
if (r.parked) {
|
|
185
|
+
parkedDevices.push(r.parked);
|
|
186
|
+
evictedDevices.push(...r.evicted ?? []);
|
|
187
|
+
} else if (r.status === "torn-down") deletedDevices.push(r.label);
|
|
188
|
+
if (r.status === "skipped") skippedDevices.push({
|
|
189
|
+
platform: "ios",
|
|
190
|
+
name: label,
|
|
191
|
+
udid,
|
|
192
|
+
reason: `${r.reason} -- not touched`
|
|
193
|
+
});
|
|
194
|
+
else if (r.status === "failed") {
|
|
195
|
+
const entry = {
|
|
196
|
+
platform: "ios",
|
|
197
|
+
name: label,
|
|
198
|
+
udid,
|
|
199
|
+
reason: `teardown failed: ${r.reason}`
|
|
200
|
+
};
|
|
201
|
+
skippedDevices.push(entry);
|
|
202
|
+
failedDevices.push(entry);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const android = project?.platforms?.android;
|
|
206
|
+
if (android?.owned && android.avdName) {
|
|
207
|
+
const bound = parkedMaxSetting("android");
|
|
208
|
+
const r = teardownOwnedAvd(android.avdName, {
|
|
209
|
+
del: true,
|
|
210
|
+
owner: { projectPath },
|
|
211
|
+
...park && !bound.error && bound.max > 0 ? { park: {
|
|
212
|
+
projectPath,
|
|
213
|
+
max: bound.max,
|
|
214
|
+
configuration: typeof android.poolConfiguration === "string" ? android.poolConfiguration : void 0
|
|
215
|
+
} } : {}
|
|
216
|
+
});
|
|
217
|
+
if (r.parkFallback) poolNotes.push(`could not park ${android.avdName}: ${r.parkFallback} -- deleted it instead`);
|
|
218
|
+
for (const failure of r.evictionFailures ?? []) poolNotes.push(failure);
|
|
219
|
+
if (r.parked) {
|
|
220
|
+
parkedDevices.push(r.parked);
|
|
221
|
+
evictedDevices.push(...r.evicted ?? []);
|
|
222
|
+
} else if (r.status === "torn-down") deletedDevices.push(android.avdName);
|
|
223
|
+
else if (r.status === "skipped") skippedDevices.push({
|
|
224
|
+
platform: "android",
|
|
225
|
+
name: android.avdName,
|
|
226
|
+
reason: `${r.reason} -- not touched`
|
|
227
|
+
});
|
|
228
|
+
else if (r.status === "failed") {
|
|
229
|
+
const entry = {
|
|
230
|
+
platform: "android",
|
|
231
|
+
name: android.avdName,
|
|
232
|
+
reason: `teardown failed: ${r.reason}`
|
|
233
|
+
};
|
|
234
|
+
skippedDevices.push(entry);
|
|
235
|
+
failedDevices.push(entry);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
deletedDevices,
|
|
240
|
+
parkedDevices,
|
|
241
|
+
evictedDevices,
|
|
242
|
+
poolNotes,
|
|
243
|
+
skippedDevices,
|
|
244
|
+
failedDevices
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
async function reclaimProject(path, { deleteOwnedDevices = false, parkOwnedDevices = false, preserveProjectRecord = false, stopSession = defaultStopSession, stopMetroTunnel = defaultStopMetroTunnel, releaseLeases = releaseWorkspaceLeases, verifyCollector = verifyCollectorOwnership } = {}) {
|
|
248
|
+
const project = getProject(path);
|
|
249
|
+
const dereferenced = describeDereferenced(project);
|
|
250
|
+
const initialState = readWorkspaceState(path);
|
|
251
|
+
let killedPid = null;
|
|
252
|
+
let skippedMetro = null;
|
|
253
|
+
let supervisorHeld = false;
|
|
254
|
+
const supervisor = resolveSupervisorTarget({
|
|
255
|
+
state: initialState?.supervisor,
|
|
256
|
+
record: project?.supervisor,
|
|
257
|
+
reservedPort: project?.metroPort
|
|
258
|
+
});
|
|
259
|
+
if (supervisor.status === "ours") {
|
|
260
|
+
if (killMetroTree(supervisor.pid, supervisor.processToken) && await waitForProcessExit(supervisor, 1e4)) killedPid = supervisor.pid;
|
|
261
|
+
else {
|
|
262
|
+
skippedMetro = `could not confirm supervisor pid ${supervisor.pid} exited`;
|
|
263
|
+
supervisorHeld = true;
|
|
264
|
+
}
|
|
265
|
+
} else if (supervisor.status === "unverified") {
|
|
266
|
+
skippedMetro = supervisor.reason ?? "supervisor identity could not be verified";
|
|
267
|
+
supervisorHeld = true;
|
|
268
|
+
} else if (typeof project?.metroPort === "number") {
|
|
269
|
+
if (!(await resolveProjectMetro(project.metroPort, path)).missing) skippedMetro = "Externally started server left alone; no verified Stim supervisor owns it";
|
|
270
|
+
}
|
|
271
|
+
const { skippedDevices: skippedCollectors, failedDevices: failedCollectors } = await reapCollectors(path, {
|
|
272
|
+
verify: verifyCollector,
|
|
273
|
+
collectors: initialState?.collectors ?? {}
|
|
274
|
+
});
|
|
275
|
+
function retainReplacementProcesses() {
|
|
276
|
+
const currentState = readWorkspaceState(path);
|
|
277
|
+
const currentProject = getProject(path);
|
|
278
|
+
if (Boolean(currentState?.supervisor && !sameProcessRecord(currentState.supervisor, initialState?.supervisor) || currentProject?.supervisor && !sameProcessRecord(currentProject.supervisor, project?.supervisor) || Object.entries(currentState?.collectors ?? {}).some(([platform, record]) => !sameProcessRecord(record, initialState?.collectors?.[platform])))) {
|
|
279
|
+
supervisorHeld = true;
|
|
280
|
+
skippedMetro = "A replacement process appeared during cleanup; its ownership records are retained";
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
retainReplacementProcesses();
|
|
284
|
+
const { deletedDevices, parkedDevices, evictedDevices, poolNotes, skippedDevices, failedDevices } = deleteOwnedDevices && !supervisorHeld && failedCollectors.length === 0 ? reclaimOwnedDevices(project, path, { park: parkOwnedDevices }) : {
|
|
285
|
+
deletedDevices: [],
|
|
286
|
+
parkedDevices: [],
|
|
287
|
+
evictedDevices: [],
|
|
288
|
+
poolNotes: [],
|
|
289
|
+
skippedDevices: [],
|
|
290
|
+
failedDevices: []
|
|
291
|
+
};
|
|
292
|
+
skippedDevices.push(...skippedCollectors);
|
|
293
|
+
failedDevices.push(...failedCollectors);
|
|
294
|
+
const remote = reclaimRemoteSession(path, { stopSession });
|
|
295
|
+
if (remote.failed) {
|
|
296
|
+
skippedDevices.push(remote.failed);
|
|
297
|
+
failedDevices.push(remote.failed);
|
|
298
|
+
}
|
|
299
|
+
const tunnel = await reclaimMetroTunnel(path, { stopMetroTunnel });
|
|
300
|
+
if (tunnel.failed) {
|
|
301
|
+
skippedDevices.push(tunnel.failed);
|
|
302
|
+
failedDevices.push(tunnel.failed);
|
|
303
|
+
}
|
|
304
|
+
let releasedLeases = [];
|
|
305
|
+
try {
|
|
306
|
+
releasedLeases = releaseLeases(path);
|
|
307
|
+
} catch {
|
|
308
|
+
releasedLeases = [];
|
|
309
|
+
}
|
|
310
|
+
retainReplacementProcesses();
|
|
311
|
+
const removedWorkspaceDirs = [];
|
|
312
|
+
const failedWorkspaceDirs = [];
|
|
313
|
+
if (failedDevices.length === 0 && !supervisorHeld) {
|
|
314
|
+
const dir = workspaceDir(path);
|
|
315
|
+
if (existsSync(dir)) try {
|
|
316
|
+
rmSync(dir, {
|
|
317
|
+
recursive: true,
|
|
318
|
+
force: true
|
|
319
|
+
});
|
|
320
|
+
removedWorkspaceDirs.push(dir);
|
|
321
|
+
} catch {
|
|
322
|
+
failedWorkspaceDirs.push(dir);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const keptEntry = supervisorHeld || failedDevices.length > 0 || failedWorkspaceDirs.length > 0;
|
|
326
|
+
if (project && !keptEntry && !preserveProjectRecord) removeProject(path);
|
|
327
|
+
return {
|
|
328
|
+
path,
|
|
329
|
+
dereferenced,
|
|
330
|
+
killedPid,
|
|
331
|
+
skippedMetro,
|
|
332
|
+
metroPort: project?.metroPort ?? null,
|
|
333
|
+
deletedDevices,
|
|
334
|
+
parkedDevices,
|
|
335
|
+
evictedDevices,
|
|
336
|
+
poolNotes,
|
|
337
|
+
skippedDevices,
|
|
338
|
+
failedDevices,
|
|
339
|
+
keptEntry,
|
|
340
|
+
stoppedSession: remote.stopped,
|
|
341
|
+
stoppedTunnel: tunnel.stopped,
|
|
342
|
+
releasedLeases,
|
|
343
|
+
removedWorkspaceDirs,
|
|
344
|
+
failedWorkspaceDirs
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
//#endregion
|
|
348
|
+
export { reclaimProject as n, describeDereferenced as t };
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { f as getProject } from "./config-CMtIaTk9.mjs";
|
|
2
|
+
import { f as readWorkspaceLaunches } from "./state-CZ-ER-8d.mjs";
|
|
3
|
+
import { a as findProjectRoot } from "./project-BBTVABUz.mjs";
|
|
4
|
+
import { i as resolveProjectMetro } from "./metro-COslRK_F.mjs";
|
|
5
|
+
import { l as phaseLine } from "./command-output-8wHMEde0.mjs";
|
|
6
|
+
import { u as androidAppProcess, y as iosAppProcess } from "./app-install-NFxUahnH.mjs";
|
|
7
|
+
import { x as resolveOwnedIosSim } from "./ios-D1h1Di39.mjs";
|
|
8
|
+
import { N as resolveOwnedAvdSerial } from "./android-K63_0UfF.mjs";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
import { WebSocket } from "ws";
|
|
11
|
+
//#region src/engine/reload.ts
|
|
12
|
+
const METRO_TIMEOUT_MS = 2e3;
|
|
13
|
+
function describe$1(error) {
|
|
14
|
+
return error instanceof Error ? error.message : String(error);
|
|
15
|
+
}
|
|
16
|
+
const ROLE_NAMES = {
|
|
17
|
+
ios: "iOS",
|
|
18
|
+
android: "Android"
|
|
19
|
+
};
|
|
20
|
+
function peerQuery(metadata) {
|
|
21
|
+
if (typeof metadata === "string") return new URLSearchParams(metadata);
|
|
22
|
+
if (!metadata || typeof metadata !== "object") return null;
|
|
23
|
+
const query = new URLSearchParams();
|
|
24
|
+
for (const [key, value] of Object.entries(metadata)) if (typeof value === "string") query.set(key, value);
|
|
25
|
+
return query;
|
|
26
|
+
}
|
|
27
|
+
function peerMatches(query, role, appId) {
|
|
28
|
+
if (role === "ios") return query.get("role")?.toLowerCase() === "ios";
|
|
29
|
+
return query.get("app") === appId;
|
|
30
|
+
}
|
|
31
|
+
function reloadThroughMetro(port, { role, appId, timeoutMs = METRO_TIMEOUT_MS }) {
|
|
32
|
+
const roleName = ROLE_NAMES[role];
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
const requestId = `stim-reload-${process.pid}-${Date.now()}`;
|
|
35
|
+
const socket = new WebSocket(`ws://127.0.0.1:${port}/message`);
|
|
36
|
+
let settled = false;
|
|
37
|
+
const finish = (result) => {
|
|
38
|
+
if (settled) return;
|
|
39
|
+
settled = true;
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
socket.close();
|
|
42
|
+
resolve(result);
|
|
43
|
+
};
|
|
44
|
+
const timer = setTimeout(() => finish({
|
|
45
|
+
failed: true,
|
|
46
|
+
unreachable: true,
|
|
47
|
+
reason: `Metro did not answer on port ${port}.`
|
|
48
|
+
}), timeoutMs);
|
|
49
|
+
socket.once("open", () => {
|
|
50
|
+
socket.send(JSON.stringify({
|
|
51
|
+
version: 2,
|
|
52
|
+
method: "getpeers",
|
|
53
|
+
target: "server",
|
|
54
|
+
id: requestId
|
|
55
|
+
}));
|
|
56
|
+
});
|
|
57
|
+
socket.on("message", (data) => {
|
|
58
|
+
let message;
|
|
59
|
+
try {
|
|
60
|
+
message = JSON.parse(data.toString());
|
|
61
|
+
} catch {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (message.id !== requestId) return;
|
|
65
|
+
if (message.result && typeof message.result === "object") {
|
|
66
|
+
const entries = Object.entries(message.result);
|
|
67
|
+
const peers = entries.length;
|
|
68
|
+
const rolePeers = entries.filter(([, metadata]) => {
|
|
69
|
+
const query = peerQuery(metadata);
|
|
70
|
+
return query ? peerMatches(query, role, appId) : false;
|
|
71
|
+
});
|
|
72
|
+
if (rolePeers.length === 0) {
|
|
73
|
+
socket.send(JSON.stringify({
|
|
74
|
+
version: 2,
|
|
75
|
+
method: "reload"
|
|
76
|
+
}));
|
|
77
|
+
const others = peers === 0 ? " Nothing at all is connected to it, so the broadcast Stim sent anyway reached nothing." : ` Metro has ${peers} other connected client${peers === 1 ? "" : "s"}, and Stim broadcast a reload to them in case one is this app unmatched.`;
|
|
78
|
+
finish({
|
|
79
|
+
failed: true,
|
|
80
|
+
noPeer: true,
|
|
81
|
+
peers,
|
|
82
|
+
reason: `No ${roleName} React Native app is connected to Metro on port ${port}.${others}`
|
|
83
|
+
});
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
for (const [id] of rolePeers) socket.send(JSON.stringify({
|
|
87
|
+
version: 2,
|
|
88
|
+
method: "reload",
|
|
89
|
+
target: id
|
|
90
|
+
}));
|
|
91
|
+
finish({
|
|
92
|
+
ok: true,
|
|
93
|
+
peers,
|
|
94
|
+
targets: rolePeers.length
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
socket.send(JSON.stringify({
|
|
99
|
+
version: 2,
|
|
100
|
+
method: "reload"
|
|
101
|
+
}));
|
|
102
|
+
finish({
|
|
103
|
+
ok: true,
|
|
104
|
+
broadcast: true
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
socket.once("error", (error) => finish({
|
|
108
|
+
failed: true,
|
|
109
|
+
unreachable: true,
|
|
110
|
+
reason: `Metro reload failed: ${describe$1(error)}`
|
|
111
|
+
}));
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/commands/reload.ts
|
|
116
|
+
const DEFAULT_DEPS = {
|
|
117
|
+
findProjectRoot,
|
|
118
|
+
getProject,
|
|
119
|
+
readLaunches: readWorkspaceLaunches,
|
|
120
|
+
resolveIos: resolveOwnedIosSim,
|
|
121
|
+
resolveAndroid: resolveOwnedAvdSerial,
|
|
122
|
+
iosProcess: iosAppProcess,
|
|
123
|
+
androidProcess: androidAppProcess,
|
|
124
|
+
resolveMetro: resolveProjectMetro,
|
|
125
|
+
reloadMetro: reloadThroughMetro
|
|
126
|
+
};
|
|
127
|
+
function failure(code, message, remedy) {
|
|
128
|
+
return {
|
|
129
|
+
code,
|
|
130
|
+
message,
|
|
131
|
+
remedy
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function describe(error) {
|
|
135
|
+
return error instanceof Error ? error.message : String(error);
|
|
136
|
+
}
|
|
137
|
+
function processFailure(platform, record, d) {
|
|
138
|
+
const process = platform === "ios" ? d.iosProcess(record.deviceId, record.appId) : d.androidProcess(record.deviceId, record.appId);
|
|
139
|
+
if (process === void 0) {
|
|
140
|
+
const probe = platform === "ios" ? `xcrun simctl spawn ${record.deviceId} launchctl list` : `adb -s ${record.deviceId} shell pidof ${record.appId}`;
|
|
141
|
+
return failure("STIM_RELOAD_PROBE_FAILED", `Stim could not determine whether ${record.appId} is running on ${record.deviceId}.`, `Run \`${probe}\` and retry when the device tool responds.`);
|
|
142
|
+
}
|
|
143
|
+
if (process === null) return failure("STIM_RELOAD_STOPPED", `${record.appId} is not running on ${record.deviceId}.`, `Run \`stim ${platform}\` to launch it.`);
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
function inspectTarget(platform, record, project, d) {
|
|
147
|
+
if (platform === "ios") {
|
|
148
|
+
const configured = project.platforms?.ios;
|
|
149
|
+
if (!configured?.owned || configured.deviceUdid !== record.deviceId) return {
|
|
150
|
+
platform,
|
|
151
|
+
error: failure("STIM_RELOAD_UNOWNED", `The recorded iOS launch on ${record.deviceId} is not this workspace's current owned simulator.`, "Run `stim ios` to launch the app on this workspace's owned simulator.")
|
|
152
|
+
};
|
|
153
|
+
let resolved;
|
|
154
|
+
try {
|
|
155
|
+
resolved = d.resolveIos(record.deviceId);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
return {
|
|
158
|
+
platform,
|
|
159
|
+
error: failure("STIM_RELOAD_PROBE_FAILED", `Stim could not inspect iOS simulator ${record.deviceId}: ${describe(error)}`, "Run `xcrun simctl list devices` and retry when simctl responds.")
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (!resolved.sim || resolved.sim.state !== "Booted") return {
|
|
163
|
+
platform,
|
|
164
|
+
error: failure("STIM_RELOAD_STOPPED", `The recorded iOS app is not running on a booted owned simulator.`, "Run `stim ios` to boot, install, and launch it.")
|
|
165
|
+
};
|
|
166
|
+
const stopped = processFailure(platform, record, d);
|
|
167
|
+
if (stopped) return {
|
|
168
|
+
platform,
|
|
169
|
+
error: stopped
|
|
170
|
+
};
|
|
171
|
+
return {
|
|
172
|
+
platform,
|
|
173
|
+
record,
|
|
174
|
+
deviceName: resolved.sim.name
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
const configured = project.platforms?.android;
|
|
178
|
+
if (!configured?.owned || !configured.avdName) return {
|
|
179
|
+
platform,
|
|
180
|
+
error: failure("STIM_RELOAD_UNOWNED", `The recorded Android launch on ${record.deviceId} is not this workspace's current owned emulator.`, "Run `stim android` to launch the app on this workspace's owned emulator.")
|
|
181
|
+
};
|
|
182
|
+
let resolved;
|
|
183
|
+
try {
|
|
184
|
+
resolved = d.resolveAndroid(configured.avdName);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
return {
|
|
187
|
+
platform,
|
|
188
|
+
error: failure("STIM_RELOAD_PROBE_FAILED", `Stim could not inspect Android emulator ${configured.avdName}: ${describe(error)}`, "Run `adb devices` and retry when adb responds.")
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
if (!resolved.serial || resolved.serial !== record.deviceId) return {
|
|
192
|
+
platform,
|
|
193
|
+
error: failure("STIM_RELOAD_STOPPED", `The recorded Android app is not running on this workspace's owned emulator.`, "Run `stim android` to boot, install, and launch it.")
|
|
194
|
+
};
|
|
195
|
+
const stopped = processFailure(platform, record, d);
|
|
196
|
+
if (stopped) return {
|
|
197
|
+
platform,
|
|
198
|
+
error: stopped
|
|
199
|
+
};
|
|
200
|
+
return {
|
|
201
|
+
platform,
|
|
202
|
+
record,
|
|
203
|
+
deviceName: configured.avdName
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function isTargetFailure(value) {
|
|
207
|
+
return "error" in value;
|
|
208
|
+
}
|
|
209
|
+
async function runReload({ root, platform = null, deps = {} }) {
|
|
210
|
+
const d = {
|
|
211
|
+
...DEFAULT_DEPS,
|
|
212
|
+
...deps
|
|
213
|
+
};
|
|
214
|
+
const project = d.getProject(root);
|
|
215
|
+
if (!project) return {
|
|
216
|
+
ok: false,
|
|
217
|
+
error: failure("STIM_NO_PROJECT", `No Stim environment is registered for ${root}.`, "Run `stim start` first.")
|
|
218
|
+
};
|
|
219
|
+
const launches = d.readLaunches(root);
|
|
220
|
+
const inspected = (platform ? [platform] : ["ios", "android"]).flatMap((candidate) => {
|
|
221
|
+
const record = launches[candidate];
|
|
222
|
+
return record ? [inspectTarget(candidate, record, project, d)] : [];
|
|
223
|
+
});
|
|
224
|
+
const live = inspected.filter((target) => !isTargetFailure(target));
|
|
225
|
+
if (live.length > 1) return {
|
|
226
|
+
ok: false,
|
|
227
|
+
error: failure("STIM_RELOAD_AMBIGUOUS", "Both the iOS and Android apps are running.", "Choose one with `stim reload ios` or `stim reload android`.")
|
|
228
|
+
};
|
|
229
|
+
if (live.length === 0) return {
|
|
230
|
+
ok: false,
|
|
231
|
+
error: inspected.find(isTargetFailure)?.error ?? failure("STIM_RELOAD_STOPPED", platform ? `No ${platform} app launch is recorded for this workspace.` : "No live app launch is recorded.", platform ? `Run \`stim ${platform}\` first.` : "Run `stim ios` or `stim android` first.")
|
|
232
|
+
};
|
|
233
|
+
const target = live[0];
|
|
234
|
+
if (target.record.release) return {
|
|
235
|
+
ok: false,
|
|
236
|
+
error: failure("STIM_RELOAD_RELEASE", `${target.record.appId} was launched with an embedded release bundle.`, `Run \`stim ${target.platform}\` with a Debug configuration or variant before reloading JavaScript.`)
|
|
237
|
+
};
|
|
238
|
+
const port = target.record.metroPort;
|
|
239
|
+
if (!port || project.metroPort !== port) return {
|
|
240
|
+
ok: false,
|
|
241
|
+
error: failure("STIM_NO_METRO", `The launch does not point at this workspace's current Metro reservation.`, "Run `stim start`, then launch the app again.")
|
|
242
|
+
};
|
|
243
|
+
if (!(await d.resolveMetro(port, root)).metro) return {
|
|
244
|
+
ok: false,
|
|
245
|
+
error: failure("STIM_NO_METRO", `Port ${port} is not serving this workspace's Metro.`, "Run `stim start`, then retry.")
|
|
246
|
+
};
|
|
247
|
+
const stopped = processFailure(target.platform, target.record, d);
|
|
248
|
+
if (stopped) return {
|
|
249
|
+
ok: false,
|
|
250
|
+
error: stopped
|
|
251
|
+
};
|
|
252
|
+
const reloaded = await d.reloadMetro(port, {
|
|
253
|
+
role: target.platform,
|
|
254
|
+
appId: target.record.appId
|
|
255
|
+
});
|
|
256
|
+
if (!reloaded.ok) {
|
|
257
|
+
const stoppedAfterMetro = processFailure(target.platform, target.record, d);
|
|
258
|
+
if (stoppedAfterMetro) return {
|
|
259
|
+
ok: false,
|
|
260
|
+
error: stoppedAfterMetro
|
|
261
|
+
};
|
|
262
|
+
const snapshot = `agent-device snapshot -i --platform ${target.platform} --${target.platform === "ios" ? "udid" : "serial"} ${target.record.deviceId}`;
|
|
263
|
+
const relaunch = `agent-device open ${target.record.appId} --platform ${target.platform} --${target.platform === "ios" ? "udid" : "serial"} ${target.record.deviceId} --metro-port ${port} --relaunch`;
|
|
264
|
+
const firstBundle = target.platform === "ios" ? " If it stays unreachable, an error in the first bundle leaves iOS without a packager connection at all, and no retry will make it a peer." : "";
|
|
265
|
+
const device = `reload from the device in your existing automation session: run \`${snapshot}\`, then press the error screen's Reload button, or open the dev menu and press Reload when no error screen is showing. Only when neither is reachable, run \`${relaunch}\`; that restarts the app and loses in-memory state. Keep your existing --session flag, verify the expected UI afterward, and do not create another session.`;
|
|
266
|
+
const remedy = reloaded.unreachable ? `Metro is registered for this workspace but did not answer on port ${port}, so nothing is known about ${target.record.appId}. Run \`stim reload ${target.platform}\` again. If it keeps timing out, run \`stim doctor\` to check the dev server before touching the app.` : `Metro reports no peer for ${target.record.appId} right now. Stim broadcast a reload anyway, so check the expected UI on ${target.record.deviceId} first -- it may already have recovered. If not, the app reconnects every 2 seconds, so run \`stim reload ${target.platform}\` once more.${firstBundle} Then ${device}`;
|
|
267
|
+
return {
|
|
268
|
+
ok: false,
|
|
269
|
+
error: failure("STIM_RELOAD_FAILED", reloaded.reason, remedy)
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const strategy = reloaded.broadcast ? "metro-broadcast" : "metro-websocket";
|
|
273
|
+
return {
|
|
274
|
+
ok: true,
|
|
275
|
+
facts: {
|
|
276
|
+
platform: target.platform,
|
|
277
|
+
deviceId: target.record.deviceId,
|
|
278
|
+
deviceName: target.deviceName,
|
|
279
|
+
appId: target.record.appId,
|
|
280
|
+
metroPort: port,
|
|
281
|
+
strategy,
|
|
282
|
+
targets: reloaded.targets ?? null
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function registerReload(program, deps = {}) {
|
|
287
|
+
program.command("reload [platform]").description("Request a JavaScript reload in this workspace's live app without observing completion. Specify ios or android when both are live.").option("--json", "Emit the reload facts as one JSON line").action(async (value, opts) => {
|
|
288
|
+
if (value !== void 0 && value !== "ios" && value !== "android") {
|
|
289
|
+
const error = failure("STIM_BAD_ARG", `Unknown reload platform ${JSON.stringify(value)}.`, "Use ios or android.");
|
|
290
|
+
if (opts.json) console.log(JSON.stringify(error));
|
|
291
|
+
else {
|
|
292
|
+
console.error(chalk.red(phaseLine("error", `${error.code}: ${error.message}`)));
|
|
293
|
+
if (error.remedy) console.error(phaseLine("remedy", error.remedy));
|
|
294
|
+
}
|
|
295
|
+
process.exitCode = 1;
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const root = (deps.findProjectRoot ?? DEFAULT_DEPS.findProjectRoot)(process.cwd());
|
|
299
|
+
if (!root) {
|
|
300
|
+
const error = failure("STIM_NO_PROJECT", "Not inside a project (no package.json found above the current directory).", null);
|
|
301
|
+
if (opts.json) console.log(JSON.stringify(error));
|
|
302
|
+
else console.error(chalk.red(phaseLine("error", `${error.code}: ${error.message}`)));
|
|
303
|
+
process.exitCode = 1;
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const result = await runReload({
|
|
307
|
+
root,
|
|
308
|
+
platform: value ?? null,
|
|
309
|
+
deps
|
|
310
|
+
});
|
|
311
|
+
if (!result.ok) {
|
|
312
|
+
if (opts.json) console.log(JSON.stringify(result.error));
|
|
313
|
+
else {
|
|
314
|
+
console.error(chalk.red(phaseLine("error", `${result.error.code}: ${result.error.message}`)));
|
|
315
|
+
if (result.error.remedy) console.error(phaseLine("remedy", result.error.remedy));
|
|
316
|
+
}
|
|
317
|
+
process.exitCode = 1;
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (opts.json) console.log(JSON.stringify(result.facts));
|
|
321
|
+
else {
|
|
322
|
+
const facts = result.facts;
|
|
323
|
+
const scope = facts.strategy === "metro-broadcast" ? ` This Metro cannot name its connected apps, so the reload request was broadcast to all of them and Stim cannot confirm ${facts.appId} was one. Check the expected UI on ${facts.deviceId}; if nothing changed, reload from the app's own error screen or dev menu.` : facts.targets && facts.targets > 1 ? ` ${facts.targets} devices are running this app on that Metro and the reload request addressed all of them, not only ${facts.deviceId}.` : "";
|
|
324
|
+
console.log(`Reload requested for ${facts.appId} on ${facts.deviceName} (${facts.deviceId}) via ${facts.strategy}; ${facts.platform} Metro port ${facts.metroPort}.${scope} Completion is not observed; verify the expected UI and run stim logs --errors.`);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
function reloadCommand(program) {
|
|
329
|
+
registerReload(program);
|
|
330
|
+
}
|
|
331
|
+
//#endregion
|
|
332
|
+
export { reloadCommand as default, registerReload, runReload };
|