stim 1.8.0 → 1.10.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-DWssqHQ8.mjs → android-787HhP5w.mjs} +168 -39
- package/dist/{android-BRgBYzU0.mjs → android-DBkeHwpm.mjs} +250 -200
- package/dist/{android-DeYdjjBP.mjs → android-DPov-fv2.mjs} +2 -2
- package/dist/{android-cas-SWIjzeuL.mjs → android-cas-CITmIeWY.mjs} +22 -14
- package/dist/android-cas-compiler.mjs +2 -2
- package/dist/app-install-CrJ6t_bv.mjs +819 -0
- package/dist/budget-BP4jZZL4.mjs +620 -0
- package/dist/build-progress-BXx-ZBe4.mjs +512 -0
- package/dist/{cache-manifest-CYJi8-Hx.mjs → cache-manifest-Pubfp8wV.mjs} +4 -4
- package/dist/cache-manifest.mjs +1 -1
- package/dist/cli.mjs +22 -19
- package/dist/collector-run.d.mts +7 -1
- package/dist/collector-run.mjs +10 -9
- package/dist/{command-output-B3ycpsYv.mjs → command-output-D5z2_bcF.mjs} +11 -2
- package/dist/{config-B7iyKWkL.mjs → config-BEq0Qw_a.mjs} +134 -121
- package/dist/{dependency-state-D0M7oKA7.mjs → dependency-state-r9UesA9a.mjs} +2 -2
- package/dist/{deps-oCXVNEqQ.mjs → deps-DP4eWqea.mjs} +8 -5
- package/dist/{detached-entry-Bpo6ZY_I.mjs → detached-entry-DJHoca49.mjs} +1 -1
- package/dist/{device-BUoZyMoR.mjs → dev-client-92U593KT.mjs} +1090 -660
- package/dist/{device-RdujjHLB.mjs → device-DhTZe7L4.mjs} +13 -20
- package/dist/{build-lock-CgtsB6TV.mjs → device-lease-CbEaIk7b.mjs} +481 -27
- package/dist/{device-pool-BxRwdufO.mjs → device-pool-Di8Q-9Ro.mjs} +7 -7
- package/dist/{device-remote-ByN8P6CB.mjs → device-remote-Dy1jGj3U.mjs} +645 -119
- package/dist/{doctor-BMt156qe.mjs → doctor-B-inAJmt.mjs} +137 -113
- package/dist/{error-diagnostics-D1Kaf-eT.mjs → error-diagnostics-xxWKNO3-.mjs} +22 -17
- package/dist/exec-DK8KaVHK.mjs +133 -0
- package/dist/{gc-v2YcoJvm.mjs → gc-ChPFZMRC.mjs} +836 -813
- package/dist/{guide-BTAlD1_y.mjs → guide-DwmaD_Wa.mjs} +953 -209
- package/dist/guide-status-CWvti2Lg.mjs +125 -0
- package/dist/idle-DpRLp5lj.mjs +157 -0
- package/dist/{build-slots-C-oAHRT1.mjs → in-use-CjUluqIX.mjs} +102 -8
- package/dist/{ios-BcCiI4TS.mjs → ios-B2Fpqvlb.mjs} +60 -71
- package/dist/{ios-BP04pY5f.mjs → ios-Dd-hYJBh.mjs} +241 -234
- package/dist/{ios-device-CFXA46ez.mjs → ios-device-BiOsSVzd.mjs} +3 -2
- package/dist/{ios-device-CauFJiYx.mjs → ios-device-CtRdEVjx.mjs} +149 -52
- package/dist/launch-verify-CQS_Kg-C.mjs +365 -0
- package/dist/{logs-CakdBTtm.mjs → logs-MARIy6m9.mjs} +20 -15
- package/dist/metro-BWLOJ_Wr.mjs +409 -0
- package/dist/{named-ports-DzMOlmaX.mjs → named-ports-DiTfSz6p.mjs} +4 -4
- package/dist/{ndjson-BZwI1NBv.mjs → ndjson-CDY9IkVC.mjs} +23 -43
- package/dist/{ndjson-DvHSsNU9.d.mts → ndjson-Du3j0AQ9.d.mts} +1 -0
- package/dist/{ownership-Cp52QAK1.mjs → ownership-D2xbLVoz.mjs} +2 -3
- package/dist/{teardown-BJqEzPo_.mjs → ownership-DZjdx8Gq.mjs} +664 -612
- package/dist/{ports-BIbJaKNW.mjs → ports-CLSSFOuz.mjs} +3 -3
- package/dist/{process-output-Dl5uKE03.mjs → process-output-Fpl8PYi8.mjs} +1 -1
- package/dist/{project-DDZWf_16.mjs → project-BXZzOABt.mjs} +8 -3
- package/dist/{reload-BdypPbjM.mjs → reload-DoETJ3rl.mjs} +11 -13
- package/dist/{server-bare-BQkyxa_b.mjs → server-bare-DVbb7kXH.mjs} +2 -2
- package/dist/{server-expo-BVAA1OqJ.mjs → server-expo-BvOCeakp.mjs} +8 -7
- package/dist/server-expo-DBVTdult.mjs +2 -0
- package/dist/{settings-BXXorVM2.mjs → settings-DOugPlPz.mjs} +59 -159
- package/dist/settings-EWYNwe72.mjs +378 -0
- package/dist/settings.schema.json +1064 -0
- package/dist/simslim-Ca3487H6.mjs +436 -0
- package/dist/{slot-launch-ZUUe0TQP.mjs → slot-launch-C5cg0fmk.mjs} +4 -4
- package/dist/spawn-claims-CUleC2cM.mjs +49 -0
- package/dist/{start-CmxtOKjU.mjs → start-F9gv5dcC.mjs} +159 -114
- package/dist/start-VoFsPSes.mjs +2 -0
- package/dist/{state-DZobzxLW.mjs → state-WGdIbi_d.mjs} +5 -3
- package/dist/{stats-BYcZ6n1h.mjs → stats-BAgJaLA4.mjs} +66 -5
- package/dist/{stats-CLg5RuDS.mjs → stats-DBYJ5bxL.mjs} +4 -4
- package/dist/status-DLCD-o3s.mjs +764 -0
- package/dist/status-Dr4fRkym.mjs +216 -0
- package/dist/{guide-status-Craq9z0t.mjs → stim-installations-CINXy_su.mjs} +3 -122
- package/dist/{stop-C8JW6kKp.mjs → stop-Dj4pfmyv.mjs} +111 -31
- package/dist/{stop-B1kOpYei.mjs → stop-ZvOeTmKb.mjs} +2 -2
- package/dist/supervisor-run.d.mts +11 -2
- package/dist/supervisor-run.mjs +228 -42
- package/dist/{workspace-process-lock-BYfZz_S-.mjs → workspace-process-lock-BXA96myD.mjs} +14 -9
- package/dist/workspaces-BTUyFfkv.mjs +1003 -0
- package/dist/worktree-4aZg2NnI.mjs +2 -0
- package/dist/{worktree-CFLA-6MX.mjs → worktree-BMS7GeqB.mjs} +523 -61
- package/dist/{worktree-FG_ZKKdb.mjs → worktree-BlizKDpZ.mjs} +200 -54
- package/dist/{xcode-B6YaWeg0.mjs → xcode-B2tBkf0m.mjs} +66 -57
- package/package.json +6 -6
- package/skill/SKILL.md +2 -0
- package/dist/app-install-BDbsSgCl.mjs +0 -1175
- package/dist/dev-client-BmT-ioFI.mjs +0 -797
- package/dist/device-lease-Ffz_M5t0.mjs +0 -359
- package/dist/exec-B29XNuhP.mjs +0 -77
- package/dist/logs-query-BGL-Pl12.mjs +0 -290
- package/dist/metro-C7bcJ0uo.mjs +0 -233
- package/dist/ownership-wolE-0Do.mjs +0 -470
- package/dist/public-http-probe-CyMDrQ3c.mjs +0 -61
- package/dist/reclaim-KJ-0uWEF.mjs +0 -361
- package/dist/server-expo-YDDG3eP7.mjs +0 -2
- package/dist/spawn-entry-nCE6m770.mjs +0 -14
- package/dist/status-DdkLki47.mjs +0 -426
- package/dist/workspace-state-BT5E3SA2.mjs +0 -146
- package/dist/{metro-store-CDDZHtPl.mjs → metro-store-DzeWkfqW.mjs} +1 -1
|
@@ -1,23 +1,418 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { a as formatElapsed,
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { n as
|
|
13
|
-
import {
|
|
1
|
+
import { D as process_identity_exports, H 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-BEq0Qw_a.mjs";
|
|
2
|
+
import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
|
|
3
|
+
import { O as workspaceDir, c as signalProcessTree, h as recordWorkspaceUse, i as pidExists, m as readWorkspaceState, s as resolveProjectMetro, t as killMetroTree } from "./metro-BWLOJ_Wr.mjs";
|
|
4
|
+
import { C as sourceCheckoutOf, S as restoreFile, T as warmWorktreePaths, _ as readWorktreeExclude, b as resolveFullRef, d as hasUncommittedWork, f as isPodInstallChurn, g as podsOutOfSync, h as locallyKnownUpstream, i as depsOutOfSync, l as hasPopulatedSubmodules, m as listWorktrees, n as cloneIgnoredEntries, o as dirtyPaths, r as deleteBranch, s as gitCommonDir, t as branchExists, u as hasRemote, v as removeWorktree, w as unpushedCommits } from "./worktree-BlizKDpZ.mjs";
|
|
5
|
+
import { o as findProjectRoot } from "./project-BXZzOABt.mjs";
|
|
6
|
+
import { a as clearRemoteSession, i as clearManagedMetroTunnel, l as readRemoteSession, s as readMetroTunnel, u as readRemoteSessionId } from "./state-WGdIbi_d.mjs";
|
|
7
|
+
import { a as formatElapsed, h as shortUdid, i as formatDuration, l as phaseLine, p as releasedLeaseFact, u as plural } from "./command-output-D5z2_bcF.mjs";
|
|
8
|
+
import { u as releaseWorkspaceLeases } from "./device-lease-CbEaIk7b.mjs";
|
|
9
|
+
import { L as unknownSettingKeys, P as settingShapeErrors, a as SETTING_SHAPE_REMEDY, j as resolveSettings } from "./settings-DOugPlPz.mjs";
|
|
10
|
+
import { d as teardownOwnedAvd, f as teardownOwnedIosSim, h as POOL_SETTING_REMEDY, i as stopTunnel, l as withManagedTunnelRemovalLock, s as withManagedRemoteWorktreeRemovalLock, t as resolveSupervisorTarget, y as parkedMaxSetting } from "./ownership-DZjdx8Gq.mjs";
|
|
11
|
+
import { n as withIdleWorkspace, t as emptyWorkspaceDir } from "./in-use-CjUluqIX.mjs";
|
|
12
|
+
import { n as verifyCollectorOwnership } from "./ownership-D2xbLVoz.mjs";
|
|
13
|
+
import { i as endRecordedSession, z as resolveEasCliBin } from "./device-remote-Dy1jGj3U.mjs";
|
|
14
|
+
import { h as podInstallCommand, n as HEARTBEAT_INTERVAL_MS } from "./xcode-B2tBkf0m.mjs";
|
|
15
|
+
import { n as hasInstalledDependencies, r as installedNpmTreeIsValid, t as dependencyState } from "./dependency-state-r9UesA9a.mjs";
|
|
16
|
+
import { t as clearNamedPorts } from "./named-ports-DiTfSz6p.mjs";
|
|
17
|
+
import { a as runPodInstall, i as runCaptured, n as podsAreStale, r as readPodState, t as DEPS_ERROR } from "./deps-DP4eWqea.mjs";
|
|
14
18
|
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
15
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
16
|
-
import { join, relative } from "node:path";
|
|
17
19
|
import { existsSync as existsSync$1, realpathSync as realpathSync$1 } from "fs";
|
|
18
|
-
import { basename
|
|
20
|
+
import { basename, dirname, isAbsolute, relative, resolve } from "path";
|
|
19
21
|
import { workspaceName } from "@stim-cli/core";
|
|
22
|
+
import { join as join$1, relative as relative$1 } from "node:path";
|
|
23
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
20
24
|
import chalk from "chalk";
|
|
25
|
+
//#region src/devices/reclaim.ts
|
|
26
|
+
async function reapCollectors(root, { verify = verifyCollectorOwnership, collectors }) {
|
|
27
|
+
const skippedDevices = [];
|
|
28
|
+
const failedDevices = [];
|
|
29
|
+
for (const [platform, record] of Object.entries(collectors)) {
|
|
30
|
+
const rec = record;
|
|
31
|
+
const pid = rec?.pid;
|
|
32
|
+
if (typeof pid !== "number" || pid <= 0 || pid === process.pid || !pidExists(pid)) continue;
|
|
33
|
+
const ownership = verify({
|
|
34
|
+
pid,
|
|
35
|
+
platform,
|
|
36
|
+
root,
|
|
37
|
+
expected: rec
|
|
38
|
+
});
|
|
39
|
+
if (ownership.status === "gone") continue;
|
|
40
|
+
if (ownership.status === "unverified") {
|
|
41
|
+
const name = `${platform} log collector (pid ${pid})`;
|
|
42
|
+
const entry = {
|
|
43
|
+
platform: platform.split(":")[0] === "android" ? "android" : "ios",
|
|
44
|
+
name,
|
|
45
|
+
reason: `${ownership.reason}; keeping the record without signalling the process`
|
|
46
|
+
};
|
|
47
|
+
skippedDevices.push(entry);
|
|
48
|
+
failedDevices.push(entry);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
process.kill(pid, "SIGTERM");
|
|
53
|
+
if (await (0, process_identity_exports.waitForProcessExit)(rec, 5e3)) continue;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (error.code === "ESRCH") continue;
|
|
56
|
+
}
|
|
57
|
+
const entry = {
|
|
58
|
+
platform: platform.split(":")[0] === "android" ? "android" : "ios",
|
|
59
|
+
name: `${platform} log collector (pid ${pid})`,
|
|
60
|
+
reason: "Collector exit could not be confirmed; keeping its ownership record."
|
|
61
|
+
};
|
|
62
|
+
skippedDevices.push(entry);
|
|
63
|
+
failedDevices.push(entry);
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
skippedDevices,
|
|
67
|
+
failedDevices
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function reclaimRemoteSession(root, { stopSession = defaultStopSession } = {}) {
|
|
71
|
+
const sessionId = readRemoteSessionId(root);
|
|
72
|
+
if (!sessionId) return {
|
|
73
|
+
stopped: null,
|
|
74
|
+
failed: null
|
|
75
|
+
};
|
|
76
|
+
let result;
|
|
77
|
+
try {
|
|
78
|
+
result = stopSession(root, sessionId);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
result = {
|
|
81
|
+
status: "failed",
|
|
82
|
+
reason: String(err?.message ?? err)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (result.status === "torn-down") {
|
|
86
|
+
if (result.reason) return {
|
|
87
|
+
stopped: sessionId,
|
|
88
|
+
failed: {
|
|
89
|
+
platform: "ios",
|
|
90
|
+
name: `remote session ${sessionId}`,
|
|
91
|
+
reason: `${result.reason} The session is stopped. Re-run cleanup to reconcile its retained ownership claim.`
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
clearRemoteSession(root, sessionId);
|
|
95
|
+
return {
|
|
96
|
+
stopped: sessionId,
|
|
97
|
+
failed: null
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
stopped: null,
|
|
102
|
+
failed: {
|
|
103
|
+
platform: "ios",
|
|
104
|
+
name: `remote session ${sessionId}`,
|
|
105
|
+
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}`
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function defaultStopSession(root, sessionId) {
|
|
110
|
+
return endRecordedSession({
|
|
111
|
+
root,
|
|
112
|
+
sessionId,
|
|
113
|
+
easBin: resolveEasCliBin(root)?.file ?? null
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function defaultStopMetroTunnel(record) {
|
|
117
|
+
return stopTunnel(record);
|
|
118
|
+
}
|
|
119
|
+
async function reclaimMetroTunnel(root, { stopMetroTunnel = defaultStopMetroTunnel } = {}) {
|
|
120
|
+
const record = readMetroTunnel(root);
|
|
121
|
+
if (record?.kind !== "managed") return {
|
|
122
|
+
stopped: null,
|
|
123
|
+
failed: null
|
|
124
|
+
};
|
|
125
|
+
let result;
|
|
126
|
+
try {
|
|
127
|
+
result = await stopMetroTunnel(record);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
result = {
|
|
130
|
+
status: "failed",
|
|
131
|
+
reason: String(err?.message ?? err)
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (result.status === "failed") return {
|
|
135
|
+
stopped: null,
|
|
136
|
+
failed: {
|
|
137
|
+
platform: "ios",
|
|
138
|
+
name: `${record.provider} tunnel (pid ${record.pid})`,
|
|
139
|
+
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\`.`
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
if (!clearManagedMetroTunnel(root, record)) return {
|
|
143
|
+
stopped: null,
|
|
144
|
+
failed: {
|
|
145
|
+
platform: "ios",
|
|
146
|
+
name: "replacement managed tunnel",
|
|
147
|
+
reason: "A replacement managed tunnel record appeared during cleanup and is retained for a later stop."
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
return {
|
|
151
|
+
stopped: record.provider,
|
|
152
|
+
failed: null
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function describeDereferenced(project) {
|
|
156
|
+
const devices = Object.entries(project?.ports ?? {}).map(([label, port]) => `port ${label} (${port})`);
|
|
157
|
+
for (const { platforms } of projectDeviceSlots(project)) {
|
|
158
|
+
const ios = platforms.ios;
|
|
159
|
+
if (ios?.deviceUdid) devices.push(`ios sim ${ios.deviceUdid}`);
|
|
160
|
+
const android = platforms.android;
|
|
161
|
+
if (android?.avdName) devices.push(`android avd ${android.avdName}`);
|
|
162
|
+
else if (android?.serial) devices.push(`android device ${android.serial}`);
|
|
163
|
+
}
|
|
164
|
+
return devices;
|
|
165
|
+
}
|
|
166
|
+
function parkRequest(projectPath, slot, simslimManaged) {
|
|
167
|
+
const { max, error } = parkedMaxSetting("ios");
|
|
168
|
+
if (error || max <= 0) return void 0;
|
|
169
|
+
return {
|
|
170
|
+
projectPath,
|
|
171
|
+
max,
|
|
172
|
+
simslimManaged,
|
|
173
|
+
slot
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function reclaimOwnedDevices(project, projectPath, { park = false } = {}) {
|
|
177
|
+
const deletedDevices = [];
|
|
178
|
+
const parkedDevices = [];
|
|
179
|
+
const evictedDevices = [];
|
|
180
|
+
const poolNotes = [];
|
|
181
|
+
const skippedDevices = [];
|
|
182
|
+
const failedDevices = [];
|
|
183
|
+
for (const { slot, platforms } of projectDeviceSlots(project)) {
|
|
184
|
+
const ios = platforms.ios;
|
|
185
|
+
if (ios?.owned && ios.deviceUdid) {
|
|
186
|
+
const udid = ios.deviceUdid;
|
|
187
|
+
const label = ios.deviceName || udid;
|
|
188
|
+
const r = teardownOwnedIosSim(udid, {
|
|
189
|
+
del: true,
|
|
190
|
+
label,
|
|
191
|
+
...park ? { park: parkRequest(projectPath, slot, Boolean(ios.simslimManaged)) } : {}
|
|
192
|
+
});
|
|
193
|
+
if (r.parkFallback) poolNotes.push(`could not park ${label}: ${r.parkFallback} -- deleted it instead`);
|
|
194
|
+
for (const failure of r.evictionFailures ?? []) poolNotes.push(failure);
|
|
195
|
+
if (r.parked) {
|
|
196
|
+
parkedDevices.push(r.parked);
|
|
197
|
+
evictedDevices.push(...r.evicted ?? []);
|
|
198
|
+
} else if (r.status === "torn-down") deletedDevices.push(r.label);
|
|
199
|
+
if (!r.parked && (r.status === "torn-down" || r.status === "missing")) clearDevice(projectPath, "ios", slot, udid);
|
|
200
|
+
if (r.status === "skipped") skippedDevices.push({
|
|
201
|
+
platform: "ios",
|
|
202
|
+
name: label,
|
|
203
|
+
udid,
|
|
204
|
+
reason: `${r.reason} -- not touched`
|
|
205
|
+
});
|
|
206
|
+
else if (r.status === "failed") {
|
|
207
|
+
const entry = {
|
|
208
|
+
platform: "ios",
|
|
209
|
+
name: label,
|
|
210
|
+
udid,
|
|
211
|
+
reason: `teardown failed: ${r.reason}`
|
|
212
|
+
};
|
|
213
|
+
skippedDevices.push(entry);
|
|
214
|
+
failedDevices.push(entry);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const android = platforms.android;
|
|
218
|
+
if (android?.owned && android.avdName) {
|
|
219
|
+
const bound = parkedMaxSetting("android");
|
|
220
|
+
const r = teardownOwnedAvd(android.avdName, {
|
|
221
|
+
del: true,
|
|
222
|
+
owner: {
|
|
223
|
+
projectPath,
|
|
224
|
+
slot
|
|
225
|
+
},
|
|
226
|
+
...park && !bound.error && bound.max > 0 ? { park: {
|
|
227
|
+
projectPath,
|
|
228
|
+
slot,
|
|
229
|
+
max: bound.max,
|
|
230
|
+
configuration: typeof android.poolConfiguration === "string" ? android.poolConfiguration : void 0
|
|
231
|
+
} } : {}
|
|
232
|
+
});
|
|
233
|
+
if (r.parkFallback) poolNotes.push(`could not park ${android.avdName}: ${r.parkFallback} -- deleted it instead`);
|
|
234
|
+
for (const failure of r.evictionFailures ?? []) poolNotes.push(failure);
|
|
235
|
+
if (!r.parked && (r.status === "torn-down" || r.status === "missing")) clearDevice(projectPath, "android", slot, android.avdName);
|
|
236
|
+
if (r.parked) {
|
|
237
|
+
parkedDevices.push(r.parked);
|
|
238
|
+
evictedDevices.push(...r.evicted ?? []);
|
|
239
|
+
} else if (r.status === "torn-down") deletedDevices.push(android.avdName);
|
|
240
|
+
else if (r.status === "skipped") skippedDevices.push({
|
|
241
|
+
platform: "android",
|
|
242
|
+
name: android.avdName,
|
|
243
|
+
reason: `${r.reason} -- not touched`
|
|
244
|
+
});
|
|
245
|
+
else if (r.status === "failed") {
|
|
246
|
+
const entry = {
|
|
247
|
+
platform: "android",
|
|
248
|
+
name: android.avdName,
|
|
249
|
+
reason: `teardown failed: ${r.reason}`
|
|
250
|
+
};
|
|
251
|
+
skippedDevices.push(entry);
|
|
252
|
+
failedDevices.push(entry);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
deletedDevices,
|
|
258
|
+
parkedDevices,
|
|
259
|
+
evictedDevices,
|
|
260
|
+
poolNotes,
|
|
261
|
+
skippedDevices,
|
|
262
|
+
failedDevices
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
async function reclaimProject(path, options = {}) {
|
|
266
|
+
const dir = workspaceDir(path);
|
|
267
|
+
const existed = existsSync(dir);
|
|
268
|
+
const run = await withIdleWorkspace(path, () => reclaimIdleProject(path, existed, options), {
|
|
269
|
+
purpose: "workspace removal",
|
|
270
|
+
supervisor: false,
|
|
271
|
+
managedLocks: false
|
|
272
|
+
});
|
|
273
|
+
if (run.ran) return run.value;
|
|
274
|
+
const kept = {
|
|
275
|
+
name: `workspace ${dir}`,
|
|
276
|
+
reason: `kept because ${run.reasons.join("; ")}`
|
|
277
|
+
};
|
|
278
|
+
return {
|
|
279
|
+
path,
|
|
280
|
+
dereferenced: [],
|
|
281
|
+
killedPid: null,
|
|
282
|
+
skippedMetro: null,
|
|
283
|
+
metroPort: getProject(path)?.metroPort ?? null,
|
|
284
|
+
deletedDevices: [],
|
|
285
|
+
parkedDevices: [],
|
|
286
|
+
evictedDevices: [],
|
|
287
|
+
poolNotes: [],
|
|
288
|
+
skippedDevices: [kept],
|
|
289
|
+
failedDevices: [kept],
|
|
290
|
+
keptEntry: true,
|
|
291
|
+
stoppedSession: null,
|
|
292
|
+
stoppedTunnel: null,
|
|
293
|
+
releasedLeases: [],
|
|
294
|
+
removedWorkspaceDirs: [],
|
|
295
|
+
failedWorkspaceDirs: []
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
async function reclaimIdleProject(path, workspaceExisted, { deleteOwnedDevices = false, parkOwnedDevices = false, preserveProjectRecord = false, stopSession = defaultStopSession, stopMetroTunnel = defaultStopMetroTunnel, releaseLeases = releaseWorkspaceLeases, verifyCollector = verifyCollectorOwnership }) {
|
|
299
|
+
await clearNamedPorts(path, { stop: true });
|
|
300
|
+
const project = getProject(path);
|
|
301
|
+
for (const { platforms } of projectDeviceSlots(project)) if (platforms.android?.owned && platforms.android.avdName) (0, ownership_claim_exports.releaseClaim)(acquireAvdClaim(platforms.android.avdName));
|
|
302
|
+
const dereferenced = describeDereferenced(project);
|
|
303
|
+
const initialState = readWorkspaceState(path);
|
|
304
|
+
let killedPid = null;
|
|
305
|
+
let skippedMetro = null;
|
|
306
|
+
let supervisorHeld = false;
|
|
307
|
+
const supervisor = resolveSupervisorTarget({
|
|
308
|
+
state: initialState?.supervisor,
|
|
309
|
+
record: project?.supervisor,
|
|
310
|
+
reservedPort: project?.metroPort
|
|
311
|
+
});
|
|
312
|
+
const orphanServer = {
|
|
313
|
+
pid: Number(initialState?.supervisor?.serverPid),
|
|
314
|
+
processToken: initialState?.supervisor?.serverProcessToken
|
|
315
|
+
};
|
|
316
|
+
const orphanIdentity = supervisor.status === "stale" && typeof orphanServer.processToken === "string" ? (0, process_identity_exports.inspectProcessIdentity)(orphanServer) : null;
|
|
317
|
+
if (supervisor.status === "ours") {
|
|
318
|
+
if (killMetroTree(supervisor.pid, supervisor.processToken) && await (0, process_identity_exports.waitForProcessExit)(supervisor, 1e4)) killedPid = supervisor.pid;
|
|
319
|
+
else {
|
|
320
|
+
skippedMetro = `could not confirm supervisor pid ${supervisor.pid} exited`;
|
|
321
|
+
supervisorHeld = true;
|
|
322
|
+
}
|
|
323
|
+
} else if (supervisor.status === "unverified") {
|
|
324
|
+
skippedMetro = supervisor.reason ?? "supervisor identity could not be verified";
|
|
325
|
+
supervisorHeld = true;
|
|
326
|
+
} else if (orphanIdentity === "same") {
|
|
327
|
+
let signalled = false;
|
|
328
|
+
try {
|
|
329
|
+
signalled = signalProcessTree(orphanServer.pid, "SIGTERM");
|
|
330
|
+
} catch {}
|
|
331
|
+
if (signalled ? await (0, process_identity_exports.waitForProcessExit)(orphanServer, 1e4) : ["gone", "different"].includes((0, process_identity_exports.inspectProcessIdentity)(orphanServer))) killedPid = orphanServer.pid;
|
|
332
|
+
else {
|
|
333
|
+
skippedMetro = `could not confirm dev server pid ${orphanServer.pid} left by the supervisor exited`;
|
|
334
|
+
supervisorHeld = true;
|
|
335
|
+
}
|
|
336
|
+
} else if (orphanIdentity === "unknown") {
|
|
337
|
+
skippedMetro = `the identity of dev server pid ${orphanServer.pid} left by the supervisor could not be verified`;
|
|
338
|
+
supervisorHeld = true;
|
|
339
|
+
} else if (typeof project?.metroPort === "number") {
|
|
340
|
+
if (!(await resolveProjectMetro(project.metroPort, path)).missing) skippedMetro = "Externally started server left alone; no verified Stim supervisor owns it";
|
|
341
|
+
}
|
|
342
|
+
const { skippedDevices: skippedCollectors, failedDevices: failedCollectors } = await reapCollectors(path, {
|
|
343
|
+
verify: verifyCollector,
|
|
344
|
+
collectors: initialState?.collectors ?? {}
|
|
345
|
+
});
|
|
346
|
+
function retainReplacementProcesses() {
|
|
347
|
+
const currentState = readWorkspaceState(path);
|
|
348
|
+
const currentProject = getProject(path);
|
|
349
|
+
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])))) {
|
|
350
|
+
supervisorHeld = true;
|
|
351
|
+
skippedMetro = "A replacement process appeared during cleanup; its ownership records are retained";
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
retainReplacementProcesses();
|
|
355
|
+
const { deletedDevices, parkedDevices, evictedDevices, poolNotes, skippedDevices, failedDevices } = deleteOwnedDevices && !supervisorHeld && failedCollectors.length === 0 ? reclaimOwnedDevices(project, path, { park: parkOwnedDevices }) : {
|
|
356
|
+
deletedDevices: [],
|
|
357
|
+
parkedDevices: [],
|
|
358
|
+
evictedDevices: [],
|
|
359
|
+
poolNotes: [],
|
|
360
|
+
skippedDevices: [],
|
|
361
|
+
failedDevices: []
|
|
362
|
+
};
|
|
363
|
+
skippedDevices.push(...skippedCollectors);
|
|
364
|
+
failedDevices.push(...failedCollectors);
|
|
365
|
+
const remote = reclaimRemoteSession(path, { stopSession });
|
|
366
|
+
if (remote.failed) {
|
|
367
|
+
skippedDevices.push(remote.failed);
|
|
368
|
+
failedDevices.push(remote.failed);
|
|
369
|
+
}
|
|
370
|
+
const tunnel = await reclaimMetroTunnel(path, { stopMetroTunnel });
|
|
371
|
+
if (tunnel.failed) {
|
|
372
|
+
skippedDevices.push(tunnel.failed);
|
|
373
|
+
failedDevices.push(tunnel.failed);
|
|
374
|
+
}
|
|
375
|
+
let releasedLeases = [];
|
|
376
|
+
try {
|
|
377
|
+
releasedLeases = releaseLeases(path);
|
|
378
|
+
} catch {
|
|
379
|
+
releasedLeases = [];
|
|
380
|
+
}
|
|
381
|
+
retainReplacementProcesses();
|
|
382
|
+
const removedWorkspaceDirs = [];
|
|
383
|
+
const failedWorkspaceDirs = [];
|
|
384
|
+
if (failedDevices.length === 0 && !supervisorHeld) {
|
|
385
|
+
const dir = workspaceDir(path);
|
|
386
|
+
if (existsSync(dir)) try {
|
|
387
|
+
emptyWorkspaceDir(dir);
|
|
388
|
+
if (workspaceExisted) removedWorkspaceDirs.push(dir);
|
|
389
|
+
} catch {
|
|
390
|
+
failedWorkspaceDirs.push(dir);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const keptEntry = supervisorHeld || failedDevices.length > 0 || failedWorkspaceDirs.length > 0;
|
|
394
|
+
if (project && !keptEntry && !preserveProjectRecord) removeProject(path);
|
|
395
|
+
return {
|
|
396
|
+
path,
|
|
397
|
+
dereferenced,
|
|
398
|
+
killedPid,
|
|
399
|
+
skippedMetro,
|
|
400
|
+
metroPort: project?.metroPort ?? null,
|
|
401
|
+
deletedDevices,
|
|
402
|
+
parkedDevices,
|
|
403
|
+
evictedDevices,
|
|
404
|
+
poolNotes,
|
|
405
|
+
skippedDevices,
|
|
406
|
+
failedDevices,
|
|
407
|
+
keptEntry,
|
|
408
|
+
stoppedSession: remote.stopped,
|
|
409
|
+
stoppedTunnel: tunnel.stopped,
|
|
410
|
+
releasedLeases,
|
|
411
|
+
removedWorkspaceDirs,
|
|
412
|
+
failedWorkspaceDirs
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
//#endregion
|
|
21
416
|
//#region src/engine/warm-claim.ts
|
|
22
417
|
const WARM_CLAIM_LABEL = "worktree warm";
|
|
23
418
|
const WARM_CLAIM_POLL_MS = 250;
|
|
@@ -38,10 +433,10 @@ function defaultSleep(ms) {
|
|
|
38
433
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
39
434
|
}
|
|
40
435
|
function warmClaimsDir() {
|
|
41
|
-
return join(getConfigDir(), "warm-locks");
|
|
436
|
+
return join$1(getConfigDir(), "warm-locks");
|
|
42
437
|
}
|
|
43
438
|
function warmClaimPath(repositoryRoot) {
|
|
44
|
-
return join(warmClaimsDir(), `${workspaceName(repositoryRoot)}.lock`);
|
|
439
|
+
return join$1(warmClaimsDir(), `${workspaceName(repositoryRoot)}.lock`);
|
|
45
440
|
}
|
|
46
441
|
function warmCommand(phase) {
|
|
47
442
|
return phase === "refresh" ? "stim worktree warm --refresh" : "stim worktree warm";
|
|
@@ -368,11 +763,11 @@ function stepLine(label, reason, action) {
|
|
|
368
763
|
return phaseLine(label, `${reason} -> ${action}`);
|
|
369
764
|
}
|
|
370
765
|
function installRecordFile(depsRoot) {
|
|
371
|
-
return join(getConfigDir(), "warm-installs", `${workspaceName(depsRoot)}.json`);
|
|
766
|
+
return join$1(getConfigDir(), "warm-installs", `${workspaceName(depsRoot)}.json`);
|
|
372
767
|
}
|
|
373
768
|
function lockfileHash(depsRoot, lockfile) {
|
|
374
769
|
try {
|
|
375
|
-
return createHash("sha256").update(readFileSync(join(depsRoot, lockfile))).digest("hex");
|
|
770
|
+
return createHash("sha256").update(readFileSync(join$1(depsRoot, lockfile))).digest("hex");
|
|
376
771
|
} catch {
|
|
377
772
|
return null;
|
|
378
773
|
}
|
|
@@ -394,7 +789,7 @@ function readInstallEvidence(depsRoot) {
|
|
|
394
789
|
function writeInstallEvidence(depsRoot, lockfile, hash, completed) {
|
|
395
790
|
const file = installRecordFile(depsRoot);
|
|
396
791
|
const tmp = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
397
|
-
mkdirSync(join(getConfigDir(), "warm-installs"), { recursive: true });
|
|
792
|
+
mkdirSync(join$1(getConfigDir(), "warm-installs"), { recursive: true });
|
|
398
793
|
writeFileSync(tmp, JSON.stringify({
|
|
399
794
|
lock: lockfile,
|
|
400
795
|
hash: hash ?? "",
|
|
@@ -412,9 +807,9 @@ function restoreInstallEvidence(depsRoot, prior) {
|
|
|
412
807
|
else rmSync(installRecordFile(depsRoot), { force: true });
|
|
413
808
|
}
|
|
414
809
|
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;
|
|
810
|
+
if (existsSync(join$1(gitDir, "rebase-merge"))) return "rebase";
|
|
811
|
+
if (existsSync(join$1(gitDir, "rebase-apply"))) return existsSync(join$1(gitDir, "rebase-apply", "applying")) ? "am" : "rebase";
|
|
812
|
+
return existsSync(join$1(gitDir, "MERGE_HEAD")) ? "merge" : null;
|
|
418
813
|
}
|
|
419
814
|
function readMainCheckoutState(root) {
|
|
420
815
|
const exec = getExecutor();
|
|
@@ -668,7 +1063,7 @@ async function refreshMainCheckout({ root, appDir, settings, emit, spawnFn = nul
|
|
|
668
1063
|
const consumed = dependencies ? lockfileHash(dependencies.root, dependencies.lock) : null;
|
|
669
1064
|
const deps = depsPlan({
|
|
670
1065
|
lockfile: dependencies?.lock ?? null,
|
|
671
|
-
lockfileChanged: dependencies ? pathChangedBetween(root, before, head, gitPath(relative(root, dependencies.root), dependencies.lock)) : false,
|
|
1066
|
+
lockfileChanged: dependencies ? pathChangedBetween(root, before, head, gitPath(relative$1(root, dependencies.root), dependencies.lock)) : false,
|
|
672
1067
|
installed,
|
|
673
1068
|
treeValid: dependencies?.lock === "package-lock.json" && installed ? installedNpmTreeIsValid(dependencies.root) : null,
|
|
674
1069
|
lastInstall: priorInstall,
|
|
@@ -700,11 +1095,11 @@ async function refreshMainCheckout({ root, appDir, settings, emit, spawnFn = nul
|
|
|
700
1095
|
if (marked) restoreInstallEvidence(marked.root, priorInstall);
|
|
701
1096
|
emit(stepLine("deps", depsSubject, "skipped"));
|
|
702
1097
|
}
|
|
703
|
-
const app = relative(root, appDir) || ".";
|
|
1098
|
+
const app = relative$1(root, appDir) || ".";
|
|
704
1099
|
const where = `source ${appDir}: `;
|
|
705
1100
|
const podState = readPodState(appDir);
|
|
706
1101
|
const pods = podsPlan({
|
|
707
|
-
hasIos: existsSync(join(appDir, "ios")),
|
|
1102
|
+
hasIos: existsSync(join$1(appDir, "ios")),
|
|
708
1103
|
hasPodfile: podState.hasPodfile,
|
|
709
1104
|
podfileLockChanged: pathChangedBetween(root, before, head, gitPath(app, "ios", "Podfile.lock")),
|
|
710
1105
|
stale: podsAreStale(podState.lockText, podState.manifestText)
|
|
@@ -735,16 +1130,22 @@ async function refreshMainCheckout({ root, appDir, settings, emit, spawnFn = nul
|
|
|
735
1130
|
//#endregion
|
|
736
1131
|
//#region src/commands/worktree.ts
|
|
737
1132
|
function dependencyInstallCommand(target, dir = ".") {
|
|
738
|
-
const installRoot = resolve
|
|
1133
|
+
const installRoot = resolve(target, dir);
|
|
739
1134
|
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
|
|
1135
|
+
if (existsSync$1(resolve(installRoot, "pnpm-lock.yaml"))) command = "pnpm install";
|
|
1136
|
+
else if (existsSync$1(resolve(installRoot, "yarn.lock"))) command = "yarn install";
|
|
1137
|
+
else if (existsSync$1(resolve(installRoot, "bun.lock")) || existsSync$1(resolve(installRoot, "bun.lockb"))) command = "bun install";
|
|
1138
|
+
else if (existsSync$1(resolve(installRoot, "package-lock.json"))) command = "npm ci";
|
|
744
1139
|
return `cd '${installRoot.replaceAll("'", "'\\''")}' && ${command}`;
|
|
745
1140
|
}
|
|
746
1141
|
function reportCarriedStateHealth(root, target, copied) {
|
|
747
|
-
const
|
|
1142
|
+
const outOfSync = depsOutOfSync(root, target, copied);
|
|
1143
|
+
for (const d of outOfSync.filter((dependency) => dependency.reason === "installed")) {
|
|
1144
|
+
const where = d.dir === "." ? d.lockfile : `${d.dir}/${d.lockfile}`;
|
|
1145
|
+
const modules = d.dir === "." ? "node_modules" : `${d.dir}/node_modules`;
|
|
1146
|
+
console.error(chalk.yellow(phaseLine("carry", `carried ${modules} was installed from a different ${d.lockfile} than the ${where} on disk here. node_modules is gitignored and cloned from the source checkout; ${d.lockfile} is tracked, so the two can disagree. Run \`${dependencyInstallCommand(target, d.dir)}\` before building, or pod install and native builds see the wrong package versions.`)));
|
|
1147
|
+
}
|
|
1148
|
+
const staleDeps = outOfSync.filter((dependency) => dependency.reason === "lockfile");
|
|
748
1149
|
if (staleDeps.length) {
|
|
749
1150
|
const manifests = staleDeps.map((d) => d.dir === "." ? d.lockfile : `${d.dir}/${d.lockfile}`).join(", ");
|
|
750
1151
|
const remedies = [...new Set(staleDeps.map((dependency) => dependencyInstallCommand(target, dependency.dir)))];
|
|
@@ -752,7 +1153,7 @@ function reportCarriedStateHealth(root, target, copied) {
|
|
|
752
1153
|
}
|
|
753
1154
|
for (const p of podsOutOfSync(target, copied)) {
|
|
754
1155
|
const where = p.dir === "." ? "Podfile.lock" : `${p.dir}/Podfile.lock`;
|
|
755
|
-
const pod = podInstallCommand(p.dir === "." ? target : resolve
|
|
1156
|
+
const pod = podInstallCommand(p.dir === "." ? target : resolve(target, p.dir, ".."));
|
|
756
1157
|
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
1158
|
}
|
|
758
1159
|
}
|
|
@@ -766,14 +1167,16 @@ function reportRefreshFailure(failure) {
|
|
|
766
1167
|
function mainCheckoutAppDir(root, target) {
|
|
767
1168
|
const app = findProjectRoot(process.cwd());
|
|
768
1169
|
if (!app) return root;
|
|
769
|
-
const rel = relative
|
|
770
|
-
if (!rel || rel.startsWith("..") || isAbsolute
|
|
771
|
-
return resolve
|
|
1170
|
+
const rel = relative(target, app);
|
|
1171
|
+
if (!rel || rel.startsWith("..") || isAbsolute(rel)) return root;
|
|
1172
|
+
return resolve(root, rel);
|
|
772
1173
|
}
|
|
773
1174
|
function registerWarm(worktree) {
|
|
774
1175
|
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
1176
|
try {
|
|
776
1177
|
const { root, target, common } = warmWorktreePaths(process.cwd());
|
|
1178
|
+
const app = findProjectRoot(process.cwd());
|
|
1179
|
+
if (app) recordWorkspaceUse(app);
|
|
777
1180
|
const readSettings = () => {
|
|
778
1181
|
const settings = resolveSettings({
|
|
779
1182
|
gitCommonDir: common,
|
|
@@ -920,10 +1323,20 @@ function excludePodChurn(lines) {
|
|
|
920
1323
|
restore
|
|
921
1324
|
};
|
|
922
1325
|
}
|
|
1326
|
+
function nativeCanonicalPath(path) {
|
|
1327
|
+
const missing = [];
|
|
1328
|
+
for (let existing = resolve(path);; existing = dirname(existing)) try {
|
|
1329
|
+
return resolve(realpathSync$1.native(existing), ...missing);
|
|
1330
|
+
} catch {
|
|
1331
|
+
if (dirname(existing) === existing) return resolve(path);
|
|
1332
|
+
missing.unshift(basename(existing));
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
923
1335
|
function matchWorktreeEntry(entries, path) {
|
|
924
1336
|
let best = null;
|
|
1337
|
+
const target = nativeCanonicalPath(path);
|
|
925
1338
|
(entries || []).forEach((entry, index) => {
|
|
926
|
-
if (!entry?.path || !isPathPrefix(entry.path,
|
|
1339
|
+
if (!entry?.path || !isPathPrefix(nativeCanonicalPath(entry.path), target)) return;
|
|
927
1340
|
if (!best || entry.path.length > best.path.length) best = {
|
|
928
1341
|
...entry,
|
|
929
1342
|
index
|
|
@@ -972,8 +1385,9 @@ function describeKeptDevice(s) {
|
|
|
972
1385
|
function reclaimKeys(rootPath) {
|
|
973
1386
|
const cfg = loadConfig();
|
|
974
1387
|
const keys = /* @__PURE__ */ new Set([rootPath]);
|
|
1388
|
+
const root = nativeCanonicalPath(rootPath);
|
|
975
1389
|
if (cfg?.projects) {
|
|
976
|
-
for (const key of Object.keys(cfg.projects)) if (isPathPrefix(rootPath, key)) keys.add(key);
|
|
1390
|
+
for (const key of Object.keys(cfg.projects)) if (isPathPrefix(rootPath, key) || isPathPrefix(root, nativeCanonicalPath(key))) keys.add(key);
|
|
977
1391
|
}
|
|
978
1392
|
return [...keys].toSorted();
|
|
979
1393
|
}
|
|
@@ -998,11 +1412,22 @@ async function reclaimAll(rootPath, keys = reclaimKeys(rootPath), { preserveRoot
|
|
|
998
1412
|
const removedWorkspaceDirs = [];
|
|
999
1413
|
const failedWorkspaceDirs = [];
|
|
1000
1414
|
for (const key of keys) {
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1415
|
+
let r;
|
|
1416
|
+
try {
|
|
1417
|
+
r = await reclaimProject(key, {
|
|
1418
|
+
deleteOwnedDevices: true,
|
|
1419
|
+
parkOwnedDevices: true,
|
|
1420
|
+
preserveProjectRecord: preserveRootProject && key === rootPath
|
|
1421
|
+
});
|
|
1422
|
+
} catch (error) {
|
|
1423
|
+
keptEntries.push(key);
|
|
1424
|
+
retainedResources.push({
|
|
1425
|
+
name: "ownership state",
|
|
1426
|
+
reason: String(error?.message || error),
|
|
1427
|
+
project: key
|
|
1428
|
+
});
|
|
1429
|
+
continue;
|
|
1430
|
+
}
|
|
1006
1431
|
dereferenced.push(...r.dereferenced);
|
|
1007
1432
|
if (r.killedPid) killedPids.push(r.killedPid);
|
|
1008
1433
|
deletedDevices.push(...r.deletedDevices);
|
|
@@ -1116,30 +1541,32 @@ function removalPath(target) {
|
|
|
1116
1541
|
return canonicalExistingPath(target ?? process.cwd());
|
|
1117
1542
|
}
|
|
1118
1543
|
function canonicalExistingPath(target) {
|
|
1119
|
-
const resolved = resolve
|
|
1544
|
+
const resolved = resolve(target);
|
|
1120
1545
|
let existing = resolved;
|
|
1121
1546
|
const missing = [];
|
|
1122
1547
|
while (!existsSync$1(existing)) {
|
|
1123
|
-
const parent = dirname
|
|
1548
|
+
const parent = dirname(existing);
|
|
1124
1549
|
if (parent === existing) return resolved;
|
|
1125
|
-
missing.unshift(basename
|
|
1550
|
+
missing.unshift(basename(existing));
|
|
1126
1551
|
existing = parent;
|
|
1127
1552
|
}
|
|
1128
|
-
return resolve
|
|
1553
|
+
return resolve(realpathSync$1(existing), ...missing);
|
|
1129
1554
|
}
|
|
1130
|
-
function inspectRemoval(path) {
|
|
1555
|
+
function inspectRemoval(path, mergedHead) {
|
|
1131
1556
|
const gitAnswered = hasUncommittedWork(path);
|
|
1132
1557
|
const { lines: dirtyLines, restore: podChurn } = excludePodChurn(gitAnswered ? dirtyPaths(path, { limit: Infinity }) : []);
|
|
1133
1558
|
const dirty = gitAnswered === null ? null : dirtyLines.length > 0;
|
|
1134
1559
|
const unpushed = unpushedCommits(path);
|
|
1560
|
+
const blockers = removalBlockers({
|
|
1561
|
+
dirty,
|
|
1562
|
+
unpushed: Boolean(mergedHead) && resolveFullRef(path, "HEAD") === mergedHead && unpushed ? [] : unpushed
|
|
1563
|
+
});
|
|
1564
|
+
if (hasPopulatedSubmodules(path)) blockers.push("initialized submodules, which git removes only with --force");
|
|
1135
1565
|
return {
|
|
1136
1566
|
dirtyLines,
|
|
1137
1567
|
podChurn,
|
|
1138
1568
|
unpushed,
|
|
1139
|
-
blockers
|
|
1140
|
-
dirty,
|
|
1141
|
-
unpushed
|
|
1142
|
-
})
|
|
1569
|
+
blockers
|
|
1143
1570
|
};
|
|
1144
1571
|
}
|
|
1145
1572
|
function printRemovalRefusal(path, inspection) {
|
|
@@ -1171,7 +1598,14 @@ function printRemovalCleanup(result, failed) {
|
|
|
1171
1598
|
if (result.dereferenced.length) console.error(chalk.dim(phaseLine("workspace", `no longer referenced: ${result.dereferenced.join(", ")}`)));
|
|
1172
1599
|
for (const skipped of result.skippedDevices) console.error((failed ? chalk.dim : chalk.yellow)(phaseLine("device", `kept ${describeKeptDevice(skipped)} (${skipped.reason})`)));
|
|
1173
1600
|
}
|
|
1174
|
-
async function
|
|
1601
|
+
async function removeWorktreeTarget(target, opts = {}) {
|
|
1602
|
+
let removed = false;
|
|
1603
|
+
await runRemove(target, opts, () => {
|
|
1604
|
+
removed = true;
|
|
1605
|
+
});
|
|
1606
|
+
return removed;
|
|
1607
|
+
}
|
|
1608
|
+
async function runRemove(target, opts, onRemoved) {
|
|
1175
1609
|
const poolError = parkedMaxSetting("ios").error || parkedMaxSetting("android").error;
|
|
1176
1610
|
if (poolError) {
|
|
1177
1611
|
console.error(chalk.red(poolError));
|
|
@@ -1196,7 +1630,7 @@ async function runRemove(target, opts = {}) {
|
|
|
1196
1630
|
console.error(chalk.dim(phaseLine("branch", `${branch} is already absent; cleared its pending cleanup record`)));
|
|
1197
1631
|
return;
|
|
1198
1632
|
}
|
|
1199
|
-
const checkedOutAt = listWorktrees(mainRoot).find((candidate) => candidate.branch === branch && resolve
|
|
1633
|
+
const checkedOutAt = listWorktrees(mainRoot).find((candidate) => candidate.branch === branch && resolve(candidate.path) !== resolve(path))?.path;
|
|
1200
1634
|
if (checkedOutAt) {
|
|
1201
1635
|
console.error(chalk.red(`Refusing pending cleanup for ${branch}: it is checked out at ${checkedOutAt}.`));
|
|
1202
1636
|
console.error(chalk.dim("Switch that worktree to another branch, then retry this command."));
|
|
@@ -1228,7 +1662,7 @@ async function runRemove(target, opts = {}) {
|
|
|
1228
1662
|
const worktrees = listWorktrees(path);
|
|
1229
1663
|
const entry = matchWorktreeEntry(worktrees, path);
|
|
1230
1664
|
if (!entry) {
|
|
1231
|
-
if (gitCommonDir(path) === null && hasRegisteredProjectUnder(path)) {
|
|
1665
|
+
if (!opts.linkedOnly && gitCommonDir(path) === null && hasRegisteredProjectUnder(path)) {
|
|
1232
1666
|
await reclaimEnvironment(path, "it is not a git repository");
|
|
1233
1667
|
return;
|
|
1234
1668
|
}
|
|
@@ -1249,6 +1683,11 @@ async function runRemove(target, opts = {}) {
|
|
|
1249
1683
|
process.exitCode = 1;
|
|
1250
1684
|
return;
|
|
1251
1685
|
}
|
|
1686
|
+
if (entry.path === source.path && opts.linkedOnly) {
|
|
1687
|
+
console.error(chalk.red(`Refusing to remove ${path}: ${entry.path} is the source checkout.`));
|
|
1688
|
+
process.exitCode = 1;
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1252
1691
|
if (entry.path === source.path) {
|
|
1253
1692
|
if (entry.path !== path) console.error(chalk.dim(`${path} is inside the source checkout ${entry.path}; reclaiming its environment.`));
|
|
1254
1693
|
await reclaimEnvironment(entry.path, "it is the source checkout");
|
|
@@ -1258,11 +1697,17 @@ async function runRemove(target, opts = {}) {
|
|
|
1258
1697
|
console.error(chalk.dim(`${path} is inside the worktree ${entry.path}; removing that.`));
|
|
1259
1698
|
path = entry.path;
|
|
1260
1699
|
}
|
|
1700
|
+
if (entry.locked) {
|
|
1701
|
+
console.error(chalk.red(`Refusing to remove ${path}: git has it locked.`));
|
|
1702
|
+
console.error(chalk.dim(`Unlock it, then retry: git -C ${source.path} worktree unlock ${path}`));
|
|
1703
|
+
process.exitCode = 1;
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1261
1706
|
const project = getProject(path);
|
|
1262
1707
|
const branch = entry.branch;
|
|
1263
1708
|
const ownsBranch = Boolean(branch && project?.worktreeBranchOwned === true && project.worktreeBranch === branch);
|
|
1264
1709
|
const approvedBranchSha = ownsBranch ? resolveFullRef(path, "HEAD") : null;
|
|
1265
|
-
const inspection = inspectRemoval(path);
|
|
1710
|
+
const inspection = inspectRemoval(path, opts.mergedHead);
|
|
1266
1711
|
if (inspection.blockers.length && !opts.force) {
|
|
1267
1712
|
printRemovalRefusal(path, inspection);
|
|
1268
1713
|
return;
|
|
@@ -1271,12 +1716,26 @@ async function runRemove(target, opts = {}) {
|
|
|
1271
1716
|
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
1717
|
const branchDeleteCwd = source.path;
|
|
1273
1718
|
await withManagedRemoteWorktreeRemovalLock(path, () => withReclaimLocks(path, async (lockedKeys) => {
|
|
1719
|
+
if (opts.guard?.(lockedKeys).length) return;
|
|
1720
|
+
const inspectedHead = resolveFullRef(path, "HEAD");
|
|
1721
|
+
const current = inspectRemoval(path, opts.mergedHead);
|
|
1722
|
+
if (current.blockers.length && !opts.force) {
|
|
1723
|
+
printRemovalRefusal(path, current);
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1274
1726
|
const result = await reclaimAll(path, lockedKeys, { preserveRootProject: true });
|
|
1275
1727
|
if (result.keptEntries.length) {
|
|
1276
1728
|
reportRetainedResources(path, result);
|
|
1277
1729
|
return;
|
|
1278
1730
|
}
|
|
1279
|
-
|
|
1731
|
+
if (!opts.force && resolveFullRef(path, "HEAD") !== inspectedHead) {
|
|
1732
|
+
console.error(chalk.red(`Refusing to remove ${path}: its HEAD moved while its environment was reclaimed.`));
|
|
1733
|
+
console.error(chalk.dim(`The directory and Stim ownership record for ${path} were kept.`));
|
|
1734
|
+
printRemovalCleanup(result, true);
|
|
1735
|
+
process.exitCode = 1;
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
restorePodChurn(path, current.podChurn);
|
|
1280
1739
|
try {
|
|
1281
1740
|
removeWorktree(path, {
|
|
1282
1741
|
from: source.path,
|
|
@@ -1291,6 +1750,7 @@ async function runRemove(target, opts = {}) {
|
|
|
1291
1750
|
process.exitCode = 1;
|
|
1292
1751
|
return;
|
|
1293
1752
|
}
|
|
1753
|
+
onRemoved();
|
|
1294
1754
|
const finish = () => {
|
|
1295
1755
|
printRemovalCleanup(result, false);
|
|
1296
1756
|
console.error(chalk.dim(phaseLine("removed", path)));
|
|
@@ -1306,7 +1766,7 @@ async function runRemove(target, opts = {}) {
|
|
|
1306
1766
|
worktreeRemovalComplete: true,
|
|
1307
1767
|
worktreePendingBranchSha: approvedBranchSha
|
|
1308
1768
|
});
|
|
1309
|
-
const checkedOutAt = listWorktrees(branchDeleteCwd).find((candidate) => candidate.branch === branch && resolve
|
|
1769
|
+
const checkedOutAt = listWorktrees(branchDeleteCwd).find((candidate) => candidate.branch === branch && resolve(candidate.path) !== resolve(path))?.path;
|
|
1310
1770
|
if (checkedOutAt) {
|
|
1311
1771
|
console.error(chalk.yellow(phaseLine("branch", `kept ${branch} (it is checked out at ${checkedOutAt})`)));
|
|
1312
1772
|
console.error(chalk.dim(` Switch that worktree to another branch, then retry: stim worktree remove ${path}`));
|
|
@@ -1330,7 +1790,9 @@ async function runRemove(target, opts = {}) {
|
|
|
1330
1790
|
}));
|
|
1331
1791
|
}
|
|
1332
1792
|
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(
|
|
1793
|
+
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) => {
|
|
1794
|
+
await removeWorktreeTarget(target, { force: opts.force });
|
|
1795
|
+
});
|
|
1334
1796
|
}
|
|
1335
1797
|
function worktreeCommand(program) {
|
|
1336
1798
|
const worktree = program.command("worktree").description("Warm and remove Git worktrees");
|
|
@@ -1338,4 +1800,4 @@ function worktreeCommand(program) {
|
|
|
1338
1800
|
registerRemove(worktree);
|
|
1339
1801
|
}
|
|
1340
1802
|
//#endregion
|
|
1341
|
-
export {
|
|
1803
|
+
export { reclaimKeys as a, removalBlockers as c, removeWorktreeTarget as d, worktreeCommand as f, 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 };
|