stim 0.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/LICENSE +21 -0
- package/README.md +162 -0
- 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.d.mts +1 -0
- package/dist/android-cas-compiler.mjs +71 -0
- 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-oeH18ERr.mjs +66 -0
- package/dist/cache-manifest.d.mts +27 -0
- package/dist/cache-manifest.mjs +2 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +40 -0
- package/dist/collector-run.d.mts +119 -0
- package/dist/collector-run.mjs +541 -0
- package/dist/command-output-8wHMEde0.mjs +289 -0
- package/dist/config-D7uu8Gbq.mjs +381 -0
- 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/errors-B6W7FkxQ.mjs +15 -0
- package/dist/exec-bsN9MJXb.mjs +63 -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/ndjson-DcAtEx_K.mjs +131 -0
- package/dist/ndjson-DvHSsNU9.d.mts +16 -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/process-output-C7IlIzd7.mjs +40 -0
- package/dist/project-COj6nzzh.mjs +243 -0
- 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-BYHqEeyw.mjs +352 -0
- package/dist/server-expo-BEMr_m6p.mjs +312 -0
- 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-IYEY-zi8.mjs +268 -0
- 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 +84 -0
- package/dist/supervisor-run.mjs +210 -0
- 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 +61 -8
- package/shim/android-cas-pch.cmake +35 -0
- package/shim/android-cas.gradle +29 -0
- package/shim/android-cas.toolchain.cmake +12 -0
- package/shim/android-optimizations.gradle +30 -0
- package/shim/bundle-response.cjs +46 -0
- package/shim/bundle-response.d.cts +5 -0
- package/shim/expo-metro-config.cjs +188 -0
- package/shim/metro-cache-generation.cjs +20 -0
- package/shim/metro-cache-generation.d.cts +5 -0
- package/skill/SKILL.md +14 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { c as findEnclosingWorktreeRoot, f as getProject } from "./config-D7uu8Gbq.mjs";
|
|
2
|
+
import { g as repoRoot } from "./worktree-Dx3ZC5lS.mjs";
|
|
3
|
+
import { existsSync, readFileSync, readdirSync, realpathSync } from "fs";
|
|
4
|
+
import { basename, dirname, join, resolve } from "path";
|
|
5
|
+
import { createRequire } from "module";
|
|
6
|
+
//#region src/project.ts
|
|
7
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
8
|
+
function resolvePackageJson(projectRoot, name) {
|
|
9
|
+
try {
|
|
10
|
+
return requireFromHere.resolve(`${name}/package.json`, { paths: [projectRoot] });
|
|
11
|
+
} catch {
|
|
12
|
+
try {
|
|
13
|
+
const main = requireFromHere.resolve(name, { paths: [projectRoot] });
|
|
14
|
+
const marker = `/node_modules/${name}/`;
|
|
15
|
+
const at = main.lastIndexOf(marker);
|
|
16
|
+
if (at === -1) return null;
|
|
17
|
+
const candidate = join(main.slice(0, at + marker.length), "package.json");
|
|
18
|
+
return existsSync(candidate) ? candidate : null;
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function isPackageResolvable(projectRoot, name) {
|
|
25
|
+
return resolvePackageJson(projectRoot, name) !== null;
|
|
26
|
+
}
|
|
27
|
+
function projectShortcut(path, proj) {
|
|
28
|
+
if (proj?.label) return proj.label;
|
|
29
|
+
const rootPath = findEnclosingWorktreeRoot(path);
|
|
30
|
+
if (rootPath && rootPath !== path) return `${projectShortcut(rootPath, getProject(rootPath))}/${path.split("/").pop() || path}`;
|
|
31
|
+
return path.split("/").pop() || path;
|
|
32
|
+
}
|
|
33
|
+
function ownedDeviceLabel(projectPath) {
|
|
34
|
+
const app = realpathSync(projectPath);
|
|
35
|
+
const root = repoRoot(app);
|
|
36
|
+
const appLabel = basename(app);
|
|
37
|
+
if (!root) return appLabel;
|
|
38
|
+
const worktreeLabel = basename(realpathSync(root));
|
|
39
|
+
return worktreeLabel === appLabel ? appLabel : `${worktreeLabel}-${appLabel}`;
|
|
40
|
+
}
|
|
41
|
+
function findProjectRoot(startDir) {
|
|
42
|
+
let dir;
|
|
43
|
+
try {
|
|
44
|
+
dir = realpathSync(resolve(startDir));
|
|
45
|
+
} catch {
|
|
46
|
+
dir = resolve(startDir);
|
|
47
|
+
}
|
|
48
|
+
while (true) {
|
|
49
|
+
if (existsSync(join(dir, "package.json"))) return dir;
|
|
50
|
+
const parent = dirname(dir);
|
|
51
|
+
if (parent === dir) return null;
|
|
52
|
+
dir = parent;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function loadPackageJson(projectRoot) {
|
|
56
|
+
const p = join(projectRoot, "package.json");
|
|
57
|
+
if (!existsSync(p)) return {
|
|
58
|
+
pkg: null,
|
|
59
|
+
parseError: null
|
|
60
|
+
};
|
|
61
|
+
try {
|
|
62
|
+
return {
|
|
63
|
+
pkg: JSON.parse(readFileSync(p, "utf-8")),
|
|
64
|
+
parseError: null
|
|
65
|
+
};
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return {
|
|
68
|
+
pkg: null,
|
|
69
|
+
parseError: error?.message || String(error)
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function readPackageJson(projectRoot) {
|
|
74
|
+
return loadPackageJson(projectRoot).pkg;
|
|
75
|
+
}
|
|
76
|
+
const APP_DEPENDENCIES = ["react-native", "expo"];
|
|
77
|
+
function declaresAppDependency(pkg) {
|
|
78
|
+
if (!pkg || typeof pkg !== "object") return false;
|
|
79
|
+
const { dependencies, devDependencies } = pkg;
|
|
80
|
+
const deps = {
|
|
81
|
+
...dependencies,
|
|
82
|
+
...devDependencies
|
|
83
|
+
};
|
|
84
|
+
return APP_DEPENDENCIES.some((name) => name in deps);
|
|
85
|
+
}
|
|
86
|
+
function appProjectProblem(projectRoot) {
|
|
87
|
+
const file = join(projectRoot, "package.json");
|
|
88
|
+
const { pkg, parseError } = loadPackageJson(projectRoot);
|
|
89
|
+
if (parseError) return {
|
|
90
|
+
kind: "unreadable",
|
|
91
|
+
message: `${file} is not valid JSON (${parseError}), so Stim cannot tell whether this is a React Native or Expo app.`,
|
|
92
|
+
remedy: `Fix the JSON in ${file} -- an unfinished edit or a merge conflict leaves it unparseable -- then run the command again.`
|
|
93
|
+
};
|
|
94
|
+
if (declaresAppDependency(pkg)) return null;
|
|
95
|
+
return {
|
|
96
|
+
kind: "not-an-app",
|
|
97
|
+
message: `${file} depends on neither react-native nor expo, so this is not a React Native or Expo app.`,
|
|
98
|
+
remedy: "Run this from the app directory -- the one whose package.json depends on react-native or expo -- or from that directory inside a worktree."
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function detectIsExpo(projectRoot) {
|
|
102
|
+
const pkg = readPackageJson(projectRoot);
|
|
103
|
+
const iosScript = pkg?.scripts?.ios;
|
|
104
|
+
if (typeof iosScript === "string") {
|
|
105
|
+
if (/\bexpo\s+run:ios\b/.test(iosScript)) return true;
|
|
106
|
+
if (/\breact-native\s+run-ios\b/.test(iosScript)) return false;
|
|
107
|
+
}
|
|
108
|
+
if (!pkg) return false;
|
|
109
|
+
const hasExpoDep = "expo" in {
|
|
110
|
+
...pkg.dependencies,
|
|
111
|
+
...pkg.devDependencies
|
|
112
|
+
};
|
|
113
|
+
const resolvable = () => isPackageResolvable(projectRoot, "expo");
|
|
114
|
+
if (!hasExpoDep && !resolvable()) return false;
|
|
115
|
+
const appJson = readAppJson(projectRoot);
|
|
116
|
+
if (appJson?.expo) return true;
|
|
117
|
+
const text = readAppConfigText(projectRoot);
|
|
118
|
+
if (text && /\b(?:from\s+['"]expo['"]|expo\/config|ExpoConfig)\b/.test(text)) return true;
|
|
119
|
+
if (hasExpoDep && resolvable()) return true;
|
|
120
|
+
if (looksLikeExpoConfig(appJson)) return true;
|
|
121
|
+
if (podfileUsesExpoModules(projectRoot)) return true;
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
function looksLikeExpoConfig(appJson) {
|
|
125
|
+
if (!appJson || typeof appJson !== "object" || Array.isArray(appJson)) return false;
|
|
126
|
+
if (appJson.expo) return true;
|
|
127
|
+
if ([
|
|
128
|
+
"slug",
|
|
129
|
+
"plugins",
|
|
130
|
+
"sdkVersion",
|
|
131
|
+
"experiments",
|
|
132
|
+
"runtimeVersion",
|
|
133
|
+
"updates",
|
|
134
|
+
"buildCacheProvider",
|
|
135
|
+
"assetBundlePatterns",
|
|
136
|
+
"splash",
|
|
137
|
+
"orientation",
|
|
138
|
+
"userInterfaceStyle"
|
|
139
|
+
].some((key) => appJson[key] !== void 0)) return true;
|
|
140
|
+
return Boolean(appJson.extra && typeof appJson.extra === "object" && appJson.extra.eas);
|
|
141
|
+
}
|
|
142
|
+
function podfileUsesExpoModules(projectRoot) {
|
|
143
|
+
const p = join(projectRoot, "ios", "Podfile");
|
|
144
|
+
if (!existsSync(p)) return false;
|
|
145
|
+
try {
|
|
146
|
+
return /^[^#\n]*use_expo_modules!/m.test(readFileSync(p, "utf-8"));
|
|
147
|
+
} catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function readAppJson(projectRoot) {
|
|
152
|
+
const p = join(projectRoot, "app.json");
|
|
153
|
+
if (!existsSync(p)) return null;
|
|
154
|
+
try {
|
|
155
|
+
return JSON.parse(readFileSync(p, "utf-8"));
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function readAppConfigText(projectRoot) {
|
|
161
|
+
for (const name of [
|
|
162
|
+
"app.config.js",
|
|
163
|
+
"app.config.ts",
|
|
164
|
+
"app.config.cjs",
|
|
165
|
+
"app.config.mjs"
|
|
166
|
+
]) {
|
|
167
|
+
const p = join(projectRoot, name);
|
|
168
|
+
if (existsSync(p)) try {
|
|
169
|
+
return readFileSync(p, "utf-8");
|
|
170
|
+
} catch {}
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
function detectBundleId(projectRoot) {
|
|
175
|
+
const fromJson = readAppJson(projectRoot)?.expo?.ios?.bundleIdentifier;
|
|
176
|
+
if (typeof fromJson === "string" && fromJson) return fromJson;
|
|
177
|
+
const text = readAppConfigText(projectRoot);
|
|
178
|
+
if (text) {
|
|
179
|
+
const id = text.match(/bundleIdentifier\s*:\s*["']([^"']+)["']/)?.[1];
|
|
180
|
+
if (id) return id;
|
|
181
|
+
}
|
|
182
|
+
return detectBundleIdFromPbxproj(projectRoot);
|
|
183
|
+
}
|
|
184
|
+
function detectAndroidPackage(projectRoot) {
|
|
185
|
+
const fromJson = readAppJson(projectRoot)?.expo?.android?.package;
|
|
186
|
+
if (typeof fromJson === "string" && fromJson) return fromJson;
|
|
187
|
+
const text = readAppConfigText(projectRoot);
|
|
188
|
+
if (text) {
|
|
189
|
+
const pkg = text.match(/package\s*:\s*["']([^"']+)["']/)?.[1];
|
|
190
|
+
if (pkg) return pkg;
|
|
191
|
+
}
|
|
192
|
+
return detectAndroidPackageFromGradle(projectRoot);
|
|
193
|
+
}
|
|
194
|
+
function detectBundleIdFromPbxproj(projectRoot) {
|
|
195
|
+
const iosDir = join(projectRoot, "ios");
|
|
196
|
+
if (!existsSync(iosDir)) return null;
|
|
197
|
+
let entries;
|
|
198
|
+
try {
|
|
199
|
+
entries = readdirSync(iosDir, { withFileTypes: true });
|
|
200
|
+
} catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
for (const entry of entries) {
|
|
204
|
+
if (!entry.isDirectory() || !entry.name.endsWith(".xcodeproj")) continue;
|
|
205
|
+
const pbx = join(iosDir, entry.name, "project.pbxproj");
|
|
206
|
+
if (!existsSync(pbx)) continue;
|
|
207
|
+
let text;
|
|
208
|
+
try {
|
|
209
|
+
text = readFileSync(pbx, "utf-8");
|
|
210
|
+
} catch {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const concrete = [...text.matchAll(/PRODUCT_BUNDLE_IDENTIFIER\s*=\s*([^;\s"]+)\s*;/g)].map((m) => m[1]).filter((id) => !!id && !id.startsWith("$") && !id.includes("("));
|
|
214
|
+
if (concrete.length === 0) continue;
|
|
215
|
+
const counts = {};
|
|
216
|
+
for (const id of concrete) counts[id] = (counts[id] || 0) + 1;
|
|
217
|
+
let best = null, bestCount = 0, bestLen = Infinity;
|
|
218
|
+
for (const [id, count] of Object.entries(counts)) if (count > bestCount || count === bestCount && id.length < bestLen) {
|
|
219
|
+
best = id;
|
|
220
|
+
bestCount = count;
|
|
221
|
+
bestLen = id.length;
|
|
222
|
+
}
|
|
223
|
+
return best;
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
function detectAndroidPackageFromGradle(projectRoot) {
|
|
228
|
+
const gradle = join(projectRoot, "android", "app", "build.gradle");
|
|
229
|
+
if (!existsSync(gradle)) return null;
|
|
230
|
+
let text;
|
|
231
|
+
try {
|
|
232
|
+
text = readFileSync(gradle, "utf-8");
|
|
233
|
+
} catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
const nsId = text.match(/namespace\s+["']([^"']+)["']/)?.[1];
|
|
237
|
+
if (nsId) return nsId;
|
|
238
|
+
const appId = text.match(/applicationId\s+["']([^"']+)["']/)?.[1];
|
|
239
|
+
if (appId) return appId;
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
//#endregion
|
|
243
|
+
export { findProjectRoot as a, projectShortcut as c, detectIsExpo as i, resolvePackageJson as l, detectAndroidPackage as n, isPackageResolvable as o, detectBundleId as r, ownedDeviceLabel as s, appProjectProblem as t };
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { D as sameProcessRecord, O as waitForProcessExit, f as getProject, v as removeProject } from "./config-D7uu8Gbq.mjs";
|
|
2
|
+
import { T as workspaceDir, a as clearRemoteSession, c as readMetroTunnel, d as readRemoteSessionId, i as clearManagedMetroTunnel, p as readWorkspaceState } from "./state-IYEY-zi8.mjs";
|
|
3
|
+
import { i as resolveProjectMetro, n as killMetroTree, r as pidExists } from "./metro-B01H3EuD.mjs";
|
|
4
|
+
import { n as verifyCollectorOwnership } from "./ownership-D8HmSv2E.mjs";
|
|
5
|
+
import { d as resolveEasCliBin, h as teardownOwnedIosSim, m as teardownOwnedAvd, x as parkedMaxSetting } from "./remote-cache-DOi5-zLj.mjs";
|
|
6
|
+
import { l as releaseWorkspaceLeases } from "./device-lease-D-G3SgVe.mjs";
|
|
7
|
+
import { i as endRecordedSession } from "./device-remote-CCgkG82J.mjs";
|
|
8
|
+
import { r as stopTunnel, t as resolveSupervisorTarget } from "./ownership-B9QIdQOw.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
|
+
...park && !bound.error && bound.max > 0 ? { park: {
|
|
211
|
+
projectPath,
|
|
212
|
+
max: bound.max,
|
|
213
|
+
configuration: typeof android.poolConfiguration === "string" ? android.poolConfiguration : void 0
|
|
214
|
+
} } : {}
|
|
215
|
+
});
|
|
216
|
+
if (r.parkFallback) poolNotes.push(`could not park ${android.avdName}: ${r.parkFallback} -- deleted it instead`);
|
|
217
|
+
for (const failure of r.evictionFailures ?? []) poolNotes.push(failure);
|
|
218
|
+
if (r.parked) {
|
|
219
|
+
parkedDevices.push(r.parked);
|
|
220
|
+
evictedDevices.push(...r.evicted ?? []);
|
|
221
|
+
} else if (r.status === "torn-down") deletedDevices.push(android.avdName);
|
|
222
|
+
else if (r.status === "skipped") skippedDevices.push({
|
|
223
|
+
platform: "android",
|
|
224
|
+
name: android.avdName,
|
|
225
|
+
reason: `${r.reason} -- not touched`
|
|
226
|
+
});
|
|
227
|
+
else if (r.status === "failed") {
|
|
228
|
+
const entry = {
|
|
229
|
+
platform: "android",
|
|
230
|
+
name: android.avdName,
|
|
231
|
+
reason: `teardown failed: ${r.reason}`
|
|
232
|
+
};
|
|
233
|
+
skippedDevices.push(entry);
|
|
234
|
+
failedDevices.push(entry);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
deletedDevices,
|
|
239
|
+
parkedDevices,
|
|
240
|
+
evictedDevices,
|
|
241
|
+
poolNotes,
|
|
242
|
+
skippedDevices,
|
|
243
|
+
failedDevices
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
async function reclaimProject(path, { deleteOwnedDevices = false, parkOwnedDevices = false, preserveProjectRecord = false, stopSession = defaultStopSession, stopMetroTunnel = defaultStopMetroTunnel, releaseLeases = releaseWorkspaceLeases, verifyCollector = verifyCollectorOwnership } = {}) {
|
|
247
|
+
const project = getProject(path);
|
|
248
|
+
const dereferenced = describeDereferenced(project);
|
|
249
|
+
const initialState = readWorkspaceState(path);
|
|
250
|
+
let killedPid = null;
|
|
251
|
+
let skippedMetro = null;
|
|
252
|
+
let supervisorHeld = false;
|
|
253
|
+
const supervisor = resolveSupervisorTarget({
|
|
254
|
+
state: initialState?.supervisor,
|
|
255
|
+
record: project?.supervisor,
|
|
256
|
+
reservedPort: project?.metroPort
|
|
257
|
+
});
|
|
258
|
+
if (supervisor.status === "ours") {
|
|
259
|
+
if (killMetroTree(supervisor.pid, supervisor.processToken) && await waitForProcessExit(supervisor, 1e4)) killedPid = supervisor.pid;
|
|
260
|
+
else {
|
|
261
|
+
skippedMetro = `could not confirm supervisor pid ${supervisor.pid} exited`;
|
|
262
|
+
supervisorHeld = true;
|
|
263
|
+
}
|
|
264
|
+
} else if (supervisor.status === "unverified") {
|
|
265
|
+
skippedMetro = supervisor.reason ?? "supervisor identity could not be verified";
|
|
266
|
+
supervisorHeld = true;
|
|
267
|
+
} else if (typeof project?.metroPort === "number") {
|
|
268
|
+
if (!(await resolveProjectMetro(project.metroPort, path)).missing) skippedMetro = "Externally started server left alone; no verified Stim supervisor owns it";
|
|
269
|
+
}
|
|
270
|
+
const { skippedDevices: skippedCollectors, failedDevices: failedCollectors } = await reapCollectors(path, {
|
|
271
|
+
verify: verifyCollector,
|
|
272
|
+
collectors: initialState?.collectors ?? {}
|
|
273
|
+
});
|
|
274
|
+
function retainReplacementProcesses() {
|
|
275
|
+
const currentState = readWorkspaceState(path);
|
|
276
|
+
const currentProject = getProject(path);
|
|
277
|
+
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])))) {
|
|
278
|
+
supervisorHeld = true;
|
|
279
|
+
skippedMetro = "A replacement process appeared during cleanup; its ownership records are retained";
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
retainReplacementProcesses();
|
|
283
|
+
const { deletedDevices, parkedDevices, evictedDevices, poolNotes, skippedDevices, failedDevices } = deleteOwnedDevices && !supervisorHeld && failedCollectors.length === 0 ? reclaimOwnedDevices(project, path, { park: parkOwnedDevices }) : {
|
|
284
|
+
deletedDevices: [],
|
|
285
|
+
parkedDevices: [],
|
|
286
|
+
evictedDevices: [],
|
|
287
|
+
poolNotes: [],
|
|
288
|
+
skippedDevices: [],
|
|
289
|
+
failedDevices: []
|
|
290
|
+
};
|
|
291
|
+
skippedDevices.push(...skippedCollectors);
|
|
292
|
+
failedDevices.push(...failedCollectors);
|
|
293
|
+
const remote = reclaimRemoteSession(path, { stopSession });
|
|
294
|
+
if (remote.failed) {
|
|
295
|
+
skippedDevices.push(remote.failed);
|
|
296
|
+
failedDevices.push(remote.failed);
|
|
297
|
+
}
|
|
298
|
+
const tunnel = await reclaimMetroTunnel(path, { stopMetroTunnel });
|
|
299
|
+
if (tunnel.failed) {
|
|
300
|
+
skippedDevices.push(tunnel.failed);
|
|
301
|
+
failedDevices.push(tunnel.failed);
|
|
302
|
+
}
|
|
303
|
+
let releasedLeases = [];
|
|
304
|
+
try {
|
|
305
|
+
releasedLeases = releaseLeases(path);
|
|
306
|
+
} catch {
|
|
307
|
+
releasedLeases = [];
|
|
308
|
+
}
|
|
309
|
+
retainReplacementProcesses();
|
|
310
|
+
const removedWorkspaceDirs = [];
|
|
311
|
+
const failedWorkspaceDirs = [];
|
|
312
|
+
if (failedDevices.length === 0 && !supervisorHeld) {
|
|
313
|
+
const dir = workspaceDir(path);
|
|
314
|
+
if (existsSync(dir)) try {
|
|
315
|
+
rmSync(dir, {
|
|
316
|
+
recursive: true,
|
|
317
|
+
force: true
|
|
318
|
+
});
|
|
319
|
+
removedWorkspaceDirs.push(dir);
|
|
320
|
+
} catch {
|
|
321
|
+
failedWorkspaceDirs.push(dir);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const keptEntry = supervisorHeld || failedDevices.length > 0 || failedWorkspaceDirs.length > 0;
|
|
325
|
+
if (project && !keptEntry && !preserveProjectRecord) removeProject(path);
|
|
326
|
+
return {
|
|
327
|
+
path,
|
|
328
|
+
dereferenced,
|
|
329
|
+
killedPid,
|
|
330
|
+
skippedMetro,
|
|
331
|
+
metroPort: project?.metroPort ?? null,
|
|
332
|
+
deletedDevices,
|
|
333
|
+
parkedDevices,
|
|
334
|
+
evictedDevices,
|
|
335
|
+
poolNotes,
|
|
336
|
+
skippedDevices,
|
|
337
|
+
failedDevices,
|
|
338
|
+
keptEntry,
|
|
339
|
+
stoppedSession: remote.stopped,
|
|
340
|
+
stoppedTunnel: tunnel.stopped,
|
|
341
|
+
releasedLeases,
|
|
342
|
+
removedWorkspaceDirs,
|
|
343
|
+
failedWorkspaceDirs
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
//#endregion
|
|
347
|
+
export { reclaimProject as n, describeDereferenced as t };
|