stim 1.8.0 → 1.9.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 +23 -155
- package/dist/{android-BRgBYzU0.mjs → android-BPGizezh.mjs} +112 -135
- package/dist/{android-DeYdjjBP.mjs → android-DEgo4oCo.mjs} +2 -2
- package/dist/{android-DWssqHQ8.mjs → android-_y4Ohi5z.mjs} +54 -15
- package/dist/android-cas-compiler.mjs +2 -2
- package/dist/{android-cas-SWIjzeuL.mjs → android-cas-eiExNLf7.mjs} +21 -14
- package/dist/app-install-Uf3a83Tv.mjs +819 -0
- package/dist/{build-lock-CgtsB6TV.mjs → build-lock-DPXPLgTc.mjs} +120 -24
- package/dist/build-progress-CVqkfPC6.mjs +188 -0
- package/dist/{cache-manifest-CYJi8-Hx.mjs → cache-manifest-Dg7OnMrp.mjs} +4 -4
- package/dist/cache-manifest.mjs +1 -1
- package/dist/cli.mjs +22 -19
- package/dist/collector-run.d.mts +2 -0
- package/dist/collector-run.mjs +10 -9
- package/dist/{command-output-B3ycpsYv.mjs → command-output-gByVmIjy.mjs} +11 -2
- package/dist/{config-B7iyKWkL.mjs → config-Cp9lM3wX.mjs} +187 -57
- package/dist/{dependency-state-D0M7oKA7.mjs → dependency-state-Bq5DcLGn.mjs} +2 -2
- package/dist/{deps-oCXVNEqQ.mjs → deps-Cip5vUm1.mjs} +8 -5
- package/dist/{detached-entry-Bpo6ZY_I.mjs → detached-entry-KEu0_AXL.mjs} +1 -1
- package/dist/{device-BUoZyMoR.mjs → dev-client-HyqISM0h.mjs} +1178 -635
- package/dist/{device-RdujjHLB.mjs → device-D4U-xLty.mjs} +13 -20
- package/dist/{device-lease-Ffz_M5t0.mjs → device-lease-Crs-HkJY.mjs} +16 -9
- package/dist/{device-pool-BxRwdufO.mjs → device-pool-CnTAz8X1.mjs} +7 -7
- package/dist/{device-remote-ByN8P6CB.mjs → device-remote-AQ5rQPqr.mjs} +482 -35
- package/dist/{doctor-BMt156qe.mjs → doctor-B_Ei3bZu.mjs} +95 -107
- package/dist/{error-diagnostics-D1Kaf-eT.mjs → error-diagnostics-Vfz83GCF.mjs} +22 -16
- package/dist/exec-jIAlgilk.mjs +132 -0
- package/dist/{gc-v2YcoJvm.mjs → gc-Ckw8RQHi.mjs} +763 -126
- package/dist/{guide-BTAlD1_y.mjs → guide-BiAbhs1J.mjs} +891 -501
- package/dist/{guide-status-Craq9z0t.mjs → guide-status-BAMVU0ZA.mjs} +5 -3
- package/dist/{ios-BcCiI4TS.mjs → ios-BL2NFckr.mjs} +43 -20
- package/dist/{ios-BP04pY5f.mjs → ios-BzUJWk1K.mjs} +141 -175
- package/dist/{ios-device-CauFJiYx.mjs → ios-device-D972i9wf.mjs} +149 -52
- package/dist/{ios-device-CFXA46ez.mjs → ios-device-DPsSm0v_.mjs} +3 -2
- package/dist/launch-verify-Q_OtW1Rf.mjs +365 -0
- package/dist/{logs-CakdBTtm.mjs → logs-CI9NQSK7.mjs} +18 -13
- package/dist/{logs-query-BGL-Pl12.mjs → logs-query-Cqma3WIu.mjs} +36 -28
- package/dist/{metro-C7bcJ0uo.mjs → metro-DrLHaMn2.mjs} +78 -8
- package/dist/{named-ports-DzMOlmaX.mjs → named-ports-CmLQSIRj.mjs} +4 -4
- package/dist/{ndjson-BZwI1NBv.mjs → ndjson-9uFNRwfe.mjs} +36 -3
- package/dist/{ownership-Cp52QAK1.mjs → ownership-D15Op8vN.mjs} +3 -3
- package/dist/{ownership-wolE-0Do.mjs → ownership-DCQPuWEc.mjs} +26 -7
- package/dist/{ports-BIbJaKNW.mjs → ports-BNKoWVWZ.mjs} +3 -3
- package/dist/{process-output-Dl5uKE03.mjs → process-output-Fpl8PYi8.mjs} +1 -1
- package/dist/{project-DDZWf_16.mjs → project-DS112DYB.mjs} +8 -3
- package/dist/{reload-BdypPbjM.mjs → reload-ZNJUo1pl.mjs} +12 -13
- package/dist/{server-bare-BQkyxa_b.mjs → server-bare-DWkkcIxV.mjs} +2 -2
- package/dist/{server-expo-BVAA1OqJ.mjs → server-expo-B_lctBR3.mjs} +8 -7
- package/dist/server-expo-CeILEu86.mjs +2 -0
- package/dist/settings-BKNh5rl4.mjs +376 -0
- package/dist/{settings-BXXorVM2.mjs → settings-D030k0t4.mjs} +464 -152
- package/dist/settings.schema.json +989 -0
- package/dist/{teardown-BJqEzPo_.mjs → sim-pool-KaIsp2Ft.mjs} +14 -419
- package/dist/simslim-B_LsGLuJ.mjs +259 -0
- package/dist/{slot-launch-ZUUe0TQP.mjs → slot-launch-DU9A-gYw.mjs} +4 -4
- package/dist/spawn-claims-Dn3C_N6w.mjs +49 -0
- package/dist/{start-CmxtOKjU.mjs → start-DYkkKF0s.mjs} +27 -31
- package/dist/{state-DZobzxLW.mjs → state-NcJzMEBF.mjs} +5 -3
- package/dist/{stats-CLg5RuDS.mjs → stats-B63-JVAY.mjs} +4 -4
- package/dist/{stats-BYcZ6n1h.mjs → stats-DG9e66Rz.mjs} +66 -5
- package/dist/status-S6ehL4Ui.mjs +932 -0
- package/dist/{stop-C8JW6kKp.mjs → stop-BO28HGqF.mjs} +101 -25
- package/dist/{stop-B1kOpYei.mjs → stop-T1R-cZRz.mjs} +2 -2
- package/dist/supervisor-run.mjs +52 -34
- package/dist/{workspace-process-lock-BYfZz_S-.mjs → workspace-process-lock-DWaiRD0e.mjs} +14 -9
- package/dist/{workspace-state-BT5E3SA2.mjs → workspace-state-DF2gCFhx.mjs} +52 -29
- package/dist/worktree-DKhSF0UX.mjs +2 -0
- package/dist/{worktree-CFLA-6MX.mjs → worktree-LGB-UwgG.mjs} +603 -59
- package/dist/{worktree-FG_ZKKdb.mjs → worktree-R_cUlE7s.mjs} +62 -41
- package/dist/{xcode-B6YaWeg0.mjs → xcode-BZqyxh3e.mjs} +216 -58
- package/package.json +5 -6
- package/skill/SKILL.md +2 -0
- package/dist/app-install-BDbsSgCl.mjs +0 -1175
- package/dist/build-slots-C-oAHRT1.mjs +0 -151
- package/dist/dev-client-BmT-ioFI.mjs +0 -797
- package/dist/exec-B29XNuhP.mjs +0 -77
- package/dist/reclaim-KJ-0uWEF.mjs +0 -361
- package/dist/server-expo-YDDG3eP7.mjs +0 -2
- package/dist/status-DdkLki47.mjs +0 -426
- package/dist/{metro-store-CDDZHtPl.mjs → metro-store-DzeWkfqW.mjs} +1 -1
|
@@ -1,23 +1,514 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { a as
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
1
|
+
import { D as process_identity_exports, K as projectDeviceSlots, O as acquireAvdClaim, b as removeProject, f as getProject, g as loadConfig, h as isPathPrefix, i as clearDevice, k as ownership_claim_exports, u as getConfigDir, w as upsertProject } from "./config-Cp9lM3wX.mjs";
|
|
2
|
+
import { t as getExecutor } from "./exec-jIAlgilk.mjs";
|
|
3
|
+
import { _ as workspaceDir, i as recordWorkspaceUse, r as readWorkspaceState } from "./workspace-state-DF2gCFhx.mjs";
|
|
4
|
+
import { C as warmWorktreePaths, S as unpushedCommits, b as restoreFile, c as hasPopulatedSubmodules, d as isPodInstallChurn, f as listWorktrees, g as removeWorktree, h as readWorktreeExclude, i as depsOutOfSync, l as hasRemote, m as podsOutOfSync, n as cloneIgnoredEntries, o as dirtyPaths, p as locallyKnownUpstream, r as deleteBranch, s as gitCommonDir, t as branchExists, u as hasUncommittedWork, v as resolveFullRef, x as sourceCheckoutOf } from "./worktree-R_cUlE7s.mjs";
|
|
5
|
+
import { o as findProjectRoot } from "./project-DS112DYB.mjs";
|
|
6
|
+
import { a as clearRemoteSession, i as clearManagedMetroTunnel, l as readRemoteSession, s as readMetroTunnel, u as readRemoteSessionId } from "./state-NcJzMEBF.mjs";
|
|
7
|
+
import { a as pidExists, c as resolveProjectMetro, l as signalProcessTree, n as killMetroTree } from "./metro-DrLHaMn2.mjs";
|
|
8
|
+
import { n as verifyCollectorOwnership } from "./ownership-D15Op8vN.mjs";
|
|
9
|
+
import { F as unknownSettingKeys, M as settingShapeErrors, i as SETTING_SHAPE_REMEDY, k as resolveSettings } from "./settings-D030k0t4.mjs";
|
|
10
|
+
import { a as formatElapsed, h as shortUdid, i as formatDuration, l as phaseLine, p as releasedLeaseFact, u as plural } from "./command-output-gByVmIjy.mjs";
|
|
11
|
+
import { n as listBuildLocks } from "./build-lock-DPXPLgTc.mjs";
|
|
12
|
+
import { b as resolveEasCliBin, o as parkedMaxSetting, t as POOL_SETTING_REMEDY } from "./sim-pool-KaIsp2Ft.mjs";
|
|
13
|
+
import { n as workspaceProcessLockError, r as workspaceProcessLockPath, t as withWorkspaceProcessLock } from "./workspace-process-lock-DWaiRD0e.mjs";
|
|
14
|
+
import { l as releaseWorkspaceLeases } from "./device-lease-Crs-HkJY.mjs";
|
|
15
|
+
import { _ as listBuildSlots, h as podInstallCommand, n as HEARTBEAT_INTERVAL_MS } from "./xcode-BZqyxh3e.mjs";
|
|
16
|
+
import { E as teardownOwnedIosSim, T as teardownOwnedAvd, i as endRecordedSession } from "./device-remote-AQ5rQPqr.mjs";
|
|
17
|
+
import { n as hasInstalledDependencies, r as installedNpmTreeIsValid, t as dependencyState } from "./dependency-state-Bq5DcLGn.mjs";
|
|
18
|
+
import { t as clearNamedPorts } from "./named-ports-CmLQSIRj.mjs";
|
|
19
|
+
import { i as stopTunnel, l as withManagedTunnelRemovalLock, n as managedLockHolders, s as withManagedRemoteWorktreeRemovalLock, t as resolveSupervisorTarget } from "./ownership-DCQPuWEc.mjs";
|
|
20
|
+
import { a as runPodInstall, i as runCaptured, n as podsAreStale, r as readPodState, t as DEPS_ERROR } from "./deps-Cip5vUm1.mjs";
|
|
21
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
17
22
|
import { existsSync as existsSync$1, realpathSync as realpathSync$1 } from "fs";
|
|
18
|
-
import { basename
|
|
23
|
+
import { basename, dirname, isAbsolute, relative, resolve } from "path";
|
|
19
24
|
import { workspaceName } from "@stim-cli/core";
|
|
25
|
+
import { join as join$1, relative as relative$1 } from "node:path";
|
|
26
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
20
27
|
import chalk from "chalk";
|
|
28
|
+
//#region src/commands/gc/paths.ts
|
|
29
|
+
function canonicalPath(p) {
|
|
30
|
+
try {
|
|
31
|
+
return realpathSync$1(p);
|
|
32
|
+
} catch {
|
|
33
|
+
return resolve(p);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/workspace/in-use.ts
|
|
38
|
+
const NATIVE_RUN = "native-run";
|
|
39
|
+
const NATIVE_RUN_HELD = "a stim ios, android or stop run holds its native-run.lock";
|
|
40
|
+
function workspaceInUse(root, { supervisor = true, managedLocks = true, nativeRun = true } = {}) {
|
|
41
|
+
const reasons = [];
|
|
42
|
+
if (supervisor) {
|
|
43
|
+
const project = getProject(root);
|
|
44
|
+
const target = resolveSupervisorTarget({
|
|
45
|
+
state: readWorkspaceState(root)?.supervisor,
|
|
46
|
+
record: project?.supervisor,
|
|
47
|
+
reservedPort: project?.metroPort
|
|
48
|
+
});
|
|
49
|
+
if (target.status === "ours") reasons.push(`its dev server supervisor (pid ${target.pid}) is running`);
|
|
50
|
+
else if (target.status === "unverified") reasons.push(`its dev server supervisor cannot be verified: ${target.reason ?? "unknown identity"}`);
|
|
51
|
+
}
|
|
52
|
+
if (nativeRun) {
|
|
53
|
+
const claims = (0, ownership_claim_exports.readClaimSet)(workspaceProcessLockPath(workspaceDir(root), NATIVE_RUN, true));
|
|
54
|
+
if (claims.live.length) reasons.push(NATIVE_RUN_HELD);
|
|
55
|
+
else if (claims.unresolved[0]) reasons.push(`its native-run.lock cannot be resolved: ${claims.unresolved[0].reason}`);
|
|
56
|
+
}
|
|
57
|
+
const self = canonicalPath(root);
|
|
58
|
+
for (const [kind, entries] of [["build lock", listBuildLocks()], ["build slot", listBuildSlots()]]) for (const entry of entries) {
|
|
59
|
+
if (!entry.alive && !entry.unresolved) continue;
|
|
60
|
+
if (entry.projectRoot !== null && canonicalPath(entry.projectRoot) === self) reasons.push(`a ${entry.alive ? "live" : "unresolvable"} ${kind} at ${entry.path} names it`);
|
|
61
|
+
}
|
|
62
|
+
if (managedLocks) reasons.push(...managedLockHolders(root));
|
|
63
|
+
return reasons;
|
|
64
|
+
}
|
|
65
|
+
function emptyWorkspaceDir(dir) {
|
|
66
|
+
if (lstatSync(dir).isSymbolicLink()) {
|
|
67
|
+
unlinkSync(dir);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
for (const name of readdirSync(dir)) if (name !== `${NATIVE_RUN}.lock`) rmSync(join$1(dir, name), {
|
|
71
|
+
recursive: true,
|
|
72
|
+
force: true
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async function withIdleWorkspace(root, fn, { purpose, ...checks }) {
|
|
76
|
+
try {
|
|
77
|
+
return await withWorkspaceProcessLock(workspaceDir(root), NATIVE_RUN, async () => {
|
|
78
|
+
const reasons = workspaceInUse(root, {
|
|
79
|
+
...checks,
|
|
80
|
+
nativeRun: false
|
|
81
|
+
});
|
|
82
|
+
if (reasons.length) return {
|
|
83
|
+
ran: false,
|
|
84
|
+
reasons
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
ran: true,
|
|
88
|
+
value: await fn()
|
|
89
|
+
};
|
|
90
|
+
}, {
|
|
91
|
+
external: true,
|
|
92
|
+
waitMs: 0,
|
|
93
|
+
ownerPurpose: purpose
|
|
94
|
+
});
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (workspaceProcessLockError(error)) return {
|
|
97
|
+
ran: false,
|
|
98
|
+
reasons: [NATIVE_RUN_HELD]
|
|
99
|
+
};
|
|
100
|
+
if ((0, ownership_claim_exports.isClaimRefusal)(error) || (0, ownership_claim_exports.isClaimUnavailable)(error)) return {
|
|
101
|
+
ran: false,
|
|
102
|
+
reasons: [`its native-run.lock cannot be taken: ${error.reason}`]
|
|
103
|
+
};
|
|
104
|
+
throw error;
|
|
105
|
+
} finally {
|
|
106
|
+
try {
|
|
107
|
+
const dir = workspaceDir(root);
|
|
108
|
+
if (!lstatSync(dir).isSymbolicLink()) rmdirSync(dir);
|
|
109
|
+
} catch {}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/devices/reclaim.ts
|
|
114
|
+
async function reapCollectors(root, { verify = verifyCollectorOwnership, collectors }) {
|
|
115
|
+
const skippedDevices = [];
|
|
116
|
+
const failedDevices = [];
|
|
117
|
+
for (const [platform, record] of Object.entries(collectors)) {
|
|
118
|
+
const rec = record;
|
|
119
|
+
const pid = rec?.pid;
|
|
120
|
+
if (typeof pid !== "number" || pid <= 0 || pid === process.pid || !pidExists(pid)) continue;
|
|
121
|
+
const ownership = verify({
|
|
122
|
+
pid,
|
|
123
|
+
platform,
|
|
124
|
+
root,
|
|
125
|
+
expected: rec
|
|
126
|
+
});
|
|
127
|
+
if (ownership.status === "gone") continue;
|
|
128
|
+
if (ownership.status === "unverified") {
|
|
129
|
+
const name = `${platform} log collector (pid ${pid})`;
|
|
130
|
+
const entry = {
|
|
131
|
+
platform: platform.split(":")[0] === "android" ? "android" : "ios",
|
|
132
|
+
name,
|
|
133
|
+
reason: `${ownership.reason}; keeping the record without signalling the process`
|
|
134
|
+
};
|
|
135
|
+
skippedDevices.push(entry);
|
|
136
|
+
failedDevices.push(entry);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
process.kill(pid, "SIGTERM");
|
|
141
|
+
if (await (0, process_identity_exports.waitForProcessExit)(rec, 5e3)) continue;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (error.code === "ESRCH") continue;
|
|
144
|
+
}
|
|
145
|
+
const entry = {
|
|
146
|
+
platform: platform.split(":")[0] === "android" ? "android" : "ios",
|
|
147
|
+
name: `${platform} log collector (pid ${pid})`,
|
|
148
|
+
reason: "Collector exit could not be confirmed; keeping its ownership record."
|
|
149
|
+
};
|
|
150
|
+
skippedDevices.push(entry);
|
|
151
|
+
failedDevices.push(entry);
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
skippedDevices,
|
|
155
|
+
failedDevices
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function reclaimRemoteSession(root, { stopSession = defaultStopSession } = {}) {
|
|
159
|
+
const sessionId = readRemoteSessionId(root);
|
|
160
|
+
if (!sessionId) return {
|
|
161
|
+
stopped: null,
|
|
162
|
+
failed: null
|
|
163
|
+
};
|
|
164
|
+
let result;
|
|
165
|
+
try {
|
|
166
|
+
result = stopSession(root, sessionId);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
result = {
|
|
169
|
+
status: "failed",
|
|
170
|
+
reason: String(err?.message ?? err)
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (result.status === "torn-down") {
|
|
174
|
+
if (result.reason) return {
|
|
175
|
+
stopped: sessionId,
|
|
176
|
+
failed: {
|
|
177
|
+
platform: "ios",
|
|
178
|
+
name: `remote session ${sessionId}`,
|
|
179
|
+
reason: `${result.reason} The session is stopped. Re-run cleanup to reconcile its retained ownership claim.`
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
clearRemoteSession(root, sessionId);
|
|
183
|
+
return {
|
|
184
|
+
stopped: sessionId,
|
|
185
|
+
failed: null
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
stopped: null,
|
|
190
|
+
failed: {
|
|
191
|
+
platform: "ios",
|
|
192
|
+
name: `remote session ${sessionId}`,
|
|
193
|
+
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}`
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function defaultStopSession(root, sessionId) {
|
|
198
|
+
return endRecordedSession({
|
|
199
|
+
root,
|
|
200
|
+
sessionId,
|
|
201
|
+
easBin: resolveEasCliBin(root)?.file ?? null
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function defaultStopMetroTunnel(record) {
|
|
205
|
+
return stopTunnel(record);
|
|
206
|
+
}
|
|
207
|
+
async function reclaimMetroTunnel(root, { stopMetroTunnel = defaultStopMetroTunnel } = {}) {
|
|
208
|
+
const record = readMetroTunnel(root);
|
|
209
|
+
if (record?.kind !== "managed") return {
|
|
210
|
+
stopped: null,
|
|
211
|
+
failed: null
|
|
212
|
+
};
|
|
213
|
+
let result;
|
|
214
|
+
try {
|
|
215
|
+
result = await stopMetroTunnel(record);
|
|
216
|
+
} catch (err) {
|
|
217
|
+
result = {
|
|
218
|
+
status: "failed",
|
|
219
|
+
reason: String(err?.message ?? err)
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if (result.status === "failed") return {
|
|
223
|
+
stopped: null,
|
|
224
|
+
failed: {
|
|
225
|
+
platform: "ios",
|
|
226
|
+
name: `${record.provider} tunnel (pid ${record.pid})`,
|
|
227
|
+
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\`.`
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
if (!clearManagedMetroTunnel(root, record)) return {
|
|
231
|
+
stopped: null,
|
|
232
|
+
failed: {
|
|
233
|
+
platform: "ios",
|
|
234
|
+
name: "replacement managed tunnel",
|
|
235
|
+
reason: "A replacement managed tunnel record appeared during cleanup and is retained for a later stop."
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
return {
|
|
239
|
+
stopped: record.provider,
|
|
240
|
+
failed: null
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function describeDereferenced(project) {
|
|
244
|
+
const devices = Object.entries(project?.ports ?? {}).map(([label, port]) => `port ${label} (${port})`);
|
|
245
|
+
for (const { platforms } of projectDeviceSlots(project)) {
|
|
246
|
+
const ios = platforms.ios;
|
|
247
|
+
if (ios?.deviceUdid) devices.push(`ios sim ${ios.deviceUdid}`);
|
|
248
|
+
const android = platforms.android;
|
|
249
|
+
if (android?.avdName) devices.push(`android avd ${android.avdName}`);
|
|
250
|
+
else if (android?.serial) devices.push(`android device ${android.serial}`);
|
|
251
|
+
}
|
|
252
|
+
return devices;
|
|
253
|
+
}
|
|
254
|
+
function parkedIosCacheKey(lastBuild) {
|
|
255
|
+
if (lastBuild === null || typeof lastBuild !== "object" || Array.isArray(lastBuild)) return null;
|
|
256
|
+
const build = lastBuild;
|
|
257
|
+
return build.platform === "ios" && typeof build.cacheKey === "string" ? build.cacheKey : null;
|
|
258
|
+
}
|
|
259
|
+
function parkRequest(project, projectPath, slot, simslimManaged) {
|
|
260
|
+
const { max, error } = parkedMaxSetting("ios");
|
|
261
|
+
if (error || max <= 0) return void 0;
|
|
262
|
+
const cacheKey = parkedIosCacheKey(readWorkspaceState(projectPath)?.lastBuild);
|
|
263
|
+
return {
|
|
264
|
+
projectPath,
|
|
265
|
+
max,
|
|
266
|
+
bundleId: typeof project?.bundleId === "string" ? project.bundleId : null,
|
|
267
|
+
cacheKey,
|
|
268
|
+
simslimManaged,
|
|
269
|
+
slot
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function reclaimOwnedDevices(project, projectPath, { park = false } = {}) {
|
|
273
|
+
const deletedDevices = [];
|
|
274
|
+
const parkedDevices = [];
|
|
275
|
+
const evictedDevices = [];
|
|
276
|
+
const poolNotes = [];
|
|
277
|
+
const skippedDevices = [];
|
|
278
|
+
const failedDevices = [];
|
|
279
|
+
for (const { slot, platforms } of projectDeviceSlots(project)) {
|
|
280
|
+
const ios = platforms.ios;
|
|
281
|
+
if (ios?.owned && ios.deviceUdid) {
|
|
282
|
+
const udid = ios.deviceUdid;
|
|
283
|
+
const label = ios.deviceName || udid;
|
|
284
|
+
const r = teardownOwnedIosSim(udid, {
|
|
285
|
+
del: true,
|
|
286
|
+
label,
|
|
287
|
+
...park ? { park: parkRequest(project, projectPath, slot, Boolean(ios.simslimManaged)) } : {}
|
|
288
|
+
});
|
|
289
|
+
if (r.parkFallback) poolNotes.push(`could not park ${label}: ${r.parkFallback} -- deleted it instead`);
|
|
290
|
+
for (const failure of r.evictionFailures ?? []) poolNotes.push(failure);
|
|
291
|
+
if (r.parked) {
|
|
292
|
+
parkedDevices.push(r.parked);
|
|
293
|
+
evictedDevices.push(...r.evicted ?? []);
|
|
294
|
+
} else if (r.status === "torn-down") deletedDevices.push(r.label);
|
|
295
|
+
if (!r.parked && (r.status === "torn-down" || r.status === "missing")) clearDevice(projectPath, "ios", slot, udid);
|
|
296
|
+
if (r.status === "skipped") skippedDevices.push({
|
|
297
|
+
platform: "ios",
|
|
298
|
+
name: label,
|
|
299
|
+
udid,
|
|
300
|
+
reason: `${r.reason} -- not touched`
|
|
301
|
+
});
|
|
302
|
+
else if (r.status === "failed") {
|
|
303
|
+
const entry = {
|
|
304
|
+
platform: "ios",
|
|
305
|
+
name: label,
|
|
306
|
+
udid,
|
|
307
|
+
reason: `teardown failed: ${r.reason}`
|
|
308
|
+
};
|
|
309
|
+
skippedDevices.push(entry);
|
|
310
|
+
failedDevices.push(entry);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const android = platforms.android;
|
|
314
|
+
if (android?.owned && android.avdName) {
|
|
315
|
+
const bound = parkedMaxSetting("android");
|
|
316
|
+
const r = teardownOwnedAvd(android.avdName, {
|
|
317
|
+
del: true,
|
|
318
|
+
owner: {
|
|
319
|
+
projectPath,
|
|
320
|
+
slot
|
|
321
|
+
},
|
|
322
|
+
...park && !bound.error && bound.max > 0 ? { park: {
|
|
323
|
+
projectPath,
|
|
324
|
+
slot,
|
|
325
|
+
max: bound.max,
|
|
326
|
+
configuration: typeof android.poolConfiguration === "string" ? android.poolConfiguration : void 0
|
|
327
|
+
} } : {}
|
|
328
|
+
});
|
|
329
|
+
if (r.parkFallback) poolNotes.push(`could not park ${android.avdName}: ${r.parkFallback} -- deleted it instead`);
|
|
330
|
+
for (const failure of r.evictionFailures ?? []) poolNotes.push(failure);
|
|
331
|
+
if (!r.parked && (r.status === "torn-down" || r.status === "missing")) clearDevice(projectPath, "android", slot, android.avdName);
|
|
332
|
+
if (r.parked) {
|
|
333
|
+
parkedDevices.push(r.parked);
|
|
334
|
+
evictedDevices.push(...r.evicted ?? []);
|
|
335
|
+
} else if (r.status === "torn-down") deletedDevices.push(android.avdName);
|
|
336
|
+
else if (r.status === "skipped") skippedDevices.push({
|
|
337
|
+
platform: "android",
|
|
338
|
+
name: android.avdName,
|
|
339
|
+
reason: `${r.reason} -- not touched`
|
|
340
|
+
});
|
|
341
|
+
else if (r.status === "failed") {
|
|
342
|
+
const entry = {
|
|
343
|
+
platform: "android",
|
|
344
|
+
name: android.avdName,
|
|
345
|
+
reason: `teardown failed: ${r.reason}`
|
|
346
|
+
};
|
|
347
|
+
skippedDevices.push(entry);
|
|
348
|
+
failedDevices.push(entry);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
deletedDevices,
|
|
354
|
+
parkedDevices,
|
|
355
|
+
evictedDevices,
|
|
356
|
+
poolNotes,
|
|
357
|
+
skippedDevices,
|
|
358
|
+
failedDevices
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
async function reclaimProject(path, options = {}) {
|
|
362
|
+
const dir = workspaceDir(path);
|
|
363
|
+
const existed = existsSync(dir);
|
|
364
|
+
const run = await withIdleWorkspace(path, () => reclaimIdleProject(path, existed, options), {
|
|
365
|
+
purpose: "workspace removal",
|
|
366
|
+
supervisor: false,
|
|
367
|
+
managedLocks: false
|
|
368
|
+
});
|
|
369
|
+
if (run.ran) return run.value;
|
|
370
|
+
const kept = {
|
|
371
|
+
name: `workspace ${dir}`,
|
|
372
|
+
reason: `kept because ${run.reasons.join("; ")}`
|
|
373
|
+
};
|
|
374
|
+
return {
|
|
375
|
+
path,
|
|
376
|
+
dereferenced: [],
|
|
377
|
+
killedPid: null,
|
|
378
|
+
skippedMetro: null,
|
|
379
|
+
metroPort: getProject(path)?.metroPort ?? null,
|
|
380
|
+
deletedDevices: [],
|
|
381
|
+
parkedDevices: [],
|
|
382
|
+
evictedDevices: [],
|
|
383
|
+
poolNotes: [],
|
|
384
|
+
skippedDevices: [kept],
|
|
385
|
+
failedDevices: [kept],
|
|
386
|
+
keptEntry: true,
|
|
387
|
+
stoppedSession: null,
|
|
388
|
+
stoppedTunnel: null,
|
|
389
|
+
releasedLeases: [],
|
|
390
|
+
removedWorkspaceDirs: [],
|
|
391
|
+
failedWorkspaceDirs: []
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
async function reclaimIdleProject(path, workspaceExisted, { deleteOwnedDevices = false, parkOwnedDevices = false, preserveProjectRecord = false, stopSession = defaultStopSession, stopMetroTunnel = defaultStopMetroTunnel, releaseLeases = releaseWorkspaceLeases, verifyCollector = verifyCollectorOwnership }) {
|
|
395
|
+
await clearNamedPorts(path, { stop: true });
|
|
396
|
+
const project = getProject(path);
|
|
397
|
+
for (const { platforms } of projectDeviceSlots(project)) if (platforms.android?.owned && platforms.android.avdName) (0, ownership_claim_exports.releaseClaim)(acquireAvdClaim(platforms.android.avdName));
|
|
398
|
+
const dereferenced = describeDereferenced(project);
|
|
399
|
+
const initialState = readWorkspaceState(path);
|
|
400
|
+
let killedPid = null;
|
|
401
|
+
let skippedMetro = null;
|
|
402
|
+
let supervisorHeld = false;
|
|
403
|
+
const supervisor = resolveSupervisorTarget({
|
|
404
|
+
state: initialState?.supervisor,
|
|
405
|
+
record: project?.supervisor,
|
|
406
|
+
reservedPort: project?.metroPort
|
|
407
|
+
});
|
|
408
|
+
const orphanServer = {
|
|
409
|
+
pid: Number(initialState?.supervisor?.serverPid),
|
|
410
|
+
processToken: initialState?.supervisor?.serverProcessToken
|
|
411
|
+
};
|
|
412
|
+
const orphanIdentity = supervisor.status === "stale" && typeof orphanServer.processToken === "string" ? (0, process_identity_exports.inspectProcessIdentity)(orphanServer) : null;
|
|
413
|
+
if (supervisor.status === "ours") {
|
|
414
|
+
if (killMetroTree(supervisor.pid, supervisor.processToken) && await (0, process_identity_exports.waitForProcessExit)(supervisor, 1e4)) killedPid = supervisor.pid;
|
|
415
|
+
else {
|
|
416
|
+
skippedMetro = `could not confirm supervisor pid ${supervisor.pid} exited`;
|
|
417
|
+
supervisorHeld = true;
|
|
418
|
+
}
|
|
419
|
+
} else if (supervisor.status === "unverified") {
|
|
420
|
+
skippedMetro = supervisor.reason ?? "supervisor identity could not be verified";
|
|
421
|
+
supervisorHeld = true;
|
|
422
|
+
} else if (orphanIdentity === "same") {
|
|
423
|
+
let signalled = false;
|
|
424
|
+
try {
|
|
425
|
+
signalled = signalProcessTree(orphanServer.pid, "SIGTERM");
|
|
426
|
+
} catch {}
|
|
427
|
+
if (signalled ? await (0, process_identity_exports.waitForProcessExit)(orphanServer, 1e4) : ["gone", "different"].includes((0, process_identity_exports.inspectProcessIdentity)(orphanServer))) killedPid = orphanServer.pid;
|
|
428
|
+
else {
|
|
429
|
+
skippedMetro = `could not confirm dev server pid ${orphanServer.pid} left by the supervisor exited`;
|
|
430
|
+
supervisorHeld = true;
|
|
431
|
+
}
|
|
432
|
+
} else if (orphanIdentity === "unknown") {
|
|
433
|
+
skippedMetro = `the identity of dev server pid ${orphanServer.pid} left by the supervisor could not be verified`;
|
|
434
|
+
supervisorHeld = true;
|
|
435
|
+
} else if (typeof project?.metroPort === "number") {
|
|
436
|
+
if (!(await resolveProjectMetro(project.metroPort, path)).missing) skippedMetro = "Externally started server left alone; no verified Stim supervisor owns it";
|
|
437
|
+
}
|
|
438
|
+
const { skippedDevices: skippedCollectors, failedDevices: failedCollectors } = await reapCollectors(path, {
|
|
439
|
+
verify: verifyCollector,
|
|
440
|
+
collectors: initialState?.collectors ?? {}
|
|
441
|
+
});
|
|
442
|
+
function retainReplacementProcesses() {
|
|
443
|
+
const currentState = readWorkspaceState(path);
|
|
444
|
+
const currentProject = getProject(path);
|
|
445
|
+
if (Boolean(currentState?.supervisor && !(0, process_identity_exports.sameProcessRecord)(currentState.supervisor, initialState?.supervisor) || currentProject?.supervisor && !(0, process_identity_exports.sameProcessRecord)(currentProject.supervisor, project?.supervisor) || Object.entries(currentState?.collectors ?? {}).some(([platform, record]) => !(0, process_identity_exports.sameProcessRecord)(record, initialState?.collectors?.[platform])))) {
|
|
446
|
+
supervisorHeld = true;
|
|
447
|
+
skippedMetro = "A replacement process appeared during cleanup; its ownership records are retained";
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
retainReplacementProcesses();
|
|
451
|
+
const { deletedDevices, parkedDevices, evictedDevices, poolNotes, skippedDevices, failedDevices } = deleteOwnedDevices && !supervisorHeld && failedCollectors.length === 0 ? reclaimOwnedDevices(project, path, { park: parkOwnedDevices }) : {
|
|
452
|
+
deletedDevices: [],
|
|
453
|
+
parkedDevices: [],
|
|
454
|
+
evictedDevices: [],
|
|
455
|
+
poolNotes: [],
|
|
456
|
+
skippedDevices: [],
|
|
457
|
+
failedDevices: []
|
|
458
|
+
};
|
|
459
|
+
skippedDevices.push(...skippedCollectors);
|
|
460
|
+
failedDevices.push(...failedCollectors);
|
|
461
|
+
const remote = reclaimRemoteSession(path, { stopSession });
|
|
462
|
+
if (remote.failed) {
|
|
463
|
+
skippedDevices.push(remote.failed);
|
|
464
|
+
failedDevices.push(remote.failed);
|
|
465
|
+
}
|
|
466
|
+
const tunnel = await reclaimMetroTunnel(path, { stopMetroTunnel });
|
|
467
|
+
if (tunnel.failed) {
|
|
468
|
+
skippedDevices.push(tunnel.failed);
|
|
469
|
+
failedDevices.push(tunnel.failed);
|
|
470
|
+
}
|
|
471
|
+
let releasedLeases = [];
|
|
472
|
+
try {
|
|
473
|
+
releasedLeases = releaseLeases(path);
|
|
474
|
+
} catch {
|
|
475
|
+
releasedLeases = [];
|
|
476
|
+
}
|
|
477
|
+
retainReplacementProcesses();
|
|
478
|
+
const removedWorkspaceDirs = [];
|
|
479
|
+
const failedWorkspaceDirs = [];
|
|
480
|
+
if (failedDevices.length === 0 && !supervisorHeld) {
|
|
481
|
+
const dir = workspaceDir(path);
|
|
482
|
+
if (existsSync(dir)) try {
|
|
483
|
+
emptyWorkspaceDir(dir);
|
|
484
|
+
if (workspaceExisted) removedWorkspaceDirs.push(dir);
|
|
485
|
+
} catch {
|
|
486
|
+
failedWorkspaceDirs.push(dir);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const keptEntry = supervisorHeld || failedDevices.length > 0 || failedWorkspaceDirs.length > 0;
|
|
490
|
+
if (project && !keptEntry && !preserveProjectRecord) removeProject(path);
|
|
491
|
+
return {
|
|
492
|
+
path,
|
|
493
|
+
dereferenced,
|
|
494
|
+
killedPid,
|
|
495
|
+
skippedMetro,
|
|
496
|
+
metroPort: project?.metroPort ?? null,
|
|
497
|
+
deletedDevices,
|
|
498
|
+
parkedDevices,
|
|
499
|
+
evictedDevices,
|
|
500
|
+
poolNotes,
|
|
501
|
+
skippedDevices,
|
|
502
|
+
failedDevices,
|
|
503
|
+
keptEntry,
|
|
504
|
+
stoppedSession: remote.stopped,
|
|
505
|
+
stoppedTunnel: tunnel.stopped,
|
|
506
|
+
releasedLeases,
|
|
507
|
+
removedWorkspaceDirs,
|
|
508
|
+
failedWorkspaceDirs
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
//#endregion
|
|
21
512
|
//#region src/engine/warm-claim.ts
|
|
22
513
|
const WARM_CLAIM_LABEL = "worktree warm";
|
|
23
514
|
const WARM_CLAIM_POLL_MS = 250;
|
|
@@ -38,10 +529,10 @@ function defaultSleep(ms) {
|
|
|
38
529
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
39
530
|
}
|
|
40
531
|
function warmClaimsDir() {
|
|
41
|
-
return join(getConfigDir(), "warm-locks");
|
|
532
|
+
return join$1(getConfigDir(), "warm-locks");
|
|
42
533
|
}
|
|
43
534
|
function warmClaimPath(repositoryRoot) {
|
|
44
|
-
return join(warmClaimsDir(), `${workspaceName(repositoryRoot)}.lock`);
|
|
535
|
+
return join$1(warmClaimsDir(), `${workspaceName(repositoryRoot)}.lock`);
|
|
45
536
|
}
|
|
46
537
|
function warmCommand(phase) {
|
|
47
538
|
return phase === "refresh" ? "stim worktree warm --refresh" : "stim worktree warm";
|
|
@@ -368,11 +859,11 @@ function stepLine(label, reason, action) {
|
|
|
368
859
|
return phaseLine(label, `${reason} -> ${action}`);
|
|
369
860
|
}
|
|
370
861
|
function installRecordFile(depsRoot) {
|
|
371
|
-
return join(getConfigDir(), "warm-installs", `${workspaceName(depsRoot)}.json`);
|
|
862
|
+
return join$1(getConfigDir(), "warm-installs", `${workspaceName(depsRoot)}.json`);
|
|
372
863
|
}
|
|
373
864
|
function lockfileHash(depsRoot, lockfile) {
|
|
374
865
|
try {
|
|
375
|
-
return createHash("sha256").update(readFileSync(join(depsRoot, lockfile))).digest("hex");
|
|
866
|
+
return createHash("sha256").update(readFileSync(join$1(depsRoot, lockfile))).digest("hex");
|
|
376
867
|
} catch {
|
|
377
868
|
return null;
|
|
378
869
|
}
|
|
@@ -394,7 +885,7 @@ function readInstallEvidence(depsRoot) {
|
|
|
394
885
|
function writeInstallEvidence(depsRoot, lockfile, hash, completed) {
|
|
395
886
|
const file = installRecordFile(depsRoot);
|
|
396
887
|
const tmp = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
397
|
-
mkdirSync(join(getConfigDir(), "warm-installs"), { recursive: true });
|
|
888
|
+
mkdirSync(join$1(getConfigDir(), "warm-installs"), { recursive: true });
|
|
398
889
|
writeFileSync(tmp, JSON.stringify({
|
|
399
890
|
lock: lockfile,
|
|
400
891
|
hash: hash ?? "",
|
|
@@ -412,9 +903,9 @@ function restoreInstallEvidence(depsRoot, prior) {
|
|
|
412
903
|
else rmSync(installRecordFile(depsRoot), { force: true });
|
|
413
904
|
}
|
|
414
905
|
function inProgressOperation(gitDir) {
|
|
415
|
-
if (existsSync(join(gitDir, "rebase-merge"))) return "rebase";
|
|
416
|
-
if (existsSync(join(gitDir, "rebase-apply"))) return existsSync(join(gitDir, "rebase-apply", "applying")) ? "am" : "rebase";
|
|
417
|
-
return existsSync(join(gitDir, "MERGE_HEAD")) ? "merge" : null;
|
|
906
|
+
if (existsSync(join$1(gitDir, "rebase-merge"))) return "rebase";
|
|
907
|
+
if (existsSync(join$1(gitDir, "rebase-apply"))) return existsSync(join$1(gitDir, "rebase-apply", "applying")) ? "am" : "rebase";
|
|
908
|
+
return existsSync(join$1(gitDir, "MERGE_HEAD")) ? "merge" : null;
|
|
418
909
|
}
|
|
419
910
|
function readMainCheckoutState(root) {
|
|
420
911
|
const exec = getExecutor();
|
|
@@ -668,7 +1159,7 @@ async function refreshMainCheckout({ root, appDir, settings, emit, spawnFn = nul
|
|
|
668
1159
|
const consumed = dependencies ? lockfileHash(dependencies.root, dependencies.lock) : null;
|
|
669
1160
|
const deps = depsPlan({
|
|
670
1161
|
lockfile: dependencies?.lock ?? null,
|
|
671
|
-
lockfileChanged: dependencies ? pathChangedBetween(root, before, head, gitPath(relative(root, dependencies.root), dependencies.lock)) : false,
|
|
1162
|
+
lockfileChanged: dependencies ? pathChangedBetween(root, before, head, gitPath(relative$1(root, dependencies.root), dependencies.lock)) : false,
|
|
672
1163
|
installed,
|
|
673
1164
|
treeValid: dependencies?.lock === "package-lock.json" && installed ? installedNpmTreeIsValid(dependencies.root) : null,
|
|
674
1165
|
lastInstall: priorInstall,
|
|
@@ -700,11 +1191,11 @@ async function refreshMainCheckout({ root, appDir, settings, emit, spawnFn = nul
|
|
|
700
1191
|
if (marked) restoreInstallEvidence(marked.root, priorInstall);
|
|
701
1192
|
emit(stepLine("deps", depsSubject, "skipped"));
|
|
702
1193
|
}
|
|
703
|
-
const app = relative(root, appDir) || ".";
|
|
1194
|
+
const app = relative$1(root, appDir) || ".";
|
|
704
1195
|
const where = `source ${appDir}: `;
|
|
705
1196
|
const podState = readPodState(appDir);
|
|
706
1197
|
const pods = podsPlan({
|
|
707
|
-
hasIos: existsSync(join(appDir, "ios")),
|
|
1198
|
+
hasIos: existsSync(join$1(appDir, "ios")),
|
|
708
1199
|
hasPodfile: podState.hasPodfile,
|
|
709
1200
|
podfileLockChanged: pathChangedBetween(root, before, head, gitPath(app, "ios", "Podfile.lock")),
|
|
710
1201
|
stale: podsAreStale(podState.lockText, podState.manifestText)
|
|
@@ -735,12 +1226,12 @@ async function refreshMainCheckout({ root, appDir, settings, emit, spawnFn = nul
|
|
|
735
1226
|
//#endregion
|
|
736
1227
|
//#region src/commands/worktree.ts
|
|
737
1228
|
function dependencyInstallCommand(target, dir = ".") {
|
|
738
|
-
const installRoot = resolve
|
|
1229
|
+
const installRoot = resolve(target, dir);
|
|
739
1230
|
let command = "npm install";
|
|
740
|
-
if (existsSync$1(resolve
|
|
741
|
-
else if (existsSync$1(resolve
|
|
742
|
-
else if (existsSync$1(resolve
|
|
743
|
-
else if (existsSync$1(resolve
|
|
1231
|
+
if (existsSync$1(resolve(installRoot, "pnpm-lock.yaml"))) command = "pnpm install";
|
|
1232
|
+
else if (existsSync$1(resolve(installRoot, "yarn.lock"))) command = "yarn install";
|
|
1233
|
+
else if (existsSync$1(resolve(installRoot, "bun.lock")) || existsSync$1(resolve(installRoot, "bun.lockb"))) command = "bun install";
|
|
1234
|
+
else if (existsSync$1(resolve(installRoot, "package-lock.json"))) command = "npm ci";
|
|
744
1235
|
return `cd '${installRoot.replaceAll("'", "'\\''")}' && ${command}`;
|
|
745
1236
|
}
|
|
746
1237
|
function reportCarriedStateHealth(root, target, copied) {
|
|
@@ -752,7 +1243,7 @@ function reportCarriedStateHealth(root, target, copied) {
|
|
|
752
1243
|
}
|
|
753
1244
|
for (const p of podsOutOfSync(target, copied)) {
|
|
754
1245
|
const where = p.dir === "." ? "Podfile.lock" : `${p.dir}/Podfile.lock`;
|
|
755
|
-
const pod = podInstallCommand(p.dir === "." ? target : resolve
|
|
1246
|
+
const pod = podInstallCommand(p.dir === "." ? target : resolve(target, p.dir, ".."));
|
|
756
1247
|
console.error(chalk.yellow(phaseLine("carry", p.reason === "missing" ? `carried ${p.dir === "." ? "Pods" : `${p.dir}/Pods`} but there is no ${where}. Run \`${pod}\` before building.` : `carried ${p.dir === "." ? "Pods" : `${p.dir}/Pods`} does not match the ${where} on disk here. Pods are gitignored and cloned; Podfile.lock is tracked, so the two can disagree. Run \`${pod}\` before building, or xcodebuild fails with "sandbox is not in sync" only after every pod has compiled.`)));
|
|
757
1248
|
}
|
|
758
1249
|
}
|
|
@@ -766,14 +1257,16 @@ function reportRefreshFailure(failure) {
|
|
|
766
1257
|
function mainCheckoutAppDir(root, target) {
|
|
767
1258
|
const app = findProjectRoot(process.cwd());
|
|
768
1259
|
if (!app) return root;
|
|
769
|
-
const rel = relative
|
|
770
|
-
if (!rel || rel.startsWith("..") || isAbsolute
|
|
771
|
-
return resolve
|
|
1260
|
+
const rel = relative(target, app);
|
|
1261
|
+
if (!rel || rel.startsWith("..") || isAbsolute(rel)) return root;
|
|
1262
|
+
return resolve(root, rel);
|
|
772
1263
|
}
|
|
773
1264
|
function registerWarm(worktree) {
|
|
774
1265
|
worktree.command("warm").description("Copy missing ignored paths from the source checkout into the current linked worktree.").option("--refresh", "Before copying, fast-forward the source checkout to its upstream and install what the new commits moved.").action(async (opts) => {
|
|
775
1266
|
try {
|
|
776
1267
|
const { root, target, common } = warmWorktreePaths(process.cwd());
|
|
1268
|
+
const app = findProjectRoot(process.cwd());
|
|
1269
|
+
if (app) recordWorkspaceUse(app);
|
|
777
1270
|
const readSettings = () => {
|
|
778
1271
|
const settings = resolveSettings({
|
|
779
1272
|
gitCommonDir: common,
|
|
@@ -920,10 +1413,20 @@ function excludePodChurn(lines) {
|
|
|
920
1413
|
restore
|
|
921
1414
|
};
|
|
922
1415
|
}
|
|
1416
|
+
function nativeCanonicalPath(path) {
|
|
1417
|
+
const missing = [];
|
|
1418
|
+
for (let existing = resolve(path);; existing = dirname(existing)) try {
|
|
1419
|
+
return resolve(realpathSync$1.native(existing), ...missing);
|
|
1420
|
+
} catch {
|
|
1421
|
+
if (dirname(existing) === existing) return resolve(path);
|
|
1422
|
+
missing.unshift(basename(existing));
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
923
1425
|
function matchWorktreeEntry(entries, path) {
|
|
924
1426
|
let best = null;
|
|
1427
|
+
const target = nativeCanonicalPath(path);
|
|
925
1428
|
(entries || []).forEach((entry, index) => {
|
|
926
|
-
if (!entry?.path || !isPathPrefix(entry.path,
|
|
1429
|
+
if (!entry?.path || !isPathPrefix(nativeCanonicalPath(entry.path), target)) return;
|
|
927
1430
|
if (!best || entry.path.length > best.path.length) best = {
|
|
928
1431
|
...entry,
|
|
929
1432
|
index
|
|
@@ -972,8 +1475,9 @@ function describeKeptDevice(s) {
|
|
|
972
1475
|
function reclaimKeys(rootPath) {
|
|
973
1476
|
const cfg = loadConfig();
|
|
974
1477
|
const keys = /* @__PURE__ */ new Set([rootPath]);
|
|
1478
|
+
const root = nativeCanonicalPath(rootPath);
|
|
975
1479
|
if (cfg?.projects) {
|
|
976
|
-
for (const key of Object.keys(cfg.projects)) if (isPathPrefix(rootPath, key)) keys.add(key);
|
|
1480
|
+
for (const key of Object.keys(cfg.projects)) if (isPathPrefix(rootPath, key) || isPathPrefix(root, nativeCanonicalPath(key))) keys.add(key);
|
|
977
1481
|
}
|
|
978
1482
|
return [...keys].toSorted();
|
|
979
1483
|
}
|
|
@@ -998,11 +1502,22 @@ async function reclaimAll(rootPath, keys = reclaimKeys(rootPath), { preserveRoot
|
|
|
998
1502
|
const removedWorkspaceDirs = [];
|
|
999
1503
|
const failedWorkspaceDirs = [];
|
|
1000
1504
|
for (const key of keys) {
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1505
|
+
let r;
|
|
1506
|
+
try {
|
|
1507
|
+
r = await reclaimProject(key, {
|
|
1508
|
+
deleteOwnedDevices: true,
|
|
1509
|
+
parkOwnedDevices: true,
|
|
1510
|
+
preserveProjectRecord: preserveRootProject && key === rootPath
|
|
1511
|
+
});
|
|
1512
|
+
} catch (error) {
|
|
1513
|
+
keptEntries.push(key);
|
|
1514
|
+
retainedResources.push({
|
|
1515
|
+
name: "ownership state",
|
|
1516
|
+
reason: String(error?.message || error),
|
|
1517
|
+
project: key
|
|
1518
|
+
});
|
|
1519
|
+
continue;
|
|
1520
|
+
}
|
|
1006
1521
|
dereferenced.push(...r.dereferenced);
|
|
1007
1522
|
if (r.killedPid) killedPids.push(r.killedPid);
|
|
1008
1523
|
deletedDevices.push(...r.deletedDevices);
|
|
@@ -1116,30 +1631,32 @@ function removalPath(target) {
|
|
|
1116
1631
|
return canonicalExistingPath(target ?? process.cwd());
|
|
1117
1632
|
}
|
|
1118
1633
|
function canonicalExistingPath(target) {
|
|
1119
|
-
const resolved = resolve
|
|
1634
|
+
const resolved = resolve(target);
|
|
1120
1635
|
let existing = resolved;
|
|
1121
1636
|
const missing = [];
|
|
1122
1637
|
while (!existsSync$1(existing)) {
|
|
1123
|
-
const parent = dirname
|
|
1638
|
+
const parent = dirname(existing);
|
|
1124
1639
|
if (parent === existing) return resolved;
|
|
1125
|
-
missing.unshift(basename
|
|
1640
|
+
missing.unshift(basename(existing));
|
|
1126
1641
|
existing = parent;
|
|
1127
1642
|
}
|
|
1128
|
-
return resolve
|
|
1643
|
+
return resolve(realpathSync$1(existing), ...missing);
|
|
1129
1644
|
}
|
|
1130
1645
|
function inspectRemoval(path) {
|
|
1131
1646
|
const gitAnswered = hasUncommittedWork(path);
|
|
1132
1647
|
const { lines: dirtyLines, restore: podChurn } = excludePodChurn(gitAnswered ? dirtyPaths(path, { limit: Infinity }) : []);
|
|
1133
1648
|
const dirty = gitAnswered === null ? null : dirtyLines.length > 0;
|
|
1134
1649
|
const unpushed = unpushedCommits(path);
|
|
1650
|
+
const blockers = removalBlockers({
|
|
1651
|
+
dirty,
|
|
1652
|
+
unpushed
|
|
1653
|
+
});
|
|
1654
|
+
if (hasPopulatedSubmodules(path)) blockers.push("initialized submodules, which git removes only with --force");
|
|
1135
1655
|
return {
|
|
1136
1656
|
dirtyLines,
|
|
1137
1657
|
podChurn,
|
|
1138
1658
|
unpushed,
|
|
1139
|
-
blockers
|
|
1140
|
-
dirty,
|
|
1141
|
-
unpushed
|
|
1142
|
-
})
|
|
1659
|
+
blockers
|
|
1143
1660
|
};
|
|
1144
1661
|
}
|
|
1145
1662
|
function printRemovalRefusal(path, inspection) {
|
|
@@ -1171,7 +1688,14 @@ function printRemovalCleanup(result, failed) {
|
|
|
1171
1688
|
if (result.dereferenced.length) console.error(chalk.dim(phaseLine("workspace", `no longer referenced: ${result.dereferenced.join(", ")}`)));
|
|
1172
1689
|
for (const skipped of result.skippedDevices) console.error((failed ? chalk.dim : chalk.yellow)(phaseLine("device", `kept ${describeKeptDevice(skipped)} (${skipped.reason})`)));
|
|
1173
1690
|
}
|
|
1174
|
-
async function
|
|
1691
|
+
async function removeWorktreeTarget(target, opts = {}) {
|
|
1692
|
+
let removed = false;
|
|
1693
|
+
await runRemove(target, opts, () => {
|
|
1694
|
+
removed = true;
|
|
1695
|
+
});
|
|
1696
|
+
return removed;
|
|
1697
|
+
}
|
|
1698
|
+
async function runRemove(target, opts, onRemoved) {
|
|
1175
1699
|
const poolError = parkedMaxSetting("ios").error || parkedMaxSetting("android").error;
|
|
1176
1700
|
if (poolError) {
|
|
1177
1701
|
console.error(chalk.red(poolError));
|
|
@@ -1196,7 +1720,7 @@ async function runRemove(target, opts = {}) {
|
|
|
1196
1720
|
console.error(chalk.dim(phaseLine("branch", `${branch} is already absent; cleared its pending cleanup record`)));
|
|
1197
1721
|
return;
|
|
1198
1722
|
}
|
|
1199
|
-
const checkedOutAt = listWorktrees(mainRoot).find((candidate) => candidate.branch === branch && resolve
|
|
1723
|
+
const checkedOutAt = listWorktrees(mainRoot).find((candidate) => candidate.branch === branch && resolve(candidate.path) !== resolve(path))?.path;
|
|
1200
1724
|
if (checkedOutAt) {
|
|
1201
1725
|
console.error(chalk.red(`Refusing pending cleanup for ${branch}: it is checked out at ${checkedOutAt}.`));
|
|
1202
1726
|
console.error(chalk.dim("Switch that worktree to another branch, then retry this command."));
|
|
@@ -1228,7 +1752,7 @@ async function runRemove(target, opts = {}) {
|
|
|
1228
1752
|
const worktrees = listWorktrees(path);
|
|
1229
1753
|
const entry = matchWorktreeEntry(worktrees, path);
|
|
1230
1754
|
if (!entry) {
|
|
1231
|
-
if (gitCommonDir(path) === null && hasRegisteredProjectUnder(path)) {
|
|
1755
|
+
if (!opts.linkedOnly && gitCommonDir(path) === null && hasRegisteredProjectUnder(path)) {
|
|
1232
1756
|
await reclaimEnvironment(path, "it is not a git repository");
|
|
1233
1757
|
return;
|
|
1234
1758
|
}
|
|
@@ -1249,6 +1773,11 @@ async function runRemove(target, opts = {}) {
|
|
|
1249
1773
|
process.exitCode = 1;
|
|
1250
1774
|
return;
|
|
1251
1775
|
}
|
|
1776
|
+
if (entry.path === source.path && opts.linkedOnly) {
|
|
1777
|
+
console.error(chalk.red(`Refusing to remove ${path}: ${entry.path} is the source checkout.`));
|
|
1778
|
+
process.exitCode = 1;
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1252
1781
|
if (entry.path === source.path) {
|
|
1253
1782
|
if (entry.path !== path) console.error(chalk.dim(`${path} is inside the source checkout ${entry.path}; reclaiming its environment.`));
|
|
1254
1783
|
await reclaimEnvironment(entry.path, "it is the source checkout");
|
|
@@ -1258,6 +1787,12 @@ async function runRemove(target, opts = {}) {
|
|
|
1258
1787
|
console.error(chalk.dim(`${path} is inside the worktree ${entry.path}; removing that.`));
|
|
1259
1788
|
path = entry.path;
|
|
1260
1789
|
}
|
|
1790
|
+
if (entry.locked) {
|
|
1791
|
+
console.error(chalk.red(`Refusing to remove ${path}: git has it locked.`));
|
|
1792
|
+
console.error(chalk.dim(`Unlock it, then retry: git -C ${source.path} worktree unlock ${path}`));
|
|
1793
|
+
process.exitCode = 1;
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1261
1796
|
const project = getProject(path);
|
|
1262
1797
|
const branch = entry.branch;
|
|
1263
1798
|
const ownsBranch = Boolean(branch && project?.worktreeBranchOwned === true && project.worktreeBranch === branch);
|
|
@@ -1271,12 +1806,18 @@ async function runRemove(target, opts = {}) {
|
|
|
1271
1806
|
const retainedBranchReason = !branch ? null : !ownsBranch ? "Stim did not create it" : inspection.unpushed === null ? "Stim could not verify whether it has unique commits" : inspection.unpushed.length > 0 ? `it has ${plural(inspection.unpushed.length, "unique commit")}` : null;
|
|
1272
1807
|
const branchDeleteCwd = source.path;
|
|
1273
1808
|
await withManagedRemoteWorktreeRemovalLock(path, () => withReclaimLocks(path, async (lockedKeys) => {
|
|
1809
|
+
if (opts.guard?.(lockedKeys).length) return;
|
|
1810
|
+
const current = inspectRemoval(path);
|
|
1811
|
+
if (current.blockers.length && !opts.force) {
|
|
1812
|
+
printRemovalRefusal(path, current);
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1274
1815
|
const result = await reclaimAll(path, lockedKeys, { preserveRootProject: true });
|
|
1275
1816
|
if (result.keptEntries.length) {
|
|
1276
1817
|
reportRetainedResources(path, result);
|
|
1277
1818
|
return;
|
|
1278
1819
|
}
|
|
1279
|
-
restorePodChurn(path,
|
|
1820
|
+
restorePodChurn(path, current.podChurn);
|
|
1280
1821
|
try {
|
|
1281
1822
|
removeWorktree(path, {
|
|
1282
1823
|
from: source.path,
|
|
@@ -1291,6 +1832,7 @@ async function runRemove(target, opts = {}) {
|
|
|
1291
1832
|
process.exitCode = 1;
|
|
1292
1833
|
return;
|
|
1293
1834
|
}
|
|
1835
|
+
onRemoved();
|
|
1294
1836
|
const finish = () => {
|
|
1295
1837
|
printRemovalCleanup(result, false);
|
|
1296
1838
|
console.error(chalk.dim(phaseLine("removed", path)));
|
|
@@ -1306,7 +1848,7 @@ async function runRemove(target, opts = {}) {
|
|
|
1306
1848
|
worktreeRemovalComplete: true,
|
|
1307
1849
|
worktreePendingBranchSha: approvedBranchSha
|
|
1308
1850
|
});
|
|
1309
|
-
const checkedOutAt = listWorktrees(branchDeleteCwd).find((candidate) => candidate.branch === branch && resolve
|
|
1851
|
+
const checkedOutAt = listWorktrees(branchDeleteCwd).find((candidate) => candidate.branch === branch && resolve(candidate.path) !== resolve(path))?.path;
|
|
1310
1852
|
if (checkedOutAt) {
|
|
1311
1853
|
console.error(chalk.yellow(phaseLine("branch", `kept ${branch} (it is checked out at ${checkedOutAt})`)));
|
|
1312
1854
|
console.error(chalk.dim(` Switch that worktree to another branch, then retry: stim worktree remove ${path}`));
|
|
@@ -1330,7 +1872,9 @@ async function runRemove(target, opts = {}) {
|
|
|
1330
1872
|
}));
|
|
1331
1873
|
}
|
|
1332
1874
|
function registerRemove(worktree) {
|
|
1333
|
-
worktree.command("remove [target]").description("Remove a worktree, its unused Stim-created branch, build artifacts, owned devices, and Metro port. Defaults to the current workspace. On the source checkout it reclaims the environment only and leaves the tree in place.").option("--force", "remove even when the worktree holds uncommitted or unpushed work").action(
|
|
1875
|
+
worktree.command("remove [target]").description("Remove a worktree, its unused Stim-created branch, build artifacts, owned devices, and Metro port. Defaults to the current workspace. On the source checkout it reclaims the environment only and leaves the tree in place.").option("--force", "remove even when the worktree holds uncommitted or unpushed work or initialized submodules").action(async (target, opts) => {
|
|
1876
|
+
await removeWorktreeTarget(target, { force: opts.force });
|
|
1877
|
+
});
|
|
1334
1878
|
}
|
|
1335
1879
|
function worktreeCommand(program) {
|
|
1336
1880
|
const worktree = program.command("worktree").description("Warm and remove Git worktrees");
|
|
@@ -1338,4 +1882,4 @@ function worktreeCommand(program) {
|
|
|
1338
1882
|
registerRemove(worktree);
|
|
1339
1883
|
}
|
|
1340
1884
|
//#endregion
|
|
1341
|
-
export {
|
|
1885
|
+
export { workspaceInUse as _, reclaimKeys as a, removalBlockers as c, removeWorktreeTarget as d, worktreeCommand as f, withIdleWorkspace as g, emptyWorkspaceDir as h, porcelainPath as i, removalPath as l, reclaimProject as m, excludePodChurn as n, registerRemove as o, describeDereferenced as p, matchWorktreeEntry as r, registerWarm as s, dependencyInstallCommand as t, removalRemedy as u, canonicalPath as v };
|