stim 1.14.0 → 1.16.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 +2 -0
- package/dist/activity-BxwdD81S.mjs +609 -0
- package/dist/agent-device-usage-output-Dpih1Tfa.mjs +411 -0
- package/dist/{android-d6rGq1pA.mjs → android-B_NumVyQ.mjs} +169 -81
- package/dist/{android-LbPFRWVL.mjs → android-Ift4kVvt.mjs} +517 -413
- package/dist/android-cas-8U4DPqeR.mjs +167 -0
- package/dist/android-cas-compiler.mjs +2 -2
- package/dist/{android-XpvCrV7q.mjs → android-r2rBL8kV.mjs} +2 -2
- package/dist/{app-install-BpgSgkJj.mjs → app-install-CKWdbXIX.mjs} +128 -151
- package/dist/attempt-Bk2ouQVL.mjs +39 -0
- package/dist/budget-DjrURfQn.mjs +2700 -0
- package/dist/{build-plan-Dcr-3ie0.mjs → build-plan-DjeSb2Xl.mjs} +192 -653
- package/dist/build-slots-rUk77xqh.mjs +132 -0
- package/dist/{cache-manifest-C68QQ0ua.mjs → cache-manifest-DnUHBJaU.mjs} +2 -2
- package/dist/cache-manifest.mjs +1 -1
- package/dist/{chrome-BPCiC3ZX.mjs → chrome-BrcVWQyD.mjs} +1 -1
- package/dist/cli-BkYZ_6hy.mjs +51 -0
- package/dist/cli.mjs +6 -42
- package/dist/client-BTY686R6.mjs +1368 -0
- package/dist/collector-run.mjs +9 -195
- package/dist/{command-output-DVcpw6eE.mjs → command-output-DnJMdEY-.mjs} +1 -1
- package/dist/{config-lvc7d3C2.mjs → config-D0DbpZtM.mjs} +28 -29
- package/dist/{created-devices-CllH_8eb.mjs → created-devices-D5owjN4C.mjs} +2 -2
- package/dist/{dependency-state-DsT7aKeo.mjs → dependency-state-B6cMXZab.mjs} +2 -2
- package/dist/{deps-D9IcKqXS.mjs → deps-urbmfXig.mjs} +3 -3
- package/dist/{detached-entry-DJHoca49.mjs → detached-entry-CiTEgMR1.mjs} +1 -1
- package/dist/dev-client-CroAIjjj.mjs +43 -0
- package/dist/{device-CjuuAJjQ.mjs → device-DZQjF7H5.mjs} +8 -7
- package/dist/device-capacity-BHwxNBlu.mjs +136 -0
- package/dist/device-host-worker.d.mts +1 -0
- package/dist/device-host-worker.mjs +964 -0
- package/dist/device-ios-BadmeL2V.mjs +381 -0
- package/dist/{device-lease-5c3WrVRm.mjs → device-lease-D-hLqx1b.mjs} +43 -36
- package/dist/{device-pool-BiyDyPP-.mjs → device-lease-run-CqjShqWW.mjs} +6 -126
- package/dist/device-pool-CUaEWMMm.mjs +125 -0
- package/dist/{device-remote-D87zEzaq.mjs → device-remote-D5WVWqOq.mjs} +69 -163
- package/dist/{doctor-x9pnU9fq.mjs → doctor-Cimf-gGB.mjs} +241 -103
- package/dist/{error-diagnostics-C5HzcwCW.mjs → error-diagnostics-B9L4Ae0x.mjs} +15 -18
- package/dist/{exec-DK8KaVHK.mjs → exec-CaErVv7t.mjs} +5 -1
- package/dist/{gc-ko6mUAYD.mjs → gc-Bqy4bjEu.mjs} +599 -108
- package/dist/{android-cas-iZK3DOCr.mjs → gradle-BsO1MdsH.mjs} +8 -294
- package/dist/{guide-6R0VNFqI.mjs → guide-B_uyKTxW.mjs} +2511 -239
- package/dist/{guide-status-BZrFPIA_.mjs → guide-status-CjEZMAD4.mjs} +3 -3
- package/dist/hosted-client-a-WmqE2L.mjs +197 -0
- package/dist/hosted-ios-CIWelIqf.mjs +220 -0
- package/dist/hosted-logs-D5g041Q3.mjs +187 -0
- package/dist/hosted-macos-BSZyIb3y.mjs +135 -0
- package/dist/idle-DYVymG92.mjs +1099 -0
- package/dist/idle-shutdown-CTsDnAde.mjs +122 -0
- package/dist/in-use-DvBw0Kxo.mjs +130 -0
- package/dist/{ios-BwDPrnYQ.mjs → ios-BaInn3sW.mjs} +201 -10
- package/dist/ios-Cr1h5gnO.mjs +189 -0
- package/dist/{ios-P1f5nm2n.mjs → ios-aq3QoGI3.mjs} +1159 -633
- package/dist/{ios-device-BiOsSVzd.mjs → ios-device-Ee7sDXfp.mjs} +1 -1
- package/dist/{ios-device-DGglU3E_.mjs → ios-device-FfCN_C8p.mjs} +12 -7
- package/dist/ios-state-DS5UpYTH.mjs +48 -0
- package/dist/{launch-verify-Deg4tB3Z.mjs → launch-verify-2DBRizjo.mjs} +5 -5
- package/dist/logs-CHEVK8zD.mjs +371 -0
- package/dist/machines-BIBUdhpj.mjs +265 -0
- package/dist/macos-CSYEAFmF.mjs +473 -0
- package/dist/macos-run.d.mts +5 -0
- package/dist/macos-run.mjs +2 -0
- package/dist/maintenance-run.d.mts +4 -0
- package/dist/maintenance-run.mjs +254 -0
- package/dist/{metro-CcEo8Fl8.mjs → metro-BeK1ZPz1.mjs} +20 -120
- package/dist/metro-gateway-BJ-rxQAB.mjs +84 -0
- package/dist/{named-ports-Blg8IxOh.mjs → named-ports-Bk_YXEmE.mjs} +6 -59
- package/dist/native-run-NaBrRFtQ.mjs +147 -0
- package/dist/{native-runtime-DjBHRg5m.mjs → native-runtime-DBzRnEKS.mjs} +9 -5
- package/dist/{ndjson-CDY9IkVC.mjs → ndjson-PclOIwVY.mjs} +2 -2
- package/dist/offload-worker.d.mts +122 -0
- package/dist/offload-worker.mjs +606 -0
- package/dist/{ownership-DyYDF5ww.mjs → ownership-DiA16AsN.mjs} +3 -2
- package/dist/ownership-hCkPzGmr.mjs +571 -0
- package/dist/{page-BHUzTDFt.mjs → page-QF_Ke10s.mjs} +7 -2
- package/dist/{ports-BguS83ex.mjs → ports-R2k4Knz3.mjs} +3 -3
- package/dist/prebuild-D2fsca_Z.mjs +213 -0
- package/dist/preview-DhVbvPAZ.mjs +318 -0
- package/dist/process-identity-BmRm1qV0.mjs +12 -0
- package/dist/{project-CWlvEaeN.mjs → project-Du91Ilvc.mjs} +74 -19
- package/dist/pull-request-pxnDCQzn.mjs +322 -0
- package/dist/pull-requests.d.mts +120 -0
- package/dist/pull-requests.mjs +2 -0
- package/dist/recordings-BIXOzVnr.mjs +37 -0
- package/dist/{reload-BiniwsSZ.mjs → reload-rM9jV1fz.mjs} +109 -24
- package/dist/{remote-cache-CcR6o6lI.mjs → remote-cache-C8-V13XN.mjs} +5 -5
- package/dist/run-f4vI6EnK.mjs +119 -0
- package/dist/{server-bare-DGfgyJ5J.mjs → server-bare-CpPGmnmO.mjs} +33 -15
- package/dist/server-expo-CVqO2c35.mjs +2 -0
- package/dist/{server-expo-BVuwQ65v.mjs → server-expo-MC0rHlOC.mjs} +8 -12
- package/dist/settings-BBpMcDbD.mjs +25 -0
- package/dist/{settings-B-fsC-Om.mjs → settings-ZkDzkzvB.mjs} +22 -10
- package/dist/settings-oTSkkNrM.mjs +1439 -0
- package/dist/settings.schema.json +845 -97
- package/dist/{simslim-DrDN4WuV.mjs → simslim-B8-7AoTI.mjs} +8 -132
- package/dist/{slot-launch-D5t8EiMf.mjs → slot-launch-B8VrHxn1.mjs} +11 -8
- package/dist/{spawn-claims-Drw_o-Fb.mjs → spawn-claims-D0XfSe8u.mjs} +1 -1
- package/dist/spawn-entry-jw19m8Nl.mjs +17 -0
- package/dist/stage-CzaiwFsI.mjs +143 -0
- package/dist/start-CX8Ig-U2.mjs +2 -0
- package/dist/{start-s4RXp1wy.mjs → start-ClDfwXa4.mjs} +116 -65
- package/dist/state-3DwSQIBK.mjs +41 -0
- package/dist/state-B4PQFXMW.d.mts +12 -0
- package/dist/{state-Dk9RVXI7.mjs → state-C1jB7Q8q.mjs} +70 -8
- package/dist/{state-DHqvsbqC.mjs → state-DcW6vOd_.mjs} +7 -4
- package/dist/stats-CEZ_i5je.mjs +77 -0
- package/dist/{status-BgAuxESg.mjs → status-BuvBFeII.mjs} +118 -20
- package/dist/{status-BsPAnfOI.mjs → status-Cs7VLg9D.mjs} +542 -283
- package/dist/status-DlKvVJBE.mjs +49 -0
- package/dist/stdout-reader-C8yjre1x.mjs +23 -0
- package/dist/stim-desktop-Cnz8CPnl.mjs +63 -0
- package/dist/stim-footprint.swift +32 -0
- package/dist/{stim-installations-CINXy_su.mjs → stim-installations-QMEI32v1.mjs} +1 -1
- package/dist/stop-B0Yxtmiy.mjs +48 -0
- package/dist/{stop-BzreOWJp.mjs → stop-Bffozsac.mjs} +83 -16
- package/dist/{stop-BWIF_6BK.mjs → stop-Dt_dM16A.mjs} +2 -2
- package/dist/stop-cause-CidxIqup.mjs +97 -0
- package/dist/supervisor-run.d.mts +43 -11
- package/dist/supervisor-run.mjs +363 -24
- package/dist/support-CAP8qDUF.mjs +316 -0
- package/dist/support-D4aYJniO.mjs +201 -0
- package/dist/teardown-CL6vDs2Y.mjs +727 -0
- package/dist/{xcode-Cp5KvK9x.mjs → toolchain-DkUflduf.mjs} +364 -14
- package/dist/trigger-Bemu8ytw.mjs +65 -0
- package/dist/trigger-CXts6qt1.mjs +2 -0
- package/dist/warm-progress-CpGv3Wb6.mjs +732 -0
- package/dist/{web-BsQs1WlO.mjs → web-DWUaMp8P.mjs} +37 -21
- package/dist/web-run.mjs +310 -13
- package/dist/workspace-process-lock-BL7Q0HCO.mjs +57 -0
- package/dist/workspace-state-B8-aJ1_5.mjs +371 -0
- package/dist/workspaces-BV4pdHFz.mjs +306 -0
- package/dist/{worktree-UrQWXSeb.mjs → worktree-K-xj9Db0.mjs} +625 -256
- package/dist/worktree-jnG8Iph5.mjs +2 -0
- package/package.json +10 -5
- package/shim/bundle-response.cjs +28 -12
- package/dist/activity-CYVDcrt3.mjs +0 -356
- package/dist/budget-CGDkhvv_.mjs +0 -1745
- package/dist/build-progress-DxKpwnaS.mjs +0 -247
- package/dist/idle-DXtx3RbX.mjs +0 -191
- package/dist/in-use-DlqBDKqY.mjs +0 -249
- package/dist/logs-Civ4gbBH.mjs +0 -486
- package/dist/merge-state-ytFQCeh4.mjs +0 -262
- package/dist/server-expo-DIHDhvGg.mjs +0 -2
- package/dist/settings-D61iNOsB.mjs +0 -629
- package/dist/start-Cetfvy8l.mjs +0 -2
- package/dist/stats-CFpFGlJV.mjs +0 -62
- package/dist/stats-CX1Z8Tca.mjs +0 -368
- package/dist/stim-desktop-CXt8-Z3J.mjs +0 -40
- package/dist/stop-cause-hjGBOZTG.mjs +0 -1231
- package/dist/worktree-BVrHsYZG.mjs +0 -752
- package/dist/worktree-CHWWvm5v.mjs +0 -2
- package/dist/{metro-store-DzeWkfqW.mjs → metro-store-CDDZHtPl.mjs} +1 -1
|
@@ -1,30 +1,467 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { a as
|
|
6
|
-
import { a as
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { i as
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
1
|
+
import { t as getExecutor } from "./exec-CaErVv7t.mjs";
|
|
2
|
+
import { E as upsertProject, H as projectDeviceSlots, S as removeProject, _ as loadConfig$1, a as clearDevice, d as getConfigDir, g as isPathPrefix, k as acquireAvdClaim, p as getProject } from "./config-D0DbpZtM.mjs";
|
|
3
|
+
import { n as ownership_claim_exports, t as process_identity_exports } from "./process-identity-BmRm1qV0.mjs";
|
|
4
|
+
import { a as pullRequestLookup, r as endedPullRequest } from "./pull-request-pxnDCQzn.mjs";
|
|
5
|
+
import { a as formatElapsed, h as shortUdid, i as formatDuration, l as phaseLine, p as releasedLeaseFact, u as plural } from "./command-output-DnJMdEY-.mjs";
|
|
6
|
+
import { $ as cloneIgnoredEntries, A as readCommittedSettings, H as unknownSettingKeys, P as resolveSettings, Q as branchExists, R as settingShapeErrors, St as warmWorktreePaths, _t as resolveFullRef, a as SETTING_SHAPE_REMEDY, at as gitCommonDirOnDisk, bt as sourceCheckoutOf, ct as hasUncommittedWork, dt as listWorktrees, et as deleteBranch, ft as locallyKnownUpstream, ht as removeWorktree, it as gitCommonDir, lt as isPodInstallChurn, mt as readWorktreeExclude, ot as hasPopulatedSubmodules, pt as podsOutOfSync, rt as dirtyPaths, st as hasRemote, tt as depsOutOfSync, xt as unpushedCommits, yt as restoreFile } from "./settings-oTSkkNrM.mjs";
|
|
7
|
+
import { n as appProjectProblem, o as findProjectRoot$1 } from "./project-Du91Ilvc.mjs";
|
|
8
|
+
import { l as resolveProjectMetro, o as pidExists, r as killMetroTree, u as signalProcessTree } from "./metro-BeK1ZPz1.mjs";
|
|
9
|
+
import { g as workspaceAgentDeviceDir, i as recordWorkspaceUse, r as readWorkspaceState$1, v as workspaceDir } from "./workspace-state-B8-aJ1_5.mjs";
|
|
10
|
+
import { o as readWebRecord } from "./state-C1jB7Q8q.mjs";
|
|
11
|
+
import { a as machineNumber } from "./budget-DjrURfQn.mjs";
|
|
12
|
+
import { n as withIdleWorkspace, t as emptyWorkspaceDir } from "./in-use-DvBw0Kxo.mjs";
|
|
13
|
+
import { t as agentDeviceDaemon } from "./activity-BxwdD81S.mjs";
|
|
14
|
+
import { u as releaseWorkspaceLeases } from "./device-lease-D-hLqx1b.mjs";
|
|
15
|
+
import { a as clearRemoteSession, c as readMetroTunnel, d as readRemoteSessionId, i as clearManagedMetroTunnel, u as readRemoteSession } from "./state-DcW6vOd_.mjs";
|
|
16
|
+
import { ft as parkedMaxSetting, ot as POOL_SETTING_REMEDY } from "./android-B_NumVyQ.mjs";
|
|
17
|
+
import { a as workspaceLinks, i as workspaceLinkLine } from "./stim-desktop-Cnz8CPnl.mjs";
|
|
18
|
+
import { a as teardownOwnedBrowser, i as teardownOwnedAvd, o as teardownOwnedIosSim } from "./teardown-CL6vDs2Y.mjs";
|
|
19
|
+
import { t as clearNamedPorts } from "./named-ports-Bk_YXEmE.mjs";
|
|
20
|
+
import { i as stopTunnel, l as withManagedTunnelRemovalLock, s as withManagedRemoteWorktreeRemovalLock, t as resolveSupervisorTarget } from "./ownership-hCkPzGmr.mjs";
|
|
21
|
+
import { l as withdrawDevServerStopRequest, s as requestDevServerStop } from "./stop-cause-CidxIqup.mjs";
|
|
22
|
+
import { C as podInstallCommand, u as HEARTBEAT_INTERVAL_MS } from "./toolchain-DkUflduf.mjs";
|
|
23
|
+
import { n as verifyCollectorOwnership } from "./ownership-DiA16AsN.mjs";
|
|
24
|
+
import { t as stopMacosApp } from "./stop-B0Yxtmiy.mjs";
|
|
25
|
+
import { a as runPodInstall, i as runCaptured, n as podsAreStale, r as readPodState, t as DEPS_ERROR } from "./deps-urbmfXig.mjs";
|
|
26
|
+
import { f as resolveEasCliBin } from "./remote-cache-C8-V13XN.mjs";
|
|
27
|
+
import { i as endRecordedSession } from "./device-remote-D5WVWqOq.mjs";
|
|
28
|
+
import { n as hasInstalledDependencies, r as installedNpmTreeIsValid, t as dependencyState } from "./dependency-state-B6cMXZab.mjs";
|
|
29
|
+
import { a as forgetStatusMeasures, s as mergeState, t as startWarmProgress } from "./warm-progress-CpGv3Wb6.mjs";
|
|
30
|
+
import { r as workspaceRecordingEnabled } from "./recordings-BIXOzVnr.mjs";
|
|
31
|
+
import { r as stopHostedIos } from "./hosted-ios-CIWelIqf.mjs";
|
|
26
32
|
import { createHash, randomUUID } from "node:crypto";
|
|
33
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, utimesSync, writeFileSync } from "node:fs";
|
|
34
|
+
import { basename, dirname, join, relative } from "node:path";
|
|
35
|
+
import { existsSync as existsSync$1, realpathSync as realpathSync$1, rmSync as rmSync$1 } from "fs";
|
|
36
|
+
import { basename as basename$1, dirname as dirname$1, isAbsolute as isAbsolute$1, join as join$1, relative as relative$1, resolve as resolve$1 } from "path";
|
|
37
|
+
import { withDirLock, workspaceName } from "@stim-cli/core";
|
|
38
|
+
import { LAST_BUILD_KEYS, archiveDir, archiveEnabled, archiveLock, archiveRoot, archivedUsage, countErrorEntries, endedAgentSessionOf, listRecordedDevices, loadConfig, queryLogs, readArchives, readBuildHistory, readJsonObject, readLastBuilds, readMacosRecord, readPullRequestCache, readWorkspaceState, recordingEnabled, sortByTs } from "@stim-cli/core/state";
|
|
39
|
+
import { claimRemoveCommand, clearFreeClaimSet, readClaimSet, releaseClaim, tryAcquireClaim } from "@stim-cli/core/ownership-claim";
|
|
27
40
|
import chalk from "chalk";
|
|
41
|
+
//#region src/archive.ts
|
|
42
|
+
const DAY = 864e5;
|
|
43
|
+
const KINDS = [
|
|
44
|
+
"logs",
|
|
45
|
+
"recordings",
|
|
46
|
+
"agentActions"
|
|
47
|
+
];
|
|
48
|
+
const KIND_PATH = {
|
|
49
|
+
logs: "logs",
|
|
50
|
+
recordings: "recordings",
|
|
51
|
+
agentActions: "agent-device/sessions"
|
|
52
|
+
};
|
|
53
|
+
function archiveSettings() {
|
|
54
|
+
const config = loadConfig();
|
|
55
|
+
return Object.fromEntries([
|
|
56
|
+
"maxAgeDays",
|
|
57
|
+
"maxCount",
|
|
58
|
+
"maxTotalGb",
|
|
59
|
+
"logs.maxAgeDays",
|
|
60
|
+
"logs.maxMbPerWorkspace",
|
|
61
|
+
"recordings.maxAgeDays",
|
|
62
|
+
"recordings.maxTotalGb",
|
|
63
|
+
"agentActions.maxAgeDays"
|
|
64
|
+
].map((key) => {
|
|
65
|
+
const result = machineNumber(`archive.${key}`, config, process.env);
|
|
66
|
+
if (result.error) throw new Error(result.error);
|
|
67
|
+
return [key, result.value];
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
function enabled(root) {
|
|
71
|
+
const config = loadConfig();
|
|
72
|
+
const project = config?.projects?.[root];
|
|
73
|
+
const present = existsSync(root);
|
|
74
|
+
const common = present ? gitCommonDirOnDisk(root) : null;
|
|
75
|
+
if (!present && Object.values(config?.repos ?? {}).some((repo) => repo.settings?.archive?.enabled === false)) return false;
|
|
76
|
+
return archiveEnabled(process.env, [
|
|
77
|
+
project?.settings,
|
|
78
|
+
common ? config?.repos?.[common]?.settings : void 0,
|
|
79
|
+
present ? readCommittedSettings(root) : void 0,
|
|
80
|
+
config
|
|
81
|
+
]);
|
|
82
|
+
}
|
|
83
|
+
function files(dir) {
|
|
84
|
+
if (!existsSync(dir)) return [];
|
|
85
|
+
return readdirSync(dir).flatMap((name) => {
|
|
86
|
+
const path = join(dir, name);
|
|
87
|
+
const stat = lstatSync(path);
|
|
88
|
+
if (stat.isSymbolicLink()) throw new Error(`refusing symlink ${path}`);
|
|
89
|
+
return stat.isDirectory() ? files(path) : [{
|
|
90
|
+
path,
|
|
91
|
+
bytes: stat.size,
|
|
92
|
+
at: stat.mtimeMs
|
|
93
|
+
}];
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function secureTree(dir) {
|
|
97
|
+
if (lstatSync(dir).isSymbolicLink()) throw new Error(`refusing symlink ${dir}`);
|
|
98
|
+
chmodSync(dir, 448);
|
|
99
|
+
for (const name of readdirSync(dir)) {
|
|
100
|
+
const path = join(dir, name);
|
|
101
|
+
const stat = lstatSync(path);
|
|
102
|
+
if (stat.isSymbolicLink()) throw new Error(`refusing symlink ${path}`);
|
|
103
|
+
if (stat.isDirectory()) secureTree(path);
|
|
104
|
+
else chmodSync(path, 384);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function writeRecord(dir, record) {
|
|
108
|
+
const small = [
|
|
109
|
+
"state.json",
|
|
110
|
+
"ended-agents.json",
|
|
111
|
+
"log-error-index.json"
|
|
112
|
+
].reduce((sum, name) => {
|
|
113
|
+
const path = join(dir, name);
|
|
114
|
+
return sum + (existsSync(path) ? statSync(path).size : 0);
|
|
115
|
+
}, 0);
|
|
116
|
+
for (let pass = 0; pass < 8; pass++) {
|
|
117
|
+
record.bytes.total = KINDS.reduce((sum, kind) => sum + record.bytes[kind], record.bytes.record);
|
|
118
|
+
const bytes = small + Buffer.byteLength(`${JSON.stringify(record)}\n`);
|
|
119
|
+
if (bytes === record.bytes.record) break;
|
|
120
|
+
record.bytes.record = bytes;
|
|
121
|
+
}
|
|
122
|
+
record.bytes.total = KINDS.reduce((sum, kind) => sum + record.bytes[kind], record.bytes.record);
|
|
123
|
+
const tmp = join(dir, `.archive-${randomUUID()}.tmp`);
|
|
124
|
+
try {
|
|
125
|
+
writeFileSync(tmp, `${JSON.stringify(record)}\n`, { mode: 384 });
|
|
126
|
+
renameSync(tmp, join(dir, "archive.json"));
|
|
127
|
+
} finally {
|
|
128
|
+
rmSync(tmp, { force: true });
|
|
129
|
+
}
|
|
130
|
+
const now = /* @__PURE__ */ new Date();
|
|
131
|
+
utimesSync(archiveRoot(), now, now);
|
|
132
|
+
}
|
|
133
|
+
function removeRecord(id) {
|
|
134
|
+
const dir = archiveDir(id);
|
|
135
|
+
if (!existsSync(dir)) return;
|
|
136
|
+
const removing = join(archiveRoot(), `.removing-${id}`);
|
|
137
|
+
renameSync(dir, removing);
|
|
138
|
+
rmSync(removing, {
|
|
139
|
+
recursive: true,
|
|
140
|
+
force: true
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function sweepArchiveStaging(remove, waitMs = 0) {
|
|
144
|
+
if (!existsSync(archiveRoot())) return [];
|
|
145
|
+
return withDirLock(archiveLock(), () => readdirSync(archiveRoot()).filter((name) => name.startsWith(".incoming-") && (!name.endsWith(".claims") || !existsSync(join(archiveRoot(), name.slice(0, -7)))) || name.startsWith(".removing-")).flatMap((name) => {
|
|
146
|
+
const path = join(archiveRoot(), name.endsWith(".claims") ? name.slice(0, -7) : name);
|
|
147
|
+
const root = `${path}.claims`;
|
|
148
|
+
const survey = name.startsWith(".incoming-") ? readClaimSet(root) : null;
|
|
149
|
+
const unresolved = survey?.unresolved[0];
|
|
150
|
+
const kept = Boolean(survey && (survey.live.length || survey.unresolved.length));
|
|
151
|
+
const entry = {
|
|
152
|
+
path,
|
|
153
|
+
kept,
|
|
154
|
+
reason: unresolved?.reason ?? (kept ? "live archive writer" : null),
|
|
155
|
+
removeCommand: unresolved ? claimRemoveCommand(root) : null
|
|
156
|
+
};
|
|
157
|
+
if (remove && !kept) {
|
|
158
|
+
if (!survey || clearFreeClaimSet({
|
|
159
|
+
root,
|
|
160
|
+
label: "archive staging"
|
|
161
|
+
}).status === "cleared") {
|
|
162
|
+
rmSync(path, {
|
|
163
|
+
recursive: true,
|
|
164
|
+
force: true
|
|
165
|
+
});
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return [entry];
|
|
170
|
+
}), { waitMs });
|
|
171
|
+
}
|
|
172
|
+
function stripPids(value) {
|
|
173
|
+
if (Array.isArray(value)) return value.map(stripPids);
|
|
174
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).filter(([key]) => !/pids?$/i.test(key)).map(([key, child]) => [key, stripPids(child)]));
|
|
175
|
+
return value;
|
|
176
|
+
}
|
|
177
|
+
function gitFacts(root) {
|
|
178
|
+
const empty = {
|
|
179
|
+
repository: null,
|
|
180
|
+
branch: null,
|
|
181
|
+
head: null,
|
|
182
|
+
subject: null,
|
|
183
|
+
merged: null,
|
|
184
|
+
pullRequest: null
|
|
185
|
+
};
|
|
186
|
+
if (!existsSync(root)) return empty;
|
|
187
|
+
const exec = getExecutor();
|
|
188
|
+
const run = (args) => exec.runFileQuiet("git", [
|
|
189
|
+
"-C",
|
|
190
|
+
root,
|
|
191
|
+
...args
|
|
192
|
+
], { timeoutMs: 1e3 });
|
|
193
|
+
const common = gitCommonDirOnDisk(root);
|
|
194
|
+
const summary = run([
|
|
195
|
+
"show",
|
|
196
|
+
"-s",
|
|
197
|
+
"--format=%H%n%s",
|
|
198
|
+
"HEAD"
|
|
199
|
+
])?.split("\n");
|
|
200
|
+
const location = run([
|
|
201
|
+
"rev-parse",
|
|
202
|
+
"--show-toplevel",
|
|
203
|
+
"--abbrev-ref",
|
|
204
|
+
"HEAD"
|
|
205
|
+
])?.split("\n");
|
|
206
|
+
const branch = location?.[1] && location[1] !== "HEAD" ? location[1] : null;
|
|
207
|
+
const defaultRef = run([
|
|
208
|
+
"symbolic-ref",
|
|
209
|
+
"--quiet",
|
|
210
|
+
"refs/remotes/origin/HEAD"
|
|
211
|
+
]);
|
|
212
|
+
let merged = null;
|
|
213
|
+
if (defaultRef?.startsWith("refs/remotes/")) {
|
|
214
|
+
const state = mergeState(root, {
|
|
215
|
+
ref: defaultRef,
|
|
216
|
+
name: defaultRef.slice(13)
|
|
217
|
+
}, { timeoutMs: 2e3 });
|
|
218
|
+
merged = state.merged ? true : state.unknown ? null : false;
|
|
219
|
+
}
|
|
220
|
+
let prPath = root;
|
|
221
|
+
try {
|
|
222
|
+
if (location?.[0]) prPath = realpathSync(location[0]);
|
|
223
|
+
} catch {}
|
|
224
|
+
const pr = readPullRequestCache(prPath)?.pullRequest;
|
|
225
|
+
if (pr?.state === "merged") merged = true;
|
|
226
|
+
return {
|
|
227
|
+
repository: common ? basename(common) === ".git" ? dirname(common) : common : null,
|
|
228
|
+
branch,
|
|
229
|
+
head: summary?.[0] ?? null,
|
|
230
|
+
subject: summary?.[1] ?? null,
|
|
231
|
+
merged,
|
|
232
|
+
pullRequest: pr ? {
|
|
233
|
+
number: pr.number,
|
|
234
|
+
state: pr.state,
|
|
235
|
+
title: pr.title,
|
|
236
|
+
url: pr.url
|
|
237
|
+
} : null
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function archiveWorkspace(root, removedBy, state = readWorkspaceState(root)) {
|
|
241
|
+
writeArchive(root, removedBy, state);
|
|
242
|
+
try {
|
|
243
|
+
enforceArchiveRetention();
|
|
244
|
+
} catch (error) {
|
|
245
|
+
console.error(`could not enforce archive retention: ${String(error?.message ?? error).split("\n")[0]}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function writeArchive(root, removedBy, state) {
|
|
249
|
+
let staging = null;
|
|
250
|
+
let claim;
|
|
251
|
+
try {
|
|
252
|
+
const dir = workspaceDir(root);
|
|
253
|
+
if (!existsSync(dir) || lstatSync(dir).isSymbolicLink()) return;
|
|
254
|
+
if (existsSync(root)) root = realpathSync(root);
|
|
255
|
+
try {
|
|
256
|
+
sweepArchiveStaging(true, 5e3);
|
|
257
|
+
} catch {}
|
|
258
|
+
if (!enabled(root)) return;
|
|
259
|
+
const limits = archiveSettings();
|
|
260
|
+
if (!limits.maxAgeDays || !limits.maxCount) return;
|
|
261
|
+
const logs = join(dir, "logs");
|
|
262
|
+
if (!existsSync(join(dir, "state.json")) && !Object.values(readBuildHistory(state)).some((entries) => entries?.length) && (!existsSync(logs) || readdirSync(logs).length === 0)) return;
|
|
263
|
+
const worktree = gitFacts(root);
|
|
264
|
+
mkdirSync(archiveRoot(), {
|
|
265
|
+
recursive: true,
|
|
266
|
+
mode: 448
|
|
267
|
+
});
|
|
268
|
+
chmodSync(archiveRoot(), 448);
|
|
269
|
+
const token = randomUUID();
|
|
270
|
+
staging = join(archiveRoot(), `.incoming-${token}`);
|
|
271
|
+
const attempt = tryAcquireClaim({
|
|
272
|
+
root: `${staging}.claims`,
|
|
273
|
+
mode: "exclusive",
|
|
274
|
+
label: "archive staging"
|
|
275
|
+
});
|
|
276
|
+
claim = attempt.acquired;
|
|
277
|
+
if (!claim) {
|
|
278
|
+
releaseClaim(attempt.pending);
|
|
279
|
+
throw new Error("archive staging claim is held");
|
|
280
|
+
}
|
|
281
|
+
withDirLock(archiveLock(), () => {
|
|
282
|
+
mkdirSync(staging, { mode: 448 });
|
|
283
|
+
let at = Date.now();
|
|
284
|
+
const workspace = workspaceName(root);
|
|
285
|
+
while (existsSync(archiveDir(`${workspace}--${at}`))) at++;
|
|
286
|
+
const id = `${workspace}--${at}`;
|
|
287
|
+
if (state) {
|
|
288
|
+
const { supervisor: _supervisor, collectors: _collectors, warm: _warm, ...stable } = state;
|
|
289
|
+
writeFileSync(join(staging, "state.json"), `${JSON.stringify(stripPids(stable))}\n`, { mode: 384 });
|
|
290
|
+
}
|
|
291
|
+
for (const name of ["ended-agents.json", "log-error-index.json"]) {
|
|
292
|
+
const data = readJsonObject(join(dir, name));
|
|
293
|
+
if (data) writeFileSync(join(staging, name), `${JSON.stringify(stripPids(data))}\n`, { mode: 384 });
|
|
294
|
+
}
|
|
295
|
+
const move = (kind) => {
|
|
296
|
+
const from = join(dir, KIND_PATH[kind]);
|
|
297
|
+
if (!limits[`${kind}.maxAgeDays`] || !existsSync(from)) return;
|
|
298
|
+
const to = join(staging, KIND_PATH[kind]);
|
|
299
|
+
mkdirSync(join(to, ".."), {
|
|
300
|
+
recursive: true,
|
|
301
|
+
mode: 448
|
|
302
|
+
});
|
|
303
|
+
renameSync(from, to);
|
|
304
|
+
secureTree(to);
|
|
305
|
+
};
|
|
306
|
+
move("logs");
|
|
307
|
+
move("agentActions");
|
|
308
|
+
const config = loadConfig();
|
|
309
|
+
if (limits["recordings.maxAgeDays"] && workspaceRecordingEnabled(root, config?.projects?.[root] ?? {}, config, process.env)) {
|
|
310
|
+
const recordingDir = join(dir, "recordings");
|
|
311
|
+
if (existsSync(recordingDir) && lstatSync(recordingDir).isSymbolicLink()) throw new Error(`refusing symlink ${recordingDir}`);
|
|
312
|
+
for (const device of listRecordedDevices(recordingDir)) {
|
|
313
|
+
if (lstatSync(device.dir).isSymbolicLink()) throw new Error(`refusing symlink ${device.dir}`);
|
|
314
|
+
const to = join(staging, "recordings", `${device.platform}-${device.slot}`);
|
|
315
|
+
for (const segment of device.segments.filter((entry) => !entry.open)) {
|
|
316
|
+
mkdirSync(to, {
|
|
317
|
+
recursive: true,
|
|
318
|
+
mode: 448
|
|
319
|
+
});
|
|
320
|
+
try {
|
|
321
|
+
if (lstatSync(segment.file).isSymbolicLink()) throw new Error(`refusing symlink ${segment.file}`);
|
|
322
|
+
renameSync(segment.file, join(to, basename(segment.file)));
|
|
323
|
+
chmodSync(join(to, basename(segment.file)), 384);
|
|
324
|
+
} catch (error) {
|
|
325
|
+
if (error.code !== "ENOENT") throw error;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const index = readJsonObject(join(staging, "log-error-index.json"));
|
|
331
|
+
const indexed = Object.values(index?.files ?? {}).flatMap((file) => Object.values(file.markers ?? {}).concat(file.errors ?? []));
|
|
332
|
+
const lastErrorCount = countErrorEntries(sortByTs(queryLogs({
|
|
333
|
+
records: indexed,
|
|
334
|
+
errorsOnly: true
|
|
335
|
+
})));
|
|
336
|
+
const history = Object.values(readBuildHistory(state)).flat();
|
|
337
|
+
const last = Object.values(readLastBuilds(state)).toSorted((a, b) => Date.parse(b.startedAt) - Date.parse(a.startedAt))[0] ?? null;
|
|
338
|
+
const bytes = {
|
|
339
|
+
logs: 0,
|
|
340
|
+
recordings: 0,
|
|
341
|
+
agentActions: 0,
|
|
342
|
+
record: 0,
|
|
343
|
+
total: 0
|
|
344
|
+
};
|
|
345
|
+
const expires = {
|
|
346
|
+
logs: null,
|
|
347
|
+
recordings: null,
|
|
348
|
+
agentActions: null,
|
|
349
|
+
record: new Date(at + limits.maxAgeDays * DAY).toISOString()
|
|
350
|
+
};
|
|
351
|
+
for (const kind of KINDS) {
|
|
352
|
+
bytes[kind] = files(join(staging, KIND_PATH[kind])).reduce((sum, file) => sum + file.bytes, 0);
|
|
353
|
+
if (bytes[kind]) expires[kind] = new Date(at + limits[`${kind}.maxAgeDays`] * DAY).toISOString();
|
|
354
|
+
}
|
|
355
|
+
const record = {
|
|
356
|
+
id,
|
|
357
|
+
projectRoot: root,
|
|
358
|
+
project: basename(root),
|
|
359
|
+
workspace,
|
|
360
|
+
worktree,
|
|
361
|
+
removedAt: new Date(at).toISOString(),
|
|
362
|
+
removedBy,
|
|
363
|
+
lastUsedAt: typeof state?.lastUsedAt === "string" ? state.lastUsedAt : null,
|
|
364
|
+
builds: {
|
|
365
|
+
count: history.length,
|
|
366
|
+
last,
|
|
367
|
+
lastErrorCount
|
|
368
|
+
},
|
|
369
|
+
agents: (readJsonObject(join(dir, "ended-agents.json"))?.sessions ?? []).map(endedAgentSessionOf).filter((session) => session !== null),
|
|
370
|
+
bytes,
|
|
371
|
+
expires,
|
|
372
|
+
version: 1
|
|
373
|
+
};
|
|
374
|
+
writeRecord(staging, record);
|
|
375
|
+
renameSync(staging, archiveDir(id));
|
|
376
|
+
}, { waitMs: 3e4 });
|
|
377
|
+
releaseClaim(claim);
|
|
378
|
+
claim = void 0;
|
|
379
|
+
staging = null;
|
|
380
|
+
} catch (error) {
|
|
381
|
+
if (staging) try {
|
|
382
|
+
rmSync(staging, {
|
|
383
|
+
recursive: true,
|
|
384
|
+
force: true
|
|
385
|
+
});
|
|
386
|
+
} catch {}
|
|
387
|
+
console.error(`could not archive: ${String(error?.message ?? error).split("\n")[0]}`);
|
|
388
|
+
} finally {
|
|
389
|
+
releaseClaim(claim);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function removeKind(record, kind) {
|
|
393
|
+
rmSync(join(archiveDir(record.id), KIND_PATH[kind]), {
|
|
394
|
+
recursive: true,
|
|
395
|
+
force: true
|
|
396
|
+
});
|
|
397
|
+
record.bytes[kind] = 0;
|
|
398
|
+
record.expires[kind] = null;
|
|
399
|
+
writeRecord(archiveDir(record.id), record);
|
|
400
|
+
}
|
|
401
|
+
function logRemovalOrder(path) {
|
|
402
|
+
return /\.ndjson\.\d+$/.test(path) ? 0 : /^build-.*\.ndjson$/.test(basename(path)) ? 1 : 2;
|
|
403
|
+
}
|
|
404
|
+
function enforceArchiveRetention(now = Date.now()) {
|
|
405
|
+
if (!existsSync(archiveRoot())) return;
|
|
406
|
+
let limits;
|
|
407
|
+
try {
|
|
408
|
+
limits = archiveSettings();
|
|
409
|
+
} catch (error) {
|
|
410
|
+
console.error(`could not enforce archive retention: ${String(error.message).split("\n")[0]}`);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
withDirLock(archiveLock(), () => {
|
|
414
|
+
const listed = readArchives().toReversed();
|
|
415
|
+
const records = listed.filter((record, index) => limits.maxAgeDays > 0 && now - Date.parse(record.removedAt) < limits.maxAgeDays * DAY && index >= listed.length - limits.maxCount);
|
|
416
|
+
for (const record of listed) if (!records.includes(record)) removeRecord(record.id);
|
|
417
|
+
const recordings = recordingEnabled(process.env, [loadConfig()]);
|
|
418
|
+
for (const record of records) {
|
|
419
|
+
for (const kind of [
|
|
420
|
+
"recordings",
|
|
421
|
+
"agentActions",
|
|
422
|
+
"logs"
|
|
423
|
+
]) {
|
|
424
|
+
const age = limits[`${kind}.maxAgeDays`];
|
|
425
|
+
if (record.bytes[kind] && (!age || now - Date.parse(record.removedAt) >= age * DAY || kind === "recordings" && !recordings)) removeKind(record, kind);
|
|
426
|
+
}
|
|
427
|
+
const cap = limits["logs.maxMbPerWorkspace"] * 1024 ** 2;
|
|
428
|
+
if (record.bytes.logs > cap) {
|
|
429
|
+
const entries = files(join(archiveDir(record.id), "logs")).toSorted((a, b) => logRemovalOrder(a.path) - logRemovalOrder(b.path) || (logRemovalOrder(a.path) === 0 ? Number(b.path.split(".").at(-1)) - Number(a.path.split(".").at(-1)) : 0) || a.at - b.at);
|
|
430
|
+
for (const file of entries) {
|
|
431
|
+
if (record.bytes.logs <= cap) break;
|
|
432
|
+
rmSync(file.path);
|
|
433
|
+
record.bytes.logs = Math.max(0, record.bytes.logs - file.bytes);
|
|
434
|
+
}
|
|
435
|
+
if (!record.bytes.logs) record.expires.logs = null;
|
|
436
|
+
writeRecord(archiveDir(record.id), record);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const totalCap = limits.maxTotalGb * 1024 ** 3;
|
|
440
|
+
const recordingCap = limits["recordings.maxTotalGb"] * 1024 ** 3;
|
|
441
|
+
for (const record of records) for (;;) {
|
|
442
|
+
const usage = archivedUsage(records);
|
|
443
|
+
const totalOver = usage.bytes > totalCap;
|
|
444
|
+
const recordingsOver = usage.byKind.recordings > recordingCap;
|
|
445
|
+
if (!totalOver && !recordingsOver) break;
|
|
446
|
+
const candidates = KINDS.filter((kind) => record.bytes[kind] > 0 && (totalOver || kind === "recordings")).toSorted((a, b) => record.bytes[b] - record.bytes[a]);
|
|
447
|
+
if (!candidates.length) break;
|
|
448
|
+
removeKind(record, candidates[0]);
|
|
449
|
+
}
|
|
450
|
+
for (const record of records) {
|
|
451
|
+
const before = JSON.stringify(record.expires);
|
|
452
|
+
for (const kind of KINDS) record.expires[kind] = record.bytes[kind] ? new Date(Date.parse(record.removedAt) + limits[`${kind}.maxAgeDays`] * DAY).toISOString() : null;
|
|
453
|
+
record.expires.record = new Date(Date.parse(record.removedAt) + limits.maxAgeDays * DAY).toISOString();
|
|
454
|
+
if (JSON.stringify(record.expires) !== before) writeRecord(archiveDir(record.id), record);
|
|
455
|
+
}
|
|
456
|
+
}, { waitMs: 3e4 });
|
|
457
|
+
}
|
|
458
|
+
function deleteSelectedArchives(ids, kind) {
|
|
459
|
+
withDirLock(archiveLock(), () => {
|
|
460
|
+
for (const record of readArchives().filter((entry) => ids.includes(entry.id))) if (kind) removeKind(record, kind);
|
|
461
|
+
else removeRecord(record.id);
|
|
462
|
+
}, { waitMs: 3e4 });
|
|
463
|
+
}
|
|
464
|
+
//#endregion
|
|
28
465
|
//#region src/devices/reclaim.ts
|
|
29
466
|
async function reapCollectors(root, { verify = verifyCollectorOwnership, collectors }) {
|
|
30
467
|
const skippedDevices = [];
|
|
@@ -166,14 +603,22 @@ function describeDereferenced(project) {
|
|
|
166
603
|
}
|
|
167
604
|
return devices;
|
|
168
605
|
}
|
|
169
|
-
function
|
|
606
|
+
function lastBuildCacheKey(projectPath, platform) {
|
|
607
|
+
const build = readWorkspaceState$1(projectPath)?.[LAST_BUILD_KEYS[platform]];
|
|
608
|
+
if (build === null || typeof build !== "object" || Array.isArray(build)) return null;
|
|
609
|
+
const { cacheKey } = build;
|
|
610
|
+
return typeof cacheKey === "string" ? cacheKey : null;
|
|
611
|
+
}
|
|
612
|
+
function parkRequest(project, projectPath, slot, simslimManaged) {
|
|
170
613
|
const { max, error } = parkedMaxSetting("ios");
|
|
171
614
|
if (error || max <= 0) return void 0;
|
|
172
615
|
return {
|
|
173
616
|
projectPath,
|
|
174
617
|
max,
|
|
175
618
|
simslimManaged,
|
|
176
|
-
slot
|
|
619
|
+
slot,
|
|
620
|
+
appId: typeof project?.bundleId === "string" ? project.bundleId : null,
|
|
621
|
+
cacheKey: lastBuildCacheKey(projectPath, "ios")
|
|
177
622
|
};
|
|
178
623
|
}
|
|
179
624
|
function reclaimOwnedDevices(project, projectPath, { park = false } = {}) {
|
|
@@ -191,7 +636,7 @@ function reclaimOwnedDevices(project, projectPath, { park = false } = {}) {
|
|
|
191
636
|
const r = teardownOwnedIosSim(udid, {
|
|
192
637
|
del: true,
|
|
193
638
|
label,
|
|
194
|
-
...park ? { park: parkRequest(projectPath, slot, Boolean(ios.simslimManaged)) } : {}
|
|
639
|
+
...park ? { park: parkRequest(project, projectPath, slot, Boolean(ios.simslimManaged)) } : {}
|
|
195
640
|
});
|
|
196
641
|
if (r.parkFallback) poolNotes.push(`could not park ${label}: ${r.parkFallback} -- deleted it instead`);
|
|
197
642
|
for (const failure of r.evictionFailures ?? []) poolNotes.push(failure);
|
|
@@ -230,7 +675,9 @@ function reclaimOwnedDevices(project, projectPath, { park = false } = {}) {
|
|
|
230
675
|
projectPath,
|
|
231
676
|
slot,
|
|
232
677
|
max: bound.max,
|
|
233
|
-
configuration: typeof android.poolConfiguration === "string" ? android.poolConfiguration : void 0
|
|
678
|
+
configuration: typeof android.poolConfiguration === "string" ? android.poolConfiguration : void 0,
|
|
679
|
+
appId: typeof project?.androidPackage === "string" ? project.androidPackage : null,
|
|
680
|
+
cacheKey: lastBuildCacheKey(projectPath, "android")
|
|
234
681
|
} } : {}
|
|
235
682
|
});
|
|
236
683
|
if (r.parkFallback) poolNotes.push(`could not park ${android.avdName}: ${r.parkFallback} -- deleted it instead`);
|
|
@@ -270,6 +717,7 @@ async function reclaimProject(path, options = {}) {
|
|
|
270
717
|
const existed = existsSync(dir);
|
|
271
718
|
const run = await withIdleWorkspace(path, () => reclaimIdleProject(path, existed, options), {
|
|
272
719
|
purpose: "workspace removal",
|
|
720
|
+
hosted: false,
|
|
273
721
|
supervisor: false,
|
|
274
722
|
managedLocks: false
|
|
275
723
|
});
|
|
@@ -298,12 +746,12 @@ async function reclaimProject(path, options = {}) {
|
|
|
298
746
|
failedWorkspaceDirs: []
|
|
299
747
|
};
|
|
300
748
|
}
|
|
301
|
-
async function reclaimIdleProject(path, workspaceExisted, { deleteOwnedDevices = false, parkOwnedDevices = false, preserveProjectRecord = false, stopSession = defaultStopSession, stopMetroTunnel = defaultStopMetroTunnel, releaseLeases = releaseWorkspaceLeases, verifyCollector = verifyCollectorOwnership, teardownBrowser = teardownOwnedBrowser, stopRequester = { by: "stim gc --delete" } }) {
|
|
749
|
+
async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwnedDevices = false, parkOwnedDevices = false, preserveProjectRecord = false, stopSession = defaultStopSession, stopMetroTunnel = defaultStopMetroTunnel, releaseLeases = releaseWorkspaceLeases, verifyCollector = verifyCollectorOwnership, teardownBrowser = teardownOwnedBrowser, stopRequester = { by: "stim gc --delete" } }) {
|
|
302
750
|
await clearNamedPorts(path, { stop: true });
|
|
303
751
|
const project = getProject(path);
|
|
304
752
|
for (const { platforms } of projectDeviceSlots(project)) if (platforms.android?.owned && platforms.android.avdName) (0, ownership_claim_exports.releaseClaim)(acquireAvdClaim(platforms.android.avdName));
|
|
305
753
|
const dereferenced = describeDereferenced(project);
|
|
306
|
-
const initialState = readWorkspaceState(path);
|
|
754
|
+
const initialState = readWorkspaceState$1(path);
|
|
307
755
|
let killedPid = null;
|
|
308
756
|
let skippedMetro = null;
|
|
309
757
|
let supervisorHeld = false;
|
|
@@ -359,7 +807,7 @@ async function reclaimIdleProject(path, workspaceExisted, { deleteOwnedDevices =
|
|
|
359
807
|
failedCollectors.push(entry);
|
|
360
808
|
}
|
|
361
809
|
function retainReplacementProcesses() {
|
|
362
|
-
const currentState = readWorkspaceState(path);
|
|
810
|
+
const currentState = readWorkspaceState$1(path);
|
|
363
811
|
const currentProject = getProject(path);
|
|
364
812
|
if (Boolean(currentState?.supervisor && !(0, process_identity_exports.sameProcessRecord)(currentState.supervisor, initialState?.supervisor) || currentProject?.supervisor && !(0, process_identity_exports.sameProcessRecord)(currentProject.supervisor, project?.supervisor) || readWebRecord(path) !== null || Object.entries(currentState?.collectors ?? {}).some(([platform, record]) => !(0, process_identity_exports.sameProcessRecord)(record, initialState?.collectors?.[platform])))) {
|
|
365
813
|
supervisorHeld = true;
|
|
@@ -377,11 +825,33 @@ async function reclaimIdleProject(path, workspaceExisted, { deleteOwnedDevices =
|
|
|
377
825
|
};
|
|
378
826
|
skippedDevices.push(...skippedCollectors);
|
|
379
827
|
failedDevices.push(...failedCollectors);
|
|
828
|
+
try {
|
|
829
|
+
await stopHostedIos(path);
|
|
830
|
+
} catch (error) {
|
|
831
|
+
const failed = {
|
|
832
|
+
platform: "ios",
|
|
833
|
+
name: "hosted iOS simulator",
|
|
834
|
+
reason: error.message
|
|
835
|
+
};
|
|
836
|
+
failedDevices.push(failed);
|
|
837
|
+
skippedDevices.push(failed);
|
|
838
|
+
}
|
|
380
839
|
const remote = reclaimRemoteSession(path, { stopSession });
|
|
381
840
|
if (remote.failed) {
|
|
382
841
|
skippedDevices.push(remote.failed);
|
|
383
842
|
failedDevices.push(remote.failed);
|
|
384
843
|
}
|
|
844
|
+
const hosted = readMacosRecord(path)?.host;
|
|
845
|
+
if (hosted) try {
|
|
846
|
+
await stopMacosApp(path);
|
|
847
|
+
} catch (error) {
|
|
848
|
+
const failed = {
|
|
849
|
+
name: `hosted macOS session ${hosted.session} on ${hosted.machine}`,
|
|
850
|
+
reason: String(error?.message ?? error)
|
|
851
|
+
};
|
|
852
|
+
skippedDevices.push(failed);
|
|
853
|
+
failedDevices.push(failed);
|
|
854
|
+
}
|
|
385
855
|
const tunnel = await reclaimMetroTunnel(path, { stopMetroTunnel });
|
|
386
856
|
if (tunnel.failed) {
|
|
387
857
|
skippedDevices.push(tunnel.failed);
|
|
@@ -399,6 +869,28 @@ async function reclaimIdleProject(path, workspaceExisted, { deleteOwnedDevices =
|
|
|
399
869
|
if (failedDevices.length === 0 && !supervisorHeld) {
|
|
400
870
|
const dir = workspaceDir(path);
|
|
401
871
|
if (existsSync(dir)) try {
|
|
872
|
+
const stateDir = workspaceAgentDeviceDir(path);
|
|
873
|
+
if (!["none", "dead"].includes(agentDeviceDaemon(stateDir))) {
|
|
874
|
+
const exec = getExecutor();
|
|
875
|
+
try {
|
|
876
|
+
if (!exec.findExecutable("agent-device")) throw new Error("agent-device is not on PATH");
|
|
877
|
+
exec.runFile("agent-device", [
|
|
878
|
+
"daemon",
|
|
879
|
+
"stop",
|
|
880
|
+
"--state-dir",
|
|
881
|
+
stateDir,
|
|
882
|
+
"--clean"
|
|
883
|
+
], {
|
|
884
|
+
timeoutMs: 2e4,
|
|
885
|
+
killSignal: "SIGKILL",
|
|
886
|
+
env: { AGENT_DEVICE_NO_UPDATE_NOTIFIER: "1" }
|
|
887
|
+
});
|
|
888
|
+
} catch (error) {
|
|
889
|
+
process.stderr.write(`${phaseLine("agent-device", `daemon could not be stopped: ${error.message}; run agent-device daemon stop --state-dir "${stateDir}" --clean`)}\n`);
|
|
890
|
+
throw error;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
if (archive) archiveWorkspace(path, archive.removedBy, initialState);
|
|
402
894
|
emptyWorkspaceDir(dir);
|
|
403
895
|
if (workspaceExisted) removedWorkspaceDirs.push(dir);
|
|
404
896
|
} catch {
|
|
@@ -428,192 +920,6 @@ async function reclaimIdleProject(path, workspaceExisted, { deleteOwnedDevices =
|
|
|
428
920
|
};
|
|
429
921
|
}
|
|
430
922
|
//#endregion
|
|
431
|
-
//#region src/workspace/pull-request.ts
|
|
432
|
-
const GH_TIMEOUT_MS = 2e4;
|
|
433
|
-
const GH_ENV = {
|
|
434
|
-
GH_PROMPT_DISABLED: "1",
|
|
435
|
-
GH_NO_UPDATE_NOTIFIER: "1",
|
|
436
|
-
NO_COLOR: "1"
|
|
437
|
-
};
|
|
438
|
-
const GH_FIELDS = "number,state,url,headRefOid,mergedAt,closedAt,isCrossRepository";
|
|
439
|
-
const SIGNED_OUT_EXIT = 4;
|
|
440
|
-
function stateOf(state) {
|
|
441
|
-
if (state === "OPEN") return "open";
|
|
442
|
-
if (state === "MERGED") return "merged";
|
|
443
|
-
if (state === "CLOSED") return "closed";
|
|
444
|
-
return null;
|
|
445
|
-
}
|
|
446
|
-
function epoch(text) {
|
|
447
|
-
const at = text ? Date.parse(text) : NaN;
|
|
448
|
-
return Number.isFinite(at) ? at : null;
|
|
449
|
-
}
|
|
450
|
-
/**
|
|
451
|
-
* Picks the pull request that describes `head` among those `gh pr list --head <branch>` returned. One whose head is
|
|
452
|
-
* `head` wins, then one whose head contains `head`, then one whose head `head` contains. An open one wins a tier,
|
|
453
|
-
* then the newest. A pull request unrelated to `head` is from an earlier use of the branch name, and one from a fork
|
|
454
|
-
* only shares the branch name; both are ignored.
|
|
455
|
-
* `isAncestor(a, b)` answers whether commit `a` is an ancestor of commit `b`, and false when git cannot tell.
|
|
456
|
-
*/
|
|
457
|
-
function selectPullRequest(pulls, head, isAncestor) {
|
|
458
|
-
const known = pulls.flatMap((pull) => {
|
|
459
|
-
const state = stateOf(pull.state);
|
|
460
|
-
return state && pull.headRefOid && !pull.isCrossRepository ? [{
|
|
461
|
-
pull,
|
|
462
|
-
state
|
|
463
|
-
}] : [];
|
|
464
|
-
});
|
|
465
|
-
const tiers = [
|
|
466
|
-
known.filter(({ pull }) => pull.headRefOid === head),
|
|
467
|
-
known.filter(({ pull }) => pull.headRefOid !== head && isAncestor(head, pull.headRefOid)),
|
|
468
|
-
known.filter(({ pull }) => pull.headRefOid !== head && isAncestor(pull.headRefOid, head))
|
|
469
|
-
];
|
|
470
|
-
const tier = tiers.findIndex((entries) => entries.length > 0);
|
|
471
|
-
if (tier < 0) return null;
|
|
472
|
-
const { pull, state } = tiers[tier].toSorted((a, b) => Number(b.state === "open") - Number(a.state === "open") || b.pull.number - a.pull.number)[0];
|
|
473
|
-
return {
|
|
474
|
-
number: pull.number,
|
|
475
|
-
state,
|
|
476
|
-
url: pull.url,
|
|
477
|
-
head: pull.headRefOid,
|
|
478
|
-
containsHead: tier < 2,
|
|
479
|
-
endedAt: state === "merged" ? epoch(pull.mergedAt) : state === "closed" ? epoch(pull.closedAt) : null
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
function firstLine(error) {
|
|
483
|
-
const { stderr, message } = error;
|
|
484
|
-
const text = String(stderr ?? "").trim() || String(message ?? error);
|
|
485
|
-
return text.split("\n")[0] ?? text;
|
|
486
|
-
}
|
|
487
|
-
function ghFailure(error, command) {
|
|
488
|
-
if (error.status === SIGNED_OUT_EXIT) return {
|
|
489
|
-
unavailable: "gh is not signed in; run `gh auth login`",
|
|
490
|
-
sticky: true
|
|
491
|
-
};
|
|
492
|
-
if (error.code === "ETIMEDOUT") return {
|
|
493
|
-
unavailable: `${command} did not answer within ${GH_TIMEOUT_MS / 1e3}s`,
|
|
494
|
-
sticky: true
|
|
495
|
-
};
|
|
496
|
-
return {
|
|
497
|
-
unavailable: `${command} failed: ${firstLine(error)}`,
|
|
498
|
-
sticky: false
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
|
-
function ancestry(cwd) {
|
|
502
|
-
const exec = getExecutor();
|
|
503
|
-
return (ancestor, descendant) => exec.runFileQuiet("git", [
|
|
504
|
-
"-C",
|
|
505
|
-
cwd,
|
|
506
|
-
"merge-base",
|
|
507
|
-
"--is-ancestor",
|
|
508
|
-
ancestor,
|
|
509
|
-
descendant
|
|
510
|
-
]) !== null;
|
|
511
|
-
}
|
|
512
|
-
/**
|
|
513
|
-
* Asks GitHub, through `gh`, which pull requests have `branch` as their head, run from `cwd` so `gh` picks the
|
|
514
|
-
* repository from its remotes. Each call is one fixed `gh pr list` invocation. Once `gh` is missing, signed out or
|
|
515
|
-
* times out, every later lookup through the same function answers `unavailable` without running it again.
|
|
516
|
-
*/
|
|
517
|
-
function pullRequestLookup() {
|
|
518
|
-
let unavailable;
|
|
519
|
-
return (cwd, branch, head) => {
|
|
520
|
-
const exec = getExecutor();
|
|
521
|
-
if (unavailable === void 0) unavailable = exec.findExecutable("gh") ? null : "gh is not installed";
|
|
522
|
-
if (unavailable) return { unavailable };
|
|
523
|
-
let out;
|
|
524
|
-
try {
|
|
525
|
-
out = exec.runFile("gh", [
|
|
526
|
-
"pr",
|
|
527
|
-
"list",
|
|
528
|
-
"--head",
|
|
529
|
-
branch,
|
|
530
|
-
"--state",
|
|
531
|
-
"all",
|
|
532
|
-
"--limit",
|
|
533
|
-
"20",
|
|
534
|
-
"--json",
|
|
535
|
-
GH_FIELDS
|
|
536
|
-
], {
|
|
537
|
-
cwd,
|
|
538
|
-
timeoutMs: GH_TIMEOUT_MS,
|
|
539
|
-
env: GH_ENV
|
|
540
|
-
});
|
|
541
|
-
} catch (error) {
|
|
542
|
-
const failure = ghFailure(error, "gh pr list");
|
|
543
|
-
if (failure.sticky) unavailable = failure.unavailable;
|
|
544
|
-
return { unavailable: failure.unavailable };
|
|
545
|
-
}
|
|
546
|
-
let pulls;
|
|
547
|
-
try {
|
|
548
|
-
pulls = JSON.parse(out);
|
|
549
|
-
} catch {
|
|
550
|
-
return { unavailable: "gh pr list printed no JSON" };
|
|
551
|
-
}
|
|
552
|
-
return { pullRequest: Array.isArray(pulls) ? selectPullRequest(pulls, head, ancestry(cwd)) : null };
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
function branchesQuery(count) {
|
|
556
|
-
return `query($owner: String!, $repo: String!${Array.from({ length: count }, (_, i) => `, $b${i}: String!`).join("")}) { repository(owner: $owner, name: $repo) {${Array.from({ length: count }, (_, i) => ` b${i}: pullRequests(headRefName: $b${i}, states: [OPEN, CLOSED, MERGED], first: 20, orderBy: {field: CREATED_AT, direction: DESC}) { nodes { ${GH_FIELDS.replaceAll(",", " ")} } }`).join("")} } }`;
|
|
557
|
-
}
|
|
558
|
-
/**
|
|
559
|
-
* {@link pullRequestLookup} for every branch of one repository in one fixed `gh api graphql` call run from `repo`,
|
|
560
|
-
* which asks for the same 20 newest pull requests of each head branch that `gh pr list --head` returns. `gh` fills
|
|
561
|
-
* `{owner}` and `{repo}` from the remotes of `repo`; branch names travel as variables. Once `gh` is missing, signed
|
|
562
|
-
* out or times out, every later call through the same function answers `unavailable` without running it again.
|
|
563
|
-
*/
|
|
564
|
-
function pullRequestLookups() {
|
|
565
|
-
let unavailable;
|
|
566
|
-
return async (repo, queries) => {
|
|
567
|
-
const exec = getExecutor();
|
|
568
|
-
if (unavailable === void 0) unavailable = exec.findExecutable("gh") ? null : "gh is not installed";
|
|
569
|
-
const all = (lookup) => queries.map(() => lookup);
|
|
570
|
-
if (unavailable) return all({ unavailable });
|
|
571
|
-
if (!queries.length) return [];
|
|
572
|
-
const args = [
|
|
573
|
-
"api",
|
|
574
|
-
"graphql",
|
|
575
|
-
"-F",
|
|
576
|
-
"owner={owner}",
|
|
577
|
-
"-F",
|
|
578
|
-
"repo={repo}"
|
|
579
|
-
];
|
|
580
|
-
queries.forEach(({ branch }, i) => args.push("-f", `b${i}=${branch}`));
|
|
581
|
-
args.push("-f", `query=${branchesQuery(queries.length)}`);
|
|
582
|
-
let out;
|
|
583
|
-
try {
|
|
584
|
-
out = await exec.runFileAsync("gh", args, {
|
|
585
|
-
cwd: repo,
|
|
586
|
-
timeoutMs: GH_TIMEOUT_MS,
|
|
587
|
-
env: GH_ENV
|
|
588
|
-
});
|
|
589
|
-
} catch (error) {
|
|
590
|
-
const failure = ghFailure(error, "gh api graphql");
|
|
591
|
-
if (failure.sticky) unavailable = failure.unavailable;
|
|
592
|
-
return all({ unavailable: failure.unavailable });
|
|
593
|
-
}
|
|
594
|
-
let found;
|
|
595
|
-
try {
|
|
596
|
-
found = JSON.parse(out).data?.repository;
|
|
597
|
-
} catch {
|
|
598
|
-
return all({ unavailable: "gh api graphql printed no JSON" });
|
|
599
|
-
}
|
|
600
|
-
if (!found) return all({ unavailable: "gh api graphql printed no repository" });
|
|
601
|
-
return queries.map(({ cwd, head }, i) => {
|
|
602
|
-
const pulls = found[`b${i}`]?.nodes;
|
|
603
|
-
return { pullRequest: Array.isArray(pulls) ? selectPullRequest(pulls, head, ancestry(cwd)) : null };
|
|
604
|
-
});
|
|
605
|
-
};
|
|
606
|
-
}
|
|
607
|
-
/** The merged or closed pull request whose commits include HEAD, which makes the worktree removable. */
|
|
608
|
-
function endedPullRequest(lookup) {
|
|
609
|
-
const pr = lookup && "pullRequest" in lookup ? lookup.pullRequest : null;
|
|
610
|
-
return pr && pr.state !== "open" && pr.containsHead ? pr : null;
|
|
611
|
-
}
|
|
612
|
-
/** "PR #123 merged", for reports. */
|
|
613
|
-
function describePullRequest(pr) {
|
|
614
|
-
return `PR #${pr.number} ${pr.state}`;
|
|
615
|
-
}
|
|
616
|
-
//#endregion
|
|
617
923
|
//#region src/engine/warm-claim.ts
|
|
618
924
|
const WARM_CLAIM_LABEL = "worktree warm";
|
|
619
925
|
const WARM_CLAIM_POLL_MS = 250;
|
|
@@ -634,10 +940,10 @@ function defaultSleep(ms) {
|
|
|
634
940
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
635
941
|
}
|
|
636
942
|
function warmClaimsDir() {
|
|
637
|
-
return join
|
|
943
|
+
return join(getConfigDir(), "warm-locks");
|
|
638
944
|
}
|
|
639
945
|
function warmClaimPath(repositoryRoot) {
|
|
640
|
-
return join
|
|
946
|
+
return join(warmClaimsDir(), `${workspaceName(repositoryRoot)}.lock`);
|
|
641
947
|
}
|
|
642
948
|
function warmCommand(phase) {
|
|
643
949
|
return phase === "refresh" ? "stim worktree warm --refresh" : "stim worktree warm";
|
|
@@ -964,11 +1270,11 @@ function stepLine(label, reason, action) {
|
|
|
964
1270
|
return phaseLine(label, `${reason} -> ${action}`);
|
|
965
1271
|
}
|
|
966
1272
|
function installRecordFile(depsRoot) {
|
|
967
|
-
return join
|
|
1273
|
+
return join(getConfigDir(), "warm-installs", `${workspaceName(depsRoot)}.json`);
|
|
968
1274
|
}
|
|
969
1275
|
function lockfileHash(depsRoot, lockfile) {
|
|
970
1276
|
try {
|
|
971
|
-
return createHash("sha256").update(readFileSync(join
|
|
1277
|
+
return createHash("sha256").update(readFileSync(join(depsRoot, lockfile))).digest("hex");
|
|
972
1278
|
} catch {
|
|
973
1279
|
return null;
|
|
974
1280
|
}
|
|
@@ -990,7 +1296,7 @@ function readInstallEvidence(depsRoot) {
|
|
|
990
1296
|
function writeInstallEvidence(depsRoot, lockfile, hash, completed) {
|
|
991
1297
|
const file = installRecordFile(depsRoot);
|
|
992
1298
|
const tmp = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
993
|
-
mkdirSync(join
|
|
1299
|
+
mkdirSync(join(getConfigDir(), "warm-installs"), { recursive: true });
|
|
994
1300
|
writeFileSync(tmp, JSON.stringify({
|
|
995
1301
|
lock: lockfile,
|
|
996
1302
|
hash: hash ?? "",
|
|
@@ -1008,9 +1314,9 @@ function restoreInstallEvidence(depsRoot, prior) {
|
|
|
1008
1314
|
else rmSync(installRecordFile(depsRoot), { force: true });
|
|
1009
1315
|
}
|
|
1010
1316
|
function inProgressOperation(gitDir) {
|
|
1011
|
-
if (existsSync(join
|
|
1012
|
-
if (existsSync(join
|
|
1013
|
-
return existsSync(join
|
|
1317
|
+
if (existsSync(join(gitDir, "rebase-merge"))) return "rebase";
|
|
1318
|
+
if (existsSync(join(gitDir, "rebase-apply"))) return existsSync(join(gitDir, "rebase-apply", "applying")) ? "am" : "rebase";
|
|
1319
|
+
return existsSync(join(gitDir, "MERGE_HEAD")) ? "merge" : null;
|
|
1014
1320
|
}
|
|
1015
1321
|
function readMainCheckoutState(root) {
|
|
1016
1322
|
const exec = getExecutor();
|
|
@@ -1264,7 +1570,7 @@ async function refreshMainCheckout({ root, appDir, settings, emit, spawnFn = nul
|
|
|
1264
1570
|
const consumed = dependencies ? lockfileHash(dependencies.root, dependencies.lock) : null;
|
|
1265
1571
|
const deps = depsPlan({
|
|
1266
1572
|
lockfile: dependencies?.lock ?? null,
|
|
1267
|
-
lockfileChanged: dependencies ? pathChangedBetween(root, before, head, gitPath(relative
|
|
1573
|
+
lockfileChanged: dependencies ? pathChangedBetween(root, before, head, gitPath(relative(root, dependencies.root), dependencies.lock)) : false,
|
|
1268
1574
|
installed,
|
|
1269
1575
|
treeValid: dependencies?.lock === "package-lock.json" && installed ? installedNpmTreeIsValid(dependencies.root) : null,
|
|
1270
1576
|
lastInstall: priorInstall,
|
|
@@ -1296,11 +1602,11 @@ async function refreshMainCheckout({ root, appDir, settings, emit, spawnFn = nul
|
|
|
1296
1602
|
if (marked) restoreInstallEvidence(marked.root, priorInstall);
|
|
1297
1603
|
emit(stepLine("deps", depsSubject, "skipped"));
|
|
1298
1604
|
}
|
|
1299
|
-
const app = relative
|
|
1605
|
+
const app = relative(root, appDir) || ".";
|
|
1300
1606
|
const where = `source ${appDir}: `;
|
|
1301
1607
|
const podState = readPodState(appDir);
|
|
1302
1608
|
const pods = podsPlan({
|
|
1303
|
-
hasIos: existsSync(join
|
|
1609
|
+
hasIos: existsSync(join(appDir, "ios")),
|
|
1304
1610
|
hasPodfile: podState.hasPodfile,
|
|
1305
1611
|
podfileLockChanged: pathChangedBetween(root, before, head, gitPath(app, "ios", "Podfile.lock")),
|
|
1306
1612
|
stale: podsAreStale(podState.lockText, podState.manifestText)
|
|
@@ -1331,12 +1637,12 @@ async function refreshMainCheckout({ root, appDir, settings, emit, spawnFn = nul
|
|
|
1331
1637
|
//#endregion
|
|
1332
1638
|
//#region src/commands/worktree.ts
|
|
1333
1639
|
function dependencyInstallCommand(target, dir = ".") {
|
|
1334
|
-
const installRoot = resolve(target, dir);
|
|
1640
|
+
const installRoot = resolve$1(target, dir);
|
|
1335
1641
|
let command = "npm install";
|
|
1336
|
-
if (existsSync$1(resolve(installRoot, "pnpm-lock.yaml"))) command = "pnpm install";
|
|
1337
|
-
else if (existsSync$1(resolve(installRoot, "yarn.lock"))) command = "yarn install";
|
|
1338
|
-
else if (existsSync$1(resolve(installRoot, "bun.lock")) || existsSync$1(resolve(installRoot, "bun.lockb"))) command = "bun install";
|
|
1339
|
-
else if (existsSync$1(resolve(installRoot, "package-lock.json"))) command = "npm ci";
|
|
1642
|
+
if (existsSync$1(resolve$1(installRoot, "pnpm-lock.yaml"))) command = "pnpm install";
|
|
1643
|
+
else if (existsSync$1(resolve$1(installRoot, "yarn.lock"))) command = "yarn install";
|
|
1644
|
+
else if (existsSync$1(resolve$1(installRoot, "bun.lock")) || existsSync$1(resolve$1(installRoot, "bun.lockb"))) command = "bun install";
|
|
1645
|
+
else if (existsSync$1(resolve$1(installRoot, "package-lock.json"))) command = "npm ci";
|
|
1340
1646
|
return `cd '${installRoot.replaceAll("'", "'\\''")}' && ${command}`;
|
|
1341
1647
|
}
|
|
1342
1648
|
function reportCarriedStateHealth(root, target, copied) {
|
|
@@ -1354,7 +1660,7 @@ function reportCarriedStateHealth(root, target, copied) {
|
|
|
1354
1660
|
}
|
|
1355
1661
|
for (const p of podsOutOfSync(target, copied)) {
|
|
1356
1662
|
const where = p.dir === "." ? "Podfile.lock" : `${p.dir}/Podfile.lock`;
|
|
1357
|
-
const pod = podInstallCommand(p.dir === "." ? target : resolve(target, p.dir, ".."));
|
|
1663
|
+
const pod = podInstallCommand(p.dir === "." ? target : resolve$1(target, p.dir, ".."));
|
|
1358
1664
|
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.`)));
|
|
1359
1665
|
}
|
|
1360
1666
|
}
|
|
@@ -1366,18 +1672,51 @@ function reportRefreshFailure(failure) {
|
|
|
1366
1672
|
process.exitCode = 1;
|
|
1367
1673
|
}
|
|
1368
1674
|
function mainCheckoutAppDir(root, target) {
|
|
1369
|
-
const app = findProjectRoot(process.cwd());
|
|
1675
|
+
const app = findProjectRoot$1(process.cwd());
|
|
1370
1676
|
if (!app) return root;
|
|
1371
|
-
const rel = relative(target, app);
|
|
1372
|
-
if (!rel || rel.startsWith("..") || isAbsolute(rel)) return root;
|
|
1373
|
-
return resolve(root, rel);
|
|
1677
|
+
const rel = relative$1(target, app);
|
|
1678
|
+
if (!rel || rel.startsWith("..") || isAbsolute$1(rel)) return root;
|
|
1679
|
+
return resolve$1(root, rel);
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* The app workspace a warm prepares: the nearest app below the worktree root, or else the one app registered in
|
|
1683
|
+
* the source checkout, at the same place in this worktree, or else the worktree root when it is the app. A monorepo
|
|
1684
|
+
* root can declare react-native without being the app.
|
|
1685
|
+
*/
|
|
1686
|
+
function warmedWorkspace(root, target, cwd) {
|
|
1687
|
+
const nearest = findProjectRoot$1(cwd);
|
|
1688
|
+
const app = nearest && isPathPrefix(target, nearest) && appProjectProblem(nearest) === null ? nearest : null;
|
|
1689
|
+
if (app && app !== target) return app;
|
|
1690
|
+
const apps = /* @__PURE__ */ new Set();
|
|
1691
|
+
for (const path of Object.keys(loadConfig$1()?.projects ?? {})) {
|
|
1692
|
+
const rel = relative$1(root, path);
|
|
1693
|
+
if (rel.startsWith("..") || isAbsolute$1(rel)) continue;
|
|
1694
|
+
const mapped = resolve$1(target, rel);
|
|
1695
|
+
if (appProjectProblem(mapped) === null) apps.add(mapped);
|
|
1696
|
+
}
|
|
1697
|
+
return apps.size === 1 ? [...apps][0] : app;
|
|
1698
|
+
}
|
|
1699
|
+
function registerWarmedWorkspace(workspace) {
|
|
1700
|
+
try {
|
|
1701
|
+
if (!getProject(workspace)) upsertProject(workspace, {});
|
|
1702
|
+
} catch (error) {
|
|
1703
|
+
console.error(chalk.yellow(`Could not register ${workspace} for status: ${error?.message || String(error)}`));
|
|
1704
|
+
}
|
|
1374
1705
|
}
|
|
1375
1706
|
function registerWarm(worktree) {
|
|
1376
1707
|
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) => {
|
|
1708
|
+
let progress = null;
|
|
1709
|
+
let workspace = null;
|
|
1377
1710
|
try {
|
|
1378
1711
|
const { root, target, common } = warmWorktreePaths(process.cwd());
|
|
1379
|
-
const app = findProjectRoot(process.cwd());
|
|
1712
|
+
const app = findProjectRoot$1(process.cwd());
|
|
1380
1713
|
if (app) recordWorkspaceUse(app);
|
|
1714
|
+
workspace = warmedWorkspace(root, target, process.cwd());
|
|
1715
|
+
if (!workspace) console.error(chalk.dim(phaseLine("project", "no single app to register here; run warm from the app directory to list it in status while it warms")));
|
|
1716
|
+
else {
|
|
1717
|
+
registerWarmedWorkspace(workspace);
|
|
1718
|
+
progress = startWarmProgress(workspace, opts.refresh ? "refresh" : "copy", { note: (line) => console.error(chalk.yellow(line)) });
|
|
1719
|
+
}
|
|
1381
1720
|
const readSettings = () => {
|
|
1382
1721
|
const settings = resolveSettings({
|
|
1383
1722
|
gitCommonDir: common,
|
|
@@ -1393,6 +1732,7 @@ function registerWarm(worktree) {
|
|
|
1393
1732
|
return settings;
|
|
1394
1733
|
};
|
|
1395
1734
|
const copy = (wait) => {
|
|
1735
|
+
progress?.step("copy");
|
|
1396
1736
|
if (wait?.holder) console.error(warmClaimAcquiredLine(wait));
|
|
1397
1737
|
const incomplete = incompleteInstallRefusal(root, mainCheckoutAppDir(root, target));
|
|
1398
1738
|
if (incomplete) {
|
|
@@ -1490,6 +1830,10 @@ function registerWarm(worktree) {
|
|
|
1490
1830
|
if (claim) console.error(chalk.dim(claim.remedy));
|
|
1491
1831
|
if (typeof code === "string" && code.startsWith("STIM_")) console.error(chalk.red(`failed: ${code}`));
|
|
1492
1832
|
process.exitCode = 1;
|
|
1833
|
+
} finally {
|
|
1834
|
+
progress?.finish(!process.exitCode);
|
|
1835
|
+
const links = workspace && !process.exitCode ? workspaceLinks(workspace) : void 0;
|
|
1836
|
+
if (links) console.error(chalk.dim(workspaceLinkLine(links)));
|
|
1493
1837
|
}
|
|
1494
1838
|
});
|
|
1495
1839
|
}
|
|
@@ -1504,6 +1848,20 @@ function porcelainPath(line) {
|
|
|
1504
1848
|
}
|
|
1505
1849
|
const SAFE_DIFF_PATH = /^[A-Za-z0-9._/-]+$/;
|
|
1506
1850
|
const POD_CHURN_PATH = /(?:^|\/)ios\/(?:Podfile\.lock|[^/]+\.xcodeproj\/project\.pbxproj)$/;
|
|
1851
|
+
const WATCHMAN_COOKIE_PATH = /(?:^|\/)\.watchman-cookie-[A-Za-z0-9._-]+$/;
|
|
1852
|
+
function excludeWatchmanCookies(lines) {
|
|
1853
|
+
const kept = [];
|
|
1854
|
+
const cookies = [];
|
|
1855
|
+
for (const line of lines || []) {
|
|
1856
|
+
const path = String(line).startsWith("?? ") ? porcelainPath(line) : null;
|
|
1857
|
+
if (path && SAFE_DIFF_PATH.test(path) && WATCHMAN_COOKIE_PATH.test(path)) cookies.push(path);
|
|
1858
|
+
else kept.push(line);
|
|
1859
|
+
}
|
|
1860
|
+
return {
|
|
1861
|
+
lines: kept,
|
|
1862
|
+
cookies
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1507
1865
|
function excludePodChurn(lines) {
|
|
1508
1866
|
const kept = [];
|
|
1509
1867
|
const restore = [];
|
|
@@ -1526,11 +1884,11 @@ function excludePodChurn(lines) {
|
|
|
1526
1884
|
}
|
|
1527
1885
|
function nativeCanonicalPath(path) {
|
|
1528
1886
|
const missing = [];
|
|
1529
|
-
for (let existing = resolve(path);; existing = dirname(existing)) try {
|
|
1530
|
-
return resolve(realpathSync$1.native(existing), ...missing);
|
|
1887
|
+
for (let existing = resolve$1(path);; existing = dirname$1(existing)) try {
|
|
1888
|
+
return resolve$1(realpathSync$1.native(existing), ...missing);
|
|
1531
1889
|
} catch {
|
|
1532
|
-
if (dirname(existing) === existing) return resolve(path);
|
|
1533
|
-
missing.unshift(basename(existing));
|
|
1890
|
+
if (dirname$1(existing) === existing) return resolve$1(path);
|
|
1891
|
+
missing.unshift(basename$1(existing));
|
|
1534
1892
|
}
|
|
1535
1893
|
}
|
|
1536
1894
|
function matchWorktreeEntry(entries, path) {
|
|
@@ -1584,7 +1942,7 @@ function describeKeptDevice(s) {
|
|
|
1584
1942
|
return s.udid && s.udid !== s.name ? `${s.name} (${s.udid})` : s.name;
|
|
1585
1943
|
}
|
|
1586
1944
|
function reclaimKeys(rootPath) {
|
|
1587
|
-
const cfg = loadConfig();
|
|
1945
|
+
const cfg = loadConfig$1();
|
|
1588
1946
|
const keys = /* @__PURE__ */ new Set([rootPath]);
|
|
1589
1947
|
const root = nativeCanonicalPath(rootPath);
|
|
1590
1948
|
if (cfg?.projects) {
|
|
@@ -1597,7 +1955,7 @@ async function withReclaimLocks(rootPath, fn) {
|
|
|
1597
1955
|
const acquire = (index) => index === keys.length ? fn(keys) : withManagedTunnelRemovalLock(keys[index], () => acquire(index + 1));
|
|
1598
1956
|
return acquire(0);
|
|
1599
1957
|
}
|
|
1600
|
-
async function reclaimAll(rootPath, keys = reclaimKeys(rootPath), { preserveRootProject = false } = {}) {
|
|
1958
|
+
async function reclaimAll(rootPath, keys = reclaimKeys(rootPath), { preserveRootProject = false, removedBy } = {}) {
|
|
1601
1959
|
const dereferenced = [];
|
|
1602
1960
|
const killedPids = [];
|
|
1603
1961
|
const deletedDevices = [];
|
|
@@ -1616,6 +1974,7 @@ async function reclaimAll(rootPath, keys = reclaimKeys(rootPath), { preserveRoot
|
|
|
1616
1974
|
let r;
|
|
1617
1975
|
try {
|
|
1618
1976
|
r = await reclaimProject(key, {
|
|
1977
|
+
archive: removedBy ? { removedBy } : void 0,
|
|
1619
1978
|
deleteOwnedDevices: true,
|
|
1620
1979
|
parkOwnedDevices: true,
|
|
1621
1980
|
preserveProjectRecord: preserveRootProject && key === rootPath,
|
|
@@ -1716,7 +2075,7 @@ function reportRetainedResources(root, result) {
|
|
|
1716
2075
|
process.exitCode = 1;
|
|
1717
2076
|
}
|
|
1718
2077
|
function hasRegisteredProjectUnder(rootPath) {
|
|
1719
|
-
const cfg = loadConfig();
|
|
2078
|
+
const cfg = loadConfig$1();
|
|
1720
2079
|
return Object.keys(cfg?.projects ?? {}).some((key) => isPathPrefix(rootPath, key));
|
|
1721
2080
|
}
|
|
1722
2081
|
async function reclaimEnvironment(root, why) {
|
|
@@ -1743,20 +2102,21 @@ function removalPath(target) {
|
|
|
1743
2102
|
return canonicalExistingPath(target ?? process.cwd());
|
|
1744
2103
|
}
|
|
1745
2104
|
function canonicalExistingPath(target) {
|
|
1746
|
-
const resolved = resolve(target);
|
|
2105
|
+
const resolved = resolve$1(target);
|
|
1747
2106
|
let existing = resolved;
|
|
1748
2107
|
const missing = [];
|
|
1749
2108
|
while (!existsSync$1(existing)) {
|
|
1750
|
-
const parent = dirname(existing);
|
|
2109
|
+
const parent = dirname$1(existing);
|
|
1751
2110
|
if (parent === existing) return resolved;
|
|
1752
|
-
missing.unshift(basename(existing));
|
|
2111
|
+
missing.unshift(basename$1(existing));
|
|
1753
2112
|
existing = parent;
|
|
1754
2113
|
}
|
|
1755
|
-
return resolve(realpathSync$1(existing), ...missing);
|
|
2114
|
+
return resolve$1(realpathSync$1(existing), ...missing);
|
|
1756
2115
|
}
|
|
1757
2116
|
function inspectRemoval(path, mergedHead) {
|
|
1758
2117
|
const gitAnswered = hasUncommittedWork(path);
|
|
1759
|
-
const { lines:
|
|
2118
|
+
const { lines: withoutCookies, cookies } = excludeWatchmanCookies(gitAnswered ? dirtyPaths(path, { limit: Infinity }) : []);
|
|
2119
|
+
const { lines: dirtyLines, restore: podChurn } = excludePodChurn(withoutCookies);
|
|
1760
2120
|
const dirty = gitAnswered === null ? null : dirtyLines.length > 0;
|
|
1761
2121
|
const unpushed = unpushedCommits(path);
|
|
1762
2122
|
const blockers = removalBlockers({
|
|
@@ -1767,6 +2127,7 @@ function inspectRemoval(path, mergedHead) {
|
|
|
1767
2127
|
return {
|
|
1768
2128
|
dirtyLines,
|
|
1769
2129
|
podChurn,
|
|
2130
|
+
cookies,
|
|
1770
2131
|
unpushed,
|
|
1771
2132
|
blockers
|
|
1772
2133
|
};
|
|
@@ -1785,6 +2146,9 @@ function printRemovalRefusal(path, inspection) {
|
|
|
1785
2146
|
console.error(chalk.dim("work stays on the branch."));
|
|
1786
2147
|
process.exitCode = 1;
|
|
1787
2148
|
}
|
|
2149
|
+
function deleteWatchmanCookies(path, cookies) {
|
|
2150
|
+
for (const cookie of cookies) rmSync$1(join$1(path, cookie), { force: true });
|
|
2151
|
+
}
|
|
1788
2152
|
function restorePodChurn(path, files) {
|
|
1789
2153
|
for (const file of files) if (restoreFile(path, file)) console.error(chalk.dim(` restored ${file} (pod install churn; the worktree is being removed)`));
|
|
1790
2154
|
else console.error(chalk.yellow(` could not restore ${file}; git may refuse to remove the worktree`));
|
|
@@ -1832,7 +2196,7 @@ async function runRemove(target, opts, onRemoved) {
|
|
|
1832
2196
|
console.error(chalk.dim(phaseLine("branch", `${branch} is already absent; cleared its pending cleanup record`)));
|
|
1833
2197
|
return;
|
|
1834
2198
|
}
|
|
1835
|
-
const checkedOutAt = listWorktrees(mainRoot).find((candidate) => candidate.branch === branch && resolve(candidate.path) !== resolve(path))?.path;
|
|
2199
|
+
const checkedOutAt = listWorktrees(mainRoot).find((candidate) => candidate.branch === branch && resolve$1(candidate.path) !== resolve$1(path))?.path;
|
|
1836
2200
|
if (checkedOutAt) {
|
|
1837
2201
|
console.error(chalk.red(`Refusing pending cleanup for ${branch}: it is checked out at ${checkedOutAt}.`));
|
|
1838
2202
|
console.error(chalk.dim("Switch that worktree to another branch, then retry this command."));
|
|
@@ -1935,7 +2299,10 @@ async function runRemove(target, opts, onRemoved) {
|
|
|
1935
2299
|
printRemovalRefusal(path, current);
|
|
1936
2300
|
return;
|
|
1937
2301
|
}
|
|
1938
|
-
const result = await reclaimAll(path, lockedKeys, {
|
|
2302
|
+
const result = await reclaimAll(path, lockedKeys, {
|
|
2303
|
+
preserveRootProject: true,
|
|
2304
|
+
removedBy: opts.removedBy ?? "worktree-remove"
|
|
2305
|
+
});
|
|
1939
2306
|
if (result.keptEntries.length) {
|
|
1940
2307
|
reportRetainedResources(path, result);
|
|
1941
2308
|
return;
|
|
@@ -1948,6 +2315,7 @@ async function runRemove(target, opts, onRemoved) {
|
|
|
1948
2315
|
return;
|
|
1949
2316
|
}
|
|
1950
2317
|
restorePodChurn(path, current.podChurn);
|
|
2318
|
+
deleteWatchmanCookies(path, current.cookies);
|
|
1951
2319
|
try {
|
|
1952
2320
|
removeWorktree(path, {
|
|
1953
2321
|
from: source.path,
|
|
@@ -1963,6 +2331,7 @@ async function runRemove(target, opts, onRemoved) {
|
|
|
1963
2331
|
return;
|
|
1964
2332
|
}
|
|
1965
2333
|
onRemoved();
|
|
2334
|
+
forgetStatusMeasures(path, result.reclaimedKeys);
|
|
1966
2335
|
const finish = () => {
|
|
1967
2336
|
printRemovalCleanup(result, false);
|
|
1968
2337
|
console.error(chalk.dim(phaseLine("removed", path)));
|
|
@@ -1978,7 +2347,7 @@ async function runRemove(target, opts, onRemoved) {
|
|
|
1978
2347
|
worktreeRemovalComplete: true,
|
|
1979
2348
|
worktreePendingBranchSha: approvedBranchSha
|
|
1980
2349
|
});
|
|
1981
|
-
const checkedOutAt = listWorktrees(branchDeleteCwd).find((candidate) => candidate.branch === branch && resolve(candidate.path) !== resolve(path))?.path;
|
|
2350
|
+
const checkedOutAt = listWorktrees(branchDeleteCwd).find((candidate) => candidate.branch === branch && resolve$1(candidate.path) !== resolve$1(path))?.path;
|
|
1982
2351
|
if (checkedOutAt) {
|
|
1983
2352
|
console.error(chalk.yellow(phaseLine("branch", `kept ${branch} (it is checked out at ${checkedOutAt})`)));
|
|
1984
2353
|
console.error(chalk.dim(` Switch that worktree to another branch, then retry: stim worktree remove ${path}`));
|
|
@@ -2012,4 +2381,4 @@ function worktreeCommand(program) {
|
|
|
2012
2381
|
registerRemove(worktree);
|
|
2013
2382
|
}
|
|
2014
2383
|
//#endregion
|
|
2015
|
-
export {
|
|
2384
|
+
export { deleteSelectedArchives as _, porcelainPath as a, registerWarm as c, removalRemedy as d, removeWorktreeTarget as f, reclaimProject as g, describeDereferenced as h, matchWorktreeEntry as i, removalBlockers as l, worktreeCommand as m, excludePodChurn as n, reclaimKeys as o, warmedWorkspace as p, excludeWatchmanCookies as r, registerRemove as s, dependencyInstallCommand as t, removalPath as u, sweepArchiveStaging as v };
|