stim 1.15.0 → 1.17.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/dist/{activity-B2-kb7k0.mjs → activity-IRvJY_ru.mjs} +49 -16
- package/dist/agent-device-usage-output-BK41vnzN.mjs +411 -0
- package/dist/{android-Cu_L4RpL.mjs → android--GorydsK.mjs} +692 -321
- package/dist/{android-Db2eEMaf.mjs → android-B_YNZy61.mjs} +25 -60
- package/dist/{android-CYmq0jNi.mjs → android-DE4v59Mb.mjs} +12 -3
- package/dist/{android-cas-PnKfAdB2.mjs → android-cas-ZFpjJBL_.mjs} +6 -5
- package/dist/{app-install-CAAplXAt.mjs → app-install-VPkI5WEg.mjs} +52 -8
- package/dist/attempt-Bk2ouQVL.mjs +39 -0
- package/dist/{budget-CMiqghig.mjs → budget-DM984mnd.mjs} +372 -366
- package/dist/{build-plan-aeGMFX7e.mjs → build-plan-CmQCoyT-.mjs} +346 -88
- package/dist/{build-slots-Df1ApSuH.mjs → build-slots-41v_d2x9.mjs} +36 -21
- package/dist/{cli-0cX5s4NM.mjs → cli-Daxug5-K.mjs} +23 -17
- package/dist/cli.mjs +1 -1
- package/dist/collector-run.mjs +5 -192
- package/dist/{command-output-CliHHn7a.mjs → command-output-CDAIFD-8.mjs} +3 -3
- package/dist/{created-devices-BwVb3liH.mjs → created-devices-CpvzBL25.mjs} +1 -1
- package/dist/{dependency-state-BmsjKZ2H.mjs → dependency-state-CLcBHH2M.mjs} +1 -1
- package/dist/{deps-BFlKbOIp.mjs → deps-DNWaQs4V.mjs} +2 -2
- package/dist/{dev-client-CjzLoNs0.mjs → dev-client-B-XC4uRH.mjs} +3 -40
- package/dist/{device-BZ1IyS5A.mjs → device-C8IOoJPv.mjs} +7 -6
- package/dist/device-capacity-D0GWHnTM.mjs +580 -0
- package/dist/device-host-worker.mjs +587 -110
- package/dist/{device-ios-DMQBUPus.mjs → device-ios-Bc7vbGwl.mjs} +75 -44
- package/dist/{device-lease-W5Pi1q38.mjs → device-lease-DpoS36_u.mjs} +2 -2
- package/dist/{device-pool-B2r38YjS.mjs → device-lease-run-B8YltIH_.mjs} +5 -125
- package/dist/device-pool-3o7s5lo4.mjs +125 -0
- package/dist/{device-remote-9ulPsdhY.mjs → device-remote-hFSkpfQu.mjs} +13 -13
- package/dist/{doctor-BcE5xsBn.mjs → doctor-Cnk9d7Hm.mjs} +86 -59
- package/dist/{error-diagnostics-Cb0P1WLS.mjs → error-diagnostics-C-NMnhrk.mjs} +11 -15
- package/dist/{gc-BuL1Genr.mjs → gc-DxCL2A46.mjs} +478 -33
- package/dist/{gradle-CmdOBq1d.mjs → gradle-D7-L2Th3.mjs} +4 -4
- package/dist/{guide-cCSGRFd3.mjs → guide-DjrTQGik.mjs} +2198 -566
- package/dist/{guide-status-CIoWK6mG.mjs → guide-status-Du-ZQ3FD.mjs} +1 -1
- package/dist/hosted-android-DwLS4Mse.mjs +32 -0
- package/dist/hosted-client-wk1W_CNW.mjs +196 -0
- package/dist/hosted-ios-aMo7Cljf.mjs +15 -0
- package/dist/hosted-logs-DqYmWtXc.mjs +187 -0
- package/dist/hosted-macos-dlHXEeF5.mjs +135 -0
- package/dist/hosted-native-C9xJ9rpA.mjs +255 -0
- package/dist/{build-progress-C2Jv1AtA.mjs → idle-B2m-Ovod.mjs} +300 -9
- package/dist/{idle-shutdown-B5LPaNyY.mjs → idle-shutdown-_3oOV1V_.mjs} +25 -17
- package/dist/{in-use-CndPMilN.mjs → in-use-CvGOUn-x.mjs} +30 -10
- package/dist/ios-Cr1h5gnO.mjs +189 -0
- package/dist/{ios-Chob8ny7.mjs → ios-DE6duQoz.mjs} +819 -460
- package/dist/{ios-3CtCRhwr.mjs → ios-DSfnPfse.mjs} +4 -4
- package/dist/{ios-device-DERmMmk-.mjs → ios-device-pAN4t54-.mjs} +1 -1
- package/dist/ios-state-8L3lgum6.mjs +89 -0
- package/dist/{launch-verify-CLlofxRt.mjs → launch-verify-lTnWZikR.mjs} +3 -3
- package/dist/logs-DUoKlDoS.mjs +384 -0
- package/dist/{client-C2IS36Qf.mjs → machines-C2XiNwh9.mjs} +330 -37
- package/dist/{macos-BwtbUtTN.mjs → macos-32po3kUq.mjs} +23 -18
- package/dist/macos-run.mjs +1 -1
- package/dist/maintenance-run.d.mts +4 -0
- package/dist/maintenance-run.mjs +254 -0
- package/dist/{metro-Di8EN2Tb.mjs → metro-g8NqiJp-.mjs} +1 -1
- package/dist/metro-gateway-DmslUM30.mjs +103 -0
- package/dist/{named-ports-Dj_XBNaL.mjs → named-ports-C8m8EQs2.mjs} +2 -2
- package/dist/{native-run-DFDoIRbY.mjs → native-run-D1Q56rAD.mjs} +17 -6
- package/dist/{native-runtime-i6OkVg-w.mjs → native-runtime-DhJI3IVQ.mjs} +6 -6
- package/dist/{ndjson-CDY9IkVC.mjs → ndjson-PclOIwVY.mjs} +2 -2
- package/dist/offload-worker.d.mts +1 -0
- package/dist/offload-worker.mjs +13 -8
- package/dist/{teardown-BYuQafiA.mjs → ownership-BMLqyBgJ.mjs} +639 -53
- package/dist/{ownership-BkYxUtuj.mjs → ownership-DyRQru0q.mjs} +2 -2
- package/dist/{ports-DddwttKM.mjs → ports-4nYqxPWC.mjs} +2 -2
- package/dist/{prebuild-BvxDlD6C.mjs → prebuild-D7RUMkwN.mjs} +4 -4
- package/dist/preview-CxPRIEht.mjs +318 -0
- package/dist/{project-CV3tDFHl.mjs → project-D47Rh0ly.mjs} +72 -3
- package/dist/{recordings-GEXp4iBT.mjs → recordings-BNgsD1Tr.mjs} +1 -1
- package/dist/{reload-ChPiwvot.mjs → reload-qZei6iWn.mjs} +77 -13
- package/dist/{remote-cache-CO2elm_b.mjs → remote-cache-Ci4DQYYj.mjs} +4 -4
- package/dist/{run-B-xqRrsg.mjs → run-CkE9y0nc.mjs} +2 -2
- package/dist/{server-bare-X7PUgizL.mjs → server-bare-BkrSxSsZ.mjs} +1 -1
- package/dist/{server-expo-DhtX6bom.mjs → server-expo-CHTyXD0G.mjs} +4 -4
- package/dist/server-expo-CeakhI8P.mjs +2 -0
- package/dist/settings-BBpMcDbD.mjs +25 -0
- package/dist/settings-Cfd-0r2d.mjs +1440 -0
- package/dist/{settings-DbL6uhH8.mjs → settings-SR9OmW9h.mjs} +10 -4
- package/dist/settings.schema.json +472 -50
- package/dist/{simslim-Ciluxlq4.mjs → simslim-BWNrc3M3.mjs} +3 -3
- package/dist/{slot-launch-BY0BrOLh.mjs → slot-launch-DmjBmpQV.mjs} +10 -7
- package/dist/{spawn-entry-DncpJvWL.mjs → spawn-entry-jw19m8Nl.mjs} +1 -0
- package/dist/{start-CPO4wRg7.mjs → start-BopZcr4n.mjs} +1 -1
- package/dist/{start-CUD9nasA.mjs → start-uee1wWjY.mjs} +21 -17
- package/dist/{state-CaeaHbDA.mjs → state-BKa8wNN5.mjs} +1 -1
- package/dist/{state-BxCgeTQu.mjs → state-DyaNTgfR.mjs} +2 -2
- package/dist/{state-DFm77wRn.mjs → state-PeQiJuIu.mjs} +5 -5
- package/dist/{stats-DBLLFmKd.mjs → stats-Bq4fJOdB.mjs} +19 -5
- package/dist/{status-l84Gbvp2.mjs → status-CzS4bPO9.mjs} +5 -3
- package/dist/status-DlKvVJBE.mjs +49 -0
- package/dist/{status-DYXxNOT0.mjs → status-dXX2JX1C.mjs} +244 -50
- package/dist/{stop-CceYh1yG.mjs → stop-BCwvvs7F.mjs} +2 -2
- package/dist/{stop-Bad61z_2.mjs → stop-BjizrQwv.mjs} +83 -21
- package/dist/{stop-Bvf1MflS.mjs → stop-Cr1d8DSE.mjs} +4 -4
- package/dist/stop-cause-rhmGfwvt.mjs +97 -0
- package/dist/supervisor-run.d.mts +7 -4
- package/dist/supervisor-run.mjs +156 -27
- package/dist/{support-WLOzVKUv.mjs → support-BJPgYCu-.mjs} +61 -11
- package/dist/{support-CDQbDkL5.mjs → support-F9-sr0Oi.mjs} +6 -5
- package/dist/{toolchain-DmeYjXD9.mjs → toolchain-CNIBTIOv.mjs} +11 -11
- package/dist/trigger-Bemu8ytw.mjs +65 -0
- package/dist/trigger-CXts6qt1.mjs +2 -0
- package/dist/{warm-progress-DVHLN4uC.mjs → warm-progress-DF4JRvYI.mjs} +274 -5
- package/dist/watchman-APYjXSQa.mjs +48 -0
- package/dist/{web-k6w8-HnE.mjs → web-exQtGkoc.mjs} +14 -16
- package/dist/web-run.mjs +5 -5
- package/dist/{workspace-process-lock-BL7Q0HCO.mjs → workspace-process-lock-2mNzOlaB.mjs} +3 -2
- package/dist/{workspace-state-CL2V2Ons.mjs → workspace-state-DqcCbpE7.mjs} +34 -34
- package/dist/workspaces-Bcib97-6.mjs +306 -0
- package/dist/{worktree-BT_uULFx.mjs → worktree-Dc98cE6_.mjs} +1 -1
- package/dist/{worktree-qXKzADSC.mjs → worktree-ZpZR3S1k.mjs} +601 -33
- package/package.json +5 -5
- package/dist/device-capacity-DMuKC7nx.mjs +0 -136
- package/dist/hosted-macos-DwowNU7W.mjs +0 -447
- package/dist/logs-h9h6MUvE.mjs +0 -568
- package/dist/machine-usage-D_YYMzfR.mjs +0 -283
- package/dist/machines-CYtOCqcj.mjs +0 -259
- package/dist/merge-state-BnxLNQiM.mjs +0 -272
- package/dist/server-expo-KJeD109I.mjs +0 -2
- package/dist/settings-CduVcoxj.mjs +0 -680
- package/dist/stop-cause-CZ_Mr-v9.mjs +0 -665
- package/dist/watchman-Bx0wceoF.mjs +0 -238
- package/dist/worktree-C0c5Udwa.mjs +0 -753
|
@@ -2,37 +2,485 @@ import { t as getExecutor } from "./exec-CaErVv7t.mjs";
|
|
|
2
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
3
|
import { n as ownership_claim_exports, t as process_identity_exports } from "./process-identity-BmRm1qV0.mjs";
|
|
4
4
|
import { a as pullRequestLookup, r as endedPullRequest } from "./pull-request-pxnDCQzn.mjs";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
5
|
+
import { a as formatElapsed, h as shortUdid, i as formatDuration, l as phaseLine, p as releasedLeaseFact, u as plural } from "./command-output-CDAIFD-8.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, at as gitCommonDirOnDisk, bt as sourceCheckoutOf, ct as hasUncommittedWork, dt as listWorktrees, et as deleteBranch, ft as locallyKnownUpstream, ht as removeWorktree, i as SETTING_SHAPE_REMEDY, 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-Cfd-0r2d.mjs";
|
|
7
|
+
import { n as appProjectProblem, s as findProjectRoot$1 } from "./project-D47Rh0ly.mjs";
|
|
8
|
+
import { l as resolveProjectMetro, o as pidExists, r as killMetroTree, u as signalProcessTree } from "./metro-g8NqiJp-.mjs";
|
|
9
|
+
import { g as workspaceAgentDeviceDir, i as recordWorkspaceUse, r as readWorkspaceState$1, v as workspaceDir } from "./workspace-state-DqcCbpE7.mjs";
|
|
10
|
+
import { o as readWebRecord } from "./state-PeQiJuIu.mjs";
|
|
11
|
+
import { a as machineNumber } from "./budget-DM984mnd.mjs";
|
|
12
|
+
import { n as withIdleWorkspace, t as emptyWorkspaceDir } from "./in-use-CvGOUn-x.mjs";
|
|
13
|
+
import { t as agentDeviceDaemon } from "./activity-IRvJY_ru.mjs";
|
|
14
|
+
import { u as releaseWorkspaceLeases } from "./device-lease-DpoS36_u.mjs";
|
|
15
|
+
import { a as clearRemoteSession, c as readMetroTunnel, d as readRemoteSessionId, i as clearManagedMetroTunnel, u as readRemoteSession } from "./state-BKa8wNN5.mjs";
|
|
16
|
+
import { ft as parkedMaxSetting, ot as POOL_SETTING_REMEDY } from "./android-B_YNZy61.mjs";
|
|
11
17
|
import { a as workspaceLinks, i as workspaceLinkLine } from "./stim-desktop-Cnz8CPnl.mjs";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { n as
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import { t as
|
|
23
|
-
import { a as
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import { n as hasInstalledDependencies, r as installedNpmTreeIsValid, t as dependencyState } from "./dependency-state-BmsjKZ2H.mjs";
|
|
27
|
-
import { a as forgetStatusMeasures, t as startWarmProgress } from "./warm-progress-DVHLN4uC.mjs";
|
|
18
|
+
import { g as teardownOwnedIosSim, h as teardownOwnedBrowser, i as stopTunnel, l as withManagedTunnelRemovalLock, m as teardownOwnedAvd, s as withManagedRemoteWorktreeRemovalLock, t as resolveSupervisorTarget } from "./ownership-BMLqyBgJ.mjs";
|
|
19
|
+
import { t as clearNamedPorts } from "./named-ports-C8m8EQs2.mjs";
|
|
20
|
+
import { l as withdrawDevServerStopRequest, s as requestDevServerStop } from "./stop-cause-rhmGfwvt.mjs";
|
|
21
|
+
import { C as podInstallCommand, u as HEARTBEAT_INTERVAL_MS } from "./toolchain-CNIBTIOv.mjs";
|
|
22
|
+
import { n as verifyCollectorOwnership } from "./ownership-DyRQru0q.mjs";
|
|
23
|
+
import { t as stopMacosApp } from "./stop-Cr1d8DSE.mjs";
|
|
24
|
+
import { a as runPodInstall, i as runCaptured, n as podsAreStale, r as readPodState, t as DEPS_ERROR } from "./deps-DNWaQs4V.mjs";
|
|
25
|
+
import { i as stopHostedAndroid } from "./hosted-android-DwLS4Mse.mjs";
|
|
26
|
+
import { f as resolveEasCliBin } from "./remote-cache-Ci4DQYYj.mjs";
|
|
27
|
+
import { i as endRecordedSession } from "./device-remote-hFSkpfQu.mjs";
|
|
28
|
+
import { n as hasInstalledDependencies, r as installedNpmTreeIsValid, t as dependencyState } from "./dependency-state-CLcBHH2M.mjs";
|
|
29
|
+
import { a as forgetStatusMeasures, s as mergeState, t as startWarmProgress } from "./warm-progress-DF4JRvYI.mjs";
|
|
30
|
+
import { r as workspaceRecordingEnabled } from "./recordings-BNgsD1Tr.mjs";
|
|
31
|
+
import { r as stopHostedIos } from "./hosted-ios-aMo7Cljf.mjs";
|
|
28
32
|
import { createHash, randomUUID } from "node:crypto";
|
|
29
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
30
|
-
import { join, relative } from "node:path";
|
|
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";
|
|
31
35
|
import { existsSync as existsSync$1, realpathSync as realpathSync$1, rmSync as rmSync$1 } from "fs";
|
|
32
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";
|
|
33
|
-
import { workspaceName } from "@stim-cli/core";
|
|
34
|
-
import { LAST_BUILD_KEYS, readMacosRecord } from "@stim-cli/core/state";
|
|
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";
|
|
40
|
+
import { homedir } from "node:os";
|
|
35
41
|
import chalk from "chalk";
|
|
42
|
+
//#region src/archive.ts
|
|
43
|
+
const DAY = 864e5;
|
|
44
|
+
const KINDS = [
|
|
45
|
+
"logs",
|
|
46
|
+
"recordings",
|
|
47
|
+
"agentActions"
|
|
48
|
+
];
|
|
49
|
+
const KIND_PATH = {
|
|
50
|
+
logs: "logs",
|
|
51
|
+
recordings: "recordings",
|
|
52
|
+
agentActions: "agent-device/sessions"
|
|
53
|
+
};
|
|
54
|
+
function archiveSettings() {
|
|
55
|
+
const config = loadConfig();
|
|
56
|
+
return Object.fromEntries([
|
|
57
|
+
"maxAgeDays",
|
|
58
|
+
"maxCount",
|
|
59
|
+
"maxTotalGb",
|
|
60
|
+
"logs.maxAgeDays",
|
|
61
|
+
"logs.maxMbPerWorkspace",
|
|
62
|
+
"recordings.maxAgeDays",
|
|
63
|
+
"recordings.maxTotalGb",
|
|
64
|
+
"agentActions.maxAgeDays"
|
|
65
|
+
].map((key) => {
|
|
66
|
+
const result = machineNumber(`archive.${key}`, config, process.env);
|
|
67
|
+
if (result.error) throw new Error(result.error);
|
|
68
|
+
return [key, result.value];
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
function enabled(root) {
|
|
72
|
+
const config = loadConfig();
|
|
73
|
+
const project = config?.projects?.[root];
|
|
74
|
+
const present = existsSync(root);
|
|
75
|
+
const common = present ? gitCommonDirOnDisk(root) : null;
|
|
76
|
+
if (!present && Object.values(config?.repos ?? {}).some((repo) => repo.settings?.archive?.enabled === false)) return false;
|
|
77
|
+
return archiveEnabled(process.env, [
|
|
78
|
+
project?.settings,
|
|
79
|
+
common ? config?.repos?.[common]?.settings : void 0,
|
|
80
|
+
present ? readCommittedSettings(root) : void 0,
|
|
81
|
+
config
|
|
82
|
+
]);
|
|
83
|
+
}
|
|
84
|
+
function files(dir) {
|
|
85
|
+
if (!existsSync(dir)) return [];
|
|
86
|
+
return readdirSync(dir).flatMap((name) => {
|
|
87
|
+
const path = join(dir, name);
|
|
88
|
+
const stat = lstatSync(path);
|
|
89
|
+
if (stat.isSymbolicLink()) throw new Error(`refusing symlink ${path}`);
|
|
90
|
+
return stat.isDirectory() ? files(path) : [{
|
|
91
|
+
path,
|
|
92
|
+
bytes: stat.size,
|
|
93
|
+
at: stat.mtimeMs
|
|
94
|
+
}];
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function secureTree(dir) {
|
|
98
|
+
if (lstatSync(dir).isSymbolicLink()) throw new Error(`refusing symlink ${dir}`);
|
|
99
|
+
chmodSync(dir, 448);
|
|
100
|
+
for (const name of readdirSync(dir)) {
|
|
101
|
+
const path = join(dir, name);
|
|
102
|
+
const stat = lstatSync(path);
|
|
103
|
+
if (stat.isSymbolicLink()) throw new Error(`refusing symlink ${path}`);
|
|
104
|
+
if (stat.isDirectory()) secureTree(path);
|
|
105
|
+
else chmodSync(path, 384);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function writeRecord(dir, record) {
|
|
109
|
+
const small = [
|
|
110
|
+
"state.json",
|
|
111
|
+
"ended-agents.json",
|
|
112
|
+
"log-error-index.json"
|
|
113
|
+
].reduce((sum, name) => {
|
|
114
|
+
const path = join(dir, name);
|
|
115
|
+
return sum + (existsSync(path) ? statSync(path).size : 0);
|
|
116
|
+
}, 0);
|
|
117
|
+
for (let pass = 0; pass < 8; pass++) {
|
|
118
|
+
record.bytes.total = KINDS.reduce((sum, kind) => sum + record.bytes[kind], record.bytes.record);
|
|
119
|
+
const bytes = small + Buffer.byteLength(`${JSON.stringify(record)}\n`);
|
|
120
|
+
if (bytes === record.bytes.record) break;
|
|
121
|
+
record.bytes.record = bytes;
|
|
122
|
+
}
|
|
123
|
+
record.bytes.total = KINDS.reduce((sum, kind) => sum + record.bytes[kind], record.bytes.record);
|
|
124
|
+
const tmp = join(dir, `.archive-${randomUUID()}.tmp`);
|
|
125
|
+
try {
|
|
126
|
+
writeFileSync(tmp, `${JSON.stringify(record)}\n`, { mode: 384 });
|
|
127
|
+
renameSync(tmp, join(dir, "archive.json"));
|
|
128
|
+
} finally {
|
|
129
|
+
rmSync(tmp, { force: true });
|
|
130
|
+
}
|
|
131
|
+
const now = /* @__PURE__ */ new Date();
|
|
132
|
+
utimesSync(archiveRoot(), now, now);
|
|
133
|
+
}
|
|
134
|
+
function removeRecord(id) {
|
|
135
|
+
const dir = archiveDir(id);
|
|
136
|
+
if (!existsSync(dir)) return;
|
|
137
|
+
const removing = join(archiveRoot(), `.removing-${id}`);
|
|
138
|
+
renameSync(dir, removing);
|
|
139
|
+
rmSync(removing, {
|
|
140
|
+
recursive: true,
|
|
141
|
+
force: true
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function sweepArchiveStaging(remove, waitMs = 0) {
|
|
145
|
+
if (!existsSync(archiveRoot())) return [];
|
|
146
|
+
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) => {
|
|
147
|
+
const path = join(archiveRoot(), name.endsWith(".claims") ? name.slice(0, -7) : name);
|
|
148
|
+
const root = `${path}.claims`;
|
|
149
|
+
const survey = name.startsWith(".incoming-") ? readClaimSet(root) : null;
|
|
150
|
+
const unresolved = survey?.unresolved[0];
|
|
151
|
+
const kept = Boolean(survey && (survey.live.length || survey.unresolved.length));
|
|
152
|
+
const entry = {
|
|
153
|
+
path,
|
|
154
|
+
kept,
|
|
155
|
+
reason: unresolved?.reason ?? (kept ? "live archive writer" : null),
|
|
156
|
+
removeCommand: unresolved ? claimRemoveCommand(root) : null
|
|
157
|
+
};
|
|
158
|
+
if (remove && !kept) {
|
|
159
|
+
if (!survey || clearFreeClaimSet({
|
|
160
|
+
root,
|
|
161
|
+
label: "archive staging"
|
|
162
|
+
}).status === "cleared") {
|
|
163
|
+
rmSync(path, {
|
|
164
|
+
recursive: true,
|
|
165
|
+
force: true
|
|
166
|
+
});
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return [entry];
|
|
171
|
+
}), { waitMs });
|
|
172
|
+
}
|
|
173
|
+
function stripPids(value) {
|
|
174
|
+
if (Array.isArray(value)) return value.map(stripPids);
|
|
175
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).filter(([key]) => !/pids?$/i.test(key)).map(([key, child]) => [key, stripPids(child)]));
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
function gitFacts(root) {
|
|
179
|
+
const empty = {
|
|
180
|
+
repository: null,
|
|
181
|
+
branch: null,
|
|
182
|
+
head: null,
|
|
183
|
+
subject: null,
|
|
184
|
+
merged: null,
|
|
185
|
+
pullRequest: null
|
|
186
|
+
};
|
|
187
|
+
if (!existsSync(root)) return cachedFacts(root) ?? empty;
|
|
188
|
+
const exec = getExecutor();
|
|
189
|
+
const run = (args) => exec.runFileQuiet("git", [
|
|
190
|
+
"-C",
|
|
191
|
+
root,
|
|
192
|
+
...args
|
|
193
|
+
], { timeoutMs: 1e3 });
|
|
194
|
+
const common = gitCommonDirOnDisk(root);
|
|
195
|
+
const summary = run([
|
|
196
|
+
"show",
|
|
197
|
+
"-s",
|
|
198
|
+
"--format=%H%n%s",
|
|
199
|
+
"HEAD"
|
|
200
|
+
])?.split("\n");
|
|
201
|
+
const location = run([
|
|
202
|
+
"rev-parse",
|
|
203
|
+
"--show-toplevel",
|
|
204
|
+
"--abbrev-ref",
|
|
205
|
+
"HEAD"
|
|
206
|
+
])?.split("\n");
|
|
207
|
+
const branch = location?.[1] && location[1] !== "HEAD" ? location[1] : null;
|
|
208
|
+
const defaultRef = run([
|
|
209
|
+
"symbolic-ref",
|
|
210
|
+
"--quiet",
|
|
211
|
+
"refs/remotes/origin/HEAD"
|
|
212
|
+
]);
|
|
213
|
+
let merged = null;
|
|
214
|
+
if (defaultRef?.startsWith("refs/remotes/")) {
|
|
215
|
+
const state = mergeState(root, {
|
|
216
|
+
ref: defaultRef,
|
|
217
|
+
name: defaultRef.slice(13)
|
|
218
|
+
}, { timeoutMs: 2e3 });
|
|
219
|
+
merged = state.merged ? true : state.unknown ? null : false;
|
|
220
|
+
}
|
|
221
|
+
let prPath = root;
|
|
222
|
+
try {
|
|
223
|
+
if (location?.[0]) prPath = realpathSync(location[0]);
|
|
224
|
+
} catch {}
|
|
225
|
+
const pr = readPullRequestCache(prPath)?.pullRequest;
|
|
226
|
+
if (pr?.state === "merged") merged = true;
|
|
227
|
+
return {
|
|
228
|
+
repository: common ? basename(common) === ".git" ? dirname(common) : common : null,
|
|
229
|
+
branch,
|
|
230
|
+
head: summary?.[0] ?? null,
|
|
231
|
+
subject: summary?.[1] ?? null,
|
|
232
|
+
merged,
|
|
233
|
+
pullRequest: archivedPullRequest(pr)
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function archivedPullRequest(pr) {
|
|
237
|
+
return pr ? {
|
|
238
|
+
number: pr.number,
|
|
239
|
+
state: pr.state,
|
|
240
|
+
title: pr.title,
|
|
241
|
+
url: pr.url
|
|
242
|
+
} : null;
|
|
243
|
+
}
|
|
244
|
+
function cachedFacts(root) {
|
|
245
|
+
for (let path = root; !existsSync(path); path = dirname(path)) {
|
|
246
|
+
const cached = readPullRequestCache(path);
|
|
247
|
+
if (!cached) continue;
|
|
248
|
+
return {
|
|
249
|
+
repository: null,
|
|
250
|
+
branch: cached.branch,
|
|
251
|
+
head: cached.head,
|
|
252
|
+
subject: null,
|
|
253
|
+
merged: cached.pullRequest?.state === "merged" ? true : null,
|
|
254
|
+
pullRequest: archivedPullRequest(cached.pullRequest)
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
function archiveWorkspace(root, removedBy, state = readWorkspaceState(root)) {
|
|
260
|
+
writeArchive(root, removedBy, state);
|
|
261
|
+
try {
|
|
262
|
+
enforceArchiveRetention();
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.error(`could not enforce archive retention: ${String(error?.message ?? error).split("\n")[0]}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function writeArchive(root, removedBy, state) {
|
|
268
|
+
let staging = null;
|
|
269
|
+
let claim;
|
|
270
|
+
try {
|
|
271
|
+
const dir = workspaceDir(root);
|
|
272
|
+
if (!existsSync(dir) || lstatSync(dir).isSymbolicLink()) return;
|
|
273
|
+
if (existsSync(root)) root = realpathSync(root);
|
|
274
|
+
try {
|
|
275
|
+
sweepArchiveStaging(true, 5e3);
|
|
276
|
+
} catch {}
|
|
277
|
+
if (!enabled(root)) return;
|
|
278
|
+
const limits = archiveSettings();
|
|
279
|
+
if (!limits.maxAgeDays || !limits.maxCount) return;
|
|
280
|
+
const logs = join(dir, "logs");
|
|
281
|
+
if (!existsSync(join(dir, "state.json")) && !Object.values(readBuildHistory(state)).some((entries) => entries?.length) && (!existsSync(logs) || readdirSync(logs).length === 0)) return;
|
|
282
|
+
const worktree = gitFacts(root);
|
|
283
|
+
mkdirSync(archiveRoot(), {
|
|
284
|
+
recursive: true,
|
|
285
|
+
mode: 448
|
|
286
|
+
});
|
|
287
|
+
chmodSync(archiveRoot(), 448);
|
|
288
|
+
const token = randomUUID();
|
|
289
|
+
staging = join(archiveRoot(), `.incoming-${token}`);
|
|
290
|
+
const attempt = tryAcquireClaim({
|
|
291
|
+
root: `${staging}.claims`,
|
|
292
|
+
mode: "exclusive",
|
|
293
|
+
label: "archive staging"
|
|
294
|
+
});
|
|
295
|
+
claim = attempt.acquired;
|
|
296
|
+
if (!claim) {
|
|
297
|
+
releaseClaim(attempt.pending);
|
|
298
|
+
throw new Error("archive staging claim is held");
|
|
299
|
+
}
|
|
300
|
+
withDirLock(archiveLock(), () => {
|
|
301
|
+
mkdirSync(staging, { mode: 448 });
|
|
302
|
+
let at = Date.now();
|
|
303
|
+
const workspace = workspaceName(root);
|
|
304
|
+
while (existsSync(archiveDir(`${workspace}--${at}`))) at++;
|
|
305
|
+
const id = `${workspace}--${at}`;
|
|
306
|
+
if (state) {
|
|
307
|
+
const { supervisor: _supervisor, collectors: _collectors, warm: _warm, ...stable } = state;
|
|
308
|
+
writeFileSync(join(staging, "state.json"), `${JSON.stringify(stripPids(stable))}\n`, { mode: 384 });
|
|
309
|
+
}
|
|
310
|
+
for (const name of ["ended-agents.json", "log-error-index.json"]) {
|
|
311
|
+
const data = readJsonObject(join(dir, name));
|
|
312
|
+
if (data) writeFileSync(join(staging, name), `${JSON.stringify(stripPids(data))}\n`, { mode: 384 });
|
|
313
|
+
}
|
|
314
|
+
const move = (kind) => {
|
|
315
|
+
const from = join(dir, KIND_PATH[kind]);
|
|
316
|
+
if (!limits[`${kind}.maxAgeDays`] || !existsSync(from)) return;
|
|
317
|
+
const to = join(staging, KIND_PATH[kind]);
|
|
318
|
+
mkdirSync(join(to, ".."), {
|
|
319
|
+
recursive: true,
|
|
320
|
+
mode: 448
|
|
321
|
+
});
|
|
322
|
+
renameSync(from, to);
|
|
323
|
+
secureTree(to);
|
|
324
|
+
};
|
|
325
|
+
move("logs");
|
|
326
|
+
move("agentActions");
|
|
327
|
+
const config = loadConfig();
|
|
328
|
+
if (limits["recordings.maxAgeDays"] && workspaceRecordingEnabled(root, config?.projects?.[root] ?? {}, config, process.env)) {
|
|
329
|
+
const recordingDir = join(dir, "recordings");
|
|
330
|
+
if (existsSync(recordingDir) && lstatSync(recordingDir).isSymbolicLink()) throw new Error(`refusing symlink ${recordingDir}`);
|
|
331
|
+
for (const device of listRecordedDevices(recordingDir)) {
|
|
332
|
+
if (lstatSync(device.dir).isSymbolicLink()) throw new Error(`refusing symlink ${device.dir}`);
|
|
333
|
+
const to = join(staging, "recordings", `${device.platform}-${device.slot}`);
|
|
334
|
+
for (const segment of device.segments.filter((entry) => !entry.open)) {
|
|
335
|
+
mkdirSync(to, {
|
|
336
|
+
recursive: true,
|
|
337
|
+
mode: 448
|
|
338
|
+
});
|
|
339
|
+
try {
|
|
340
|
+
if (lstatSync(segment.file).isSymbolicLink()) throw new Error(`refusing symlink ${segment.file}`);
|
|
341
|
+
renameSync(segment.file, join(to, basename(segment.file)));
|
|
342
|
+
chmodSync(join(to, basename(segment.file)), 384);
|
|
343
|
+
} catch (error) {
|
|
344
|
+
if (error.code !== "ENOENT") throw error;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const index = readJsonObject(join(staging, "log-error-index.json"));
|
|
350
|
+
const indexed = Object.values(index?.files ?? {}).flatMap((file) => Object.values(file.markers ?? {}).concat(file.errors ?? []));
|
|
351
|
+
const lastErrorCount = countErrorEntries(sortByTs(queryLogs({
|
|
352
|
+
records: indexed,
|
|
353
|
+
errorsOnly: true
|
|
354
|
+
})));
|
|
355
|
+
const history = Object.values(readBuildHistory(state)).flat();
|
|
356
|
+
const last = Object.values(readLastBuilds(state)).toSorted((a, b) => Date.parse(b.startedAt) - Date.parse(a.startedAt))[0] ?? null;
|
|
357
|
+
const bytes = {
|
|
358
|
+
logs: 0,
|
|
359
|
+
recordings: 0,
|
|
360
|
+
agentActions: 0,
|
|
361
|
+
record: 0,
|
|
362
|
+
total: 0
|
|
363
|
+
};
|
|
364
|
+
const expires = {
|
|
365
|
+
logs: null,
|
|
366
|
+
recordings: null,
|
|
367
|
+
agentActions: null,
|
|
368
|
+
record: new Date(at + limits.maxAgeDays * DAY).toISOString()
|
|
369
|
+
};
|
|
370
|
+
for (const kind of KINDS) {
|
|
371
|
+
bytes[kind] = files(join(staging, KIND_PATH[kind])).reduce((sum, file) => sum + file.bytes, 0);
|
|
372
|
+
if (bytes[kind]) expires[kind] = new Date(at + limits[`${kind}.maxAgeDays`] * DAY).toISOString();
|
|
373
|
+
}
|
|
374
|
+
const record = {
|
|
375
|
+
id,
|
|
376
|
+
projectRoot: root,
|
|
377
|
+
project: basename(root),
|
|
378
|
+
workspace,
|
|
379
|
+
worktree,
|
|
380
|
+
removedAt: new Date(at).toISOString(),
|
|
381
|
+
removedBy,
|
|
382
|
+
lastUsedAt: typeof state?.lastUsedAt === "string" ? state.lastUsedAt : null,
|
|
383
|
+
builds: {
|
|
384
|
+
count: history.length,
|
|
385
|
+
last,
|
|
386
|
+
lastErrorCount
|
|
387
|
+
},
|
|
388
|
+
agents: (readJsonObject(join(dir, "ended-agents.json"))?.sessions ?? []).map(endedAgentSessionOf).filter((session) => session !== null),
|
|
389
|
+
bytes,
|
|
390
|
+
expires,
|
|
391
|
+
version: 1
|
|
392
|
+
};
|
|
393
|
+
writeRecord(staging, record);
|
|
394
|
+
renameSync(staging, archiveDir(id));
|
|
395
|
+
}, { waitMs: 3e4 });
|
|
396
|
+
releaseClaim(claim);
|
|
397
|
+
claim = void 0;
|
|
398
|
+
staging = null;
|
|
399
|
+
} catch (error) {
|
|
400
|
+
if (staging) try {
|
|
401
|
+
rmSync(staging, {
|
|
402
|
+
recursive: true,
|
|
403
|
+
force: true
|
|
404
|
+
});
|
|
405
|
+
} catch {}
|
|
406
|
+
console.error(`could not archive: ${String(error?.message ?? error).split("\n")[0]}`);
|
|
407
|
+
} finally {
|
|
408
|
+
releaseClaim(claim);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function removeKind(record, kind) {
|
|
412
|
+
rmSync(join(archiveDir(record.id), KIND_PATH[kind]), {
|
|
413
|
+
recursive: true,
|
|
414
|
+
force: true
|
|
415
|
+
});
|
|
416
|
+
record.bytes[kind] = 0;
|
|
417
|
+
record.expires[kind] = null;
|
|
418
|
+
writeRecord(archiveDir(record.id), record);
|
|
419
|
+
}
|
|
420
|
+
function logRemovalOrder(path) {
|
|
421
|
+
return /\.ndjson\.\d+$/.test(path) ? 0 : /^build-.*\.ndjson$/.test(basename(path)) ? 1 : 2;
|
|
422
|
+
}
|
|
423
|
+
function enforceArchiveRetention(now = Date.now()) {
|
|
424
|
+
if (!existsSync(archiveRoot())) return;
|
|
425
|
+
let limits;
|
|
426
|
+
try {
|
|
427
|
+
limits = archiveSettings();
|
|
428
|
+
} catch (error) {
|
|
429
|
+
console.error(`could not enforce archive retention: ${String(error.message).split("\n")[0]}`);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
withDirLock(archiveLock(), () => {
|
|
433
|
+
const listed = readArchives().toReversed();
|
|
434
|
+
const records = listed.filter((record, index) => limits.maxAgeDays > 0 && now - Date.parse(record.removedAt) < limits.maxAgeDays * DAY && index >= listed.length - limits.maxCount);
|
|
435
|
+
for (const record of listed) if (!records.includes(record)) removeRecord(record.id);
|
|
436
|
+
const recordings = recordingEnabled(process.env, [loadConfig()]);
|
|
437
|
+
for (const record of records) {
|
|
438
|
+
for (const kind of [
|
|
439
|
+
"recordings",
|
|
440
|
+
"agentActions",
|
|
441
|
+
"logs"
|
|
442
|
+
]) {
|
|
443
|
+
const age = limits[`${kind}.maxAgeDays`];
|
|
444
|
+
if (record.bytes[kind] && (!age || now - Date.parse(record.removedAt) >= age * DAY || kind === "recordings" && !recordings)) removeKind(record, kind);
|
|
445
|
+
}
|
|
446
|
+
const cap = limits["logs.maxMbPerWorkspace"] * 1024 ** 2;
|
|
447
|
+
if (record.bytes.logs > cap) {
|
|
448
|
+
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);
|
|
449
|
+
for (const file of entries) {
|
|
450
|
+
if (record.bytes.logs <= cap) break;
|
|
451
|
+
rmSync(file.path);
|
|
452
|
+
record.bytes.logs = Math.max(0, record.bytes.logs - file.bytes);
|
|
453
|
+
}
|
|
454
|
+
if (!record.bytes.logs) record.expires.logs = null;
|
|
455
|
+
writeRecord(archiveDir(record.id), record);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const totalCap = limits.maxTotalGb * 1024 ** 3;
|
|
459
|
+
const recordingCap = limits["recordings.maxTotalGb"] * 1024 ** 3;
|
|
460
|
+
for (const record of records) for (;;) {
|
|
461
|
+
const usage = archivedUsage(records);
|
|
462
|
+
const totalOver = usage.bytes > totalCap;
|
|
463
|
+
const recordingsOver = usage.byKind.recordings > recordingCap;
|
|
464
|
+
if (!totalOver && !recordingsOver) break;
|
|
465
|
+
const candidates = KINDS.filter((kind) => record.bytes[kind] > 0 && (totalOver || kind === "recordings")).toSorted((a, b) => record.bytes[b] - record.bytes[a]);
|
|
466
|
+
if (!candidates.length) break;
|
|
467
|
+
removeKind(record, candidates[0]);
|
|
468
|
+
}
|
|
469
|
+
for (const record of records) {
|
|
470
|
+
const before = JSON.stringify(record.expires);
|
|
471
|
+
for (const kind of KINDS) record.expires[kind] = record.bytes[kind] ? new Date(Date.parse(record.removedAt) + limits[`${kind}.maxAgeDays`] * DAY).toISOString() : null;
|
|
472
|
+
record.expires.record = new Date(Date.parse(record.removedAt) + limits.maxAgeDays * DAY).toISOString();
|
|
473
|
+
if (JSON.stringify(record.expires) !== before) writeRecord(archiveDir(record.id), record);
|
|
474
|
+
}
|
|
475
|
+
}, { waitMs: 3e4 });
|
|
476
|
+
}
|
|
477
|
+
function deleteSelectedArchives(ids, kind) {
|
|
478
|
+
withDirLock(archiveLock(), () => {
|
|
479
|
+
for (const record of readArchives().filter((entry) => ids.includes(entry.id))) if (kind) removeKind(record, kind);
|
|
480
|
+
else removeRecord(record.id);
|
|
481
|
+
}, { waitMs: 3e4 });
|
|
482
|
+
}
|
|
483
|
+
//#endregion
|
|
36
484
|
//#region src/devices/reclaim.ts
|
|
37
485
|
async function reapCollectors(root, { verify = verifyCollectorOwnership, collectors }) {
|
|
38
486
|
const skippedDevices = [];
|
|
@@ -288,6 +736,7 @@ async function reclaimProject(path, options = {}) {
|
|
|
288
736
|
const existed = existsSync(dir);
|
|
289
737
|
const run = await withIdleWorkspace(path, () => reclaimIdleProject(path, existed, options), {
|
|
290
738
|
purpose: "workspace removal",
|
|
739
|
+
hosted: false,
|
|
291
740
|
supervisor: false,
|
|
292
741
|
managedLocks: false
|
|
293
742
|
});
|
|
@@ -316,7 +765,7 @@ async function reclaimProject(path, options = {}) {
|
|
|
316
765
|
failedWorkspaceDirs: []
|
|
317
766
|
};
|
|
318
767
|
}
|
|
319
|
-
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" } }) {
|
|
768
|
+
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" } }) {
|
|
320
769
|
await clearNamedPorts(path, { stop: true });
|
|
321
770
|
const project = getProject(path);
|
|
322
771
|
for (const { platforms } of projectDeviceSlots(project)) if (platforms.android?.owned && platforms.android.avdName) (0, ownership_claim_exports.releaseClaim)(acquireAvdClaim(platforms.android.avdName));
|
|
@@ -395,6 +844,17 @@ async function reclaimIdleProject(path, workspaceExisted, { deleteOwnedDevices =
|
|
|
395
844
|
};
|
|
396
845
|
skippedDevices.push(...skippedCollectors);
|
|
397
846
|
failedDevices.push(...failedCollectors);
|
|
847
|
+
for (const platform of ["ios", "android"]) try {
|
|
848
|
+
await (platform === "ios" ? stopHostedIos : stopHostedAndroid)(path);
|
|
849
|
+
} catch (error) {
|
|
850
|
+
const failed = {
|
|
851
|
+
platform,
|
|
852
|
+
name: `hosted ${platform} device`,
|
|
853
|
+
reason: error.message
|
|
854
|
+
};
|
|
855
|
+
failedDevices.push(failed);
|
|
856
|
+
skippedDevices.push(failed);
|
|
857
|
+
}
|
|
398
858
|
const remote = reclaimRemoteSession(path, { stopSession });
|
|
399
859
|
if (remote.failed) {
|
|
400
860
|
skippedDevices.push(remote.failed);
|
|
@@ -428,6 +888,28 @@ async function reclaimIdleProject(path, workspaceExisted, { deleteOwnedDevices =
|
|
|
428
888
|
if (failedDevices.length === 0 && !supervisorHeld) {
|
|
429
889
|
const dir = workspaceDir(path);
|
|
430
890
|
if (existsSync(dir)) try {
|
|
891
|
+
const stateDir = workspaceAgentDeviceDir(path);
|
|
892
|
+
if (!["none", "dead"].includes(agentDeviceDaemon(stateDir))) {
|
|
893
|
+
const exec = getExecutor();
|
|
894
|
+
try {
|
|
895
|
+
if (!exec.findExecutable("agent-device")) throw new Error("agent-device is not on PATH");
|
|
896
|
+
exec.runFile("agent-device", [
|
|
897
|
+
"daemon",
|
|
898
|
+
"stop",
|
|
899
|
+
"--state-dir",
|
|
900
|
+
stateDir,
|
|
901
|
+
"--clean"
|
|
902
|
+
], {
|
|
903
|
+
timeoutMs: 2e4,
|
|
904
|
+
killSignal: "SIGKILL",
|
|
905
|
+
env: { AGENT_DEVICE_NO_UPDATE_NOTIFIER: "1" }
|
|
906
|
+
});
|
|
907
|
+
} catch (error) {
|
|
908
|
+
process.stderr.write(`${phaseLine("agent-device", `daemon could not be stopped: ${error.message}; run agent-device daemon stop --state-dir "${stateDir}" --clean`)}\n`);
|
|
909
|
+
throw error;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
if (archive) archiveWorkspace(path, archive.removedBy, initialState);
|
|
431
913
|
emptyWorkspaceDir(dir);
|
|
432
914
|
if (workspaceExisted) removedWorkspaceDirs.push(dir);
|
|
433
915
|
} catch {
|
|
@@ -457,6 +939,66 @@ async function reclaimIdleProject(path, workspaceExisted, { deleteOwnedDevices =
|
|
|
457
939
|
};
|
|
458
940
|
}
|
|
459
941
|
//#endregion
|
|
942
|
+
//#region src/workspace/service-guard.ts
|
|
943
|
+
const SERVICE_IN_USE = { code: "STIM_WORKTREE_SERVICE" };
|
|
944
|
+
function serviceUseOf(plist, file, folder, canonical) {
|
|
945
|
+
if (typeof plist !== "object" || plist === null) return null;
|
|
946
|
+
const job = plist;
|
|
947
|
+
if (typeof job.StimService !== "object" || job.StimService === null || !Array.isArray(job.ProgramArguments)) return null;
|
|
948
|
+
const argument = job.ProgramArguments.find((value) => typeof value === "string" && value.startsWith("/") && isPathPrefix(folder, canonical(value)));
|
|
949
|
+
if (argument === void 0) return null;
|
|
950
|
+
return {
|
|
951
|
+
label: typeof job.Label === "string" ? job.Label : file,
|
|
952
|
+
plist: file,
|
|
953
|
+
argument
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
/** The first installed stim-server service that runs from inside `folder`, or null. macOS only. */
|
|
957
|
+
function serviceRunningFrom(folder, canonical, agentsDir = join(homedir(), "Library", "LaunchAgents")) {
|
|
958
|
+
if (process.platform !== "darwin") return null;
|
|
959
|
+
let names;
|
|
960
|
+
try {
|
|
961
|
+
names = readdirSync(agentsDir).filter((name) => name.endsWith(".plist"));
|
|
962
|
+
} catch (error) {
|
|
963
|
+
return error.code === "ENOENT" ? null : {
|
|
964
|
+
label: "unknown",
|
|
965
|
+
plist: agentsDir,
|
|
966
|
+
argument: null
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
for (const name of names) {
|
|
970
|
+
const file = join(agentsDir, name);
|
|
971
|
+
const label = name.replace(/\.plist$/, "");
|
|
972
|
+
try {
|
|
973
|
+
if (!readFileSync(file).includes("StimService")) continue;
|
|
974
|
+
} catch {
|
|
975
|
+
return {
|
|
976
|
+
label,
|
|
977
|
+
plist: file,
|
|
978
|
+
argument: null
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
try {
|
|
982
|
+
const json = getExecutor().runFile("plutil", [
|
|
983
|
+
"-convert",
|
|
984
|
+
"json",
|
|
985
|
+
"-o",
|
|
986
|
+
"-",
|
|
987
|
+
file
|
|
988
|
+
], { timeoutMs: 5e3 });
|
|
989
|
+
const use = serviceUseOf(JSON.parse(json), file, folder, canonical);
|
|
990
|
+
if (use) return use;
|
|
991
|
+
} catch {
|
|
992
|
+
return {
|
|
993
|
+
label,
|
|
994
|
+
plist: file,
|
|
995
|
+
argument: null
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
1001
|
+
//#endregion
|
|
460
1002
|
//#region src/engine/warm-claim.ts
|
|
461
1003
|
const WARM_CLAIM_LABEL = "worktree warm";
|
|
462
1004
|
const WARM_CLAIM_POLL_MS = 250;
|
|
@@ -1492,7 +2034,7 @@ async function withReclaimLocks(rootPath, fn) {
|
|
|
1492
2034
|
const acquire = (index) => index === keys.length ? fn(keys) : withManagedTunnelRemovalLock(keys[index], () => acquire(index + 1));
|
|
1493
2035
|
return acquire(0);
|
|
1494
2036
|
}
|
|
1495
|
-
async function reclaimAll(rootPath, keys = reclaimKeys(rootPath), { preserveRootProject = false } = {}) {
|
|
2037
|
+
async function reclaimAll(rootPath, keys = reclaimKeys(rootPath), { preserveRootProject = false, removedBy } = {}) {
|
|
1496
2038
|
const dereferenced = [];
|
|
1497
2039
|
const killedPids = [];
|
|
1498
2040
|
const deletedDevices = [];
|
|
@@ -1511,6 +2053,7 @@ async function reclaimAll(rootPath, keys = reclaimKeys(rootPath), { preserveRoot
|
|
|
1511
2053
|
let r;
|
|
1512
2054
|
try {
|
|
1513
2055
|
r = await reclaimProject(key, {
|
|
2056
|
+
archive: removedBy ? { removedBy } : void 0,
|
|
1514
2057
|
deleteOwnedDevices: true,
|
|
1515
2058
|
parkOwnedDevices: true,
|
|
1516
2059
|
preserveProjectRecord: preserveRootProject && key === rootPath,
|
|
@@ -1614,9 +2157,13 @@ function hasRegisteredProjectUnder(rootPath) {
|
|
|
1614
2157
|
const cfg = loadConfig$1();
|
|
1615
2158
|
return Object.keys(cfg?.projects ?? {}).some((key) => isPathPrefix(rootPath, key));
|
|
1616
2159
|
}
|
|
2160
|
+
function projectAtOrAbove(root, path) {
|
|
2161
|
+
const here = nativeCanonicalPath(path);
|
|
2162
|
+
return Object.keys(loadConfig$1()?.projects ?? {}).filter((key) => isPathPrefix(nativeCanonicalPath(root), nativeCanonicalPath(key)) && isPathPrefix(nativeCanonicalPath(key), here)).toSorted((a, b) => b.length - a.length)[0] ?? null;
|
|
2163
|
+
}
|
|
1617
2164
|
async function reclaimEnvironment(root, why) {
|
|
1618
2165
|
await withManagedRemoteWorktreeRemovalLock(root, () => withReclaimLocks(root, async (lockedKeys) => {
|
|
1619
|
-
const result = await reclaimAll(root, lockedKeys);
|
|
2166
|
+
const result = await reclaimAll(root, lockedKeys, { removedBy: "worktree-remove" });
|
|
1620
2167
|
for (const line of poolLines(result)) console.error(line);
|
|
1621
2168
|
for (const device of result.deletedDevices) console.error(chalk.dim(phaseLine("device", `deleted ${device}`)));
|
|
1622
2169
|
for (const session of result.stoppedSessions) console.error(chalk.dim(phaseLine("device", `stopped remote session ${session}`)));
|
|
@@ -1791,7 +2338,18 @@ async function runRemove(target, opts, onRemoved) {
|
|
|
1791
2338
|
return;
|
|
1792
2339
|
}
|
|
1793
2340
|
if (entry.path === source.path) {
|
|
1794
|
-
if (entry.path !== path)
|
|
2341
|
+
if (nativeCanonicalPath(entry.path) !== nativeCanonicalPath(path)) {
|
|
2342
|
+
const project = projectAtOrAbove(entry.path, path);
|
|
2343
|
+
if (!project) {
|
|
2344
|
+
console.error(chalk.red(`Refusing to reclaim from ${path}: no Stim project is registered at or above it.`));
|
|
2345
|
+
console.error(chalk.dim(` Run it from the project folder, or from ${entry.path} to reclaim every project under it.`));
|
|
2346
|
+
process.exitCode = 1;
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
console.error(chalk.dim(`${path} is inside the source checkout ${entry.path}; reclaiming only the project ${project}.`));
|
|
2350
|
+
await reclaimEnvironment(project, "it is the source checkout");
|
|
2351
|
+
return;
|
|
2352
|
+
}
|
|
1795
2353
|
await reclaimEnvironment(entry.path, "it is the source checkout");
|
|
1796
2354
|
return;
|
|
1797
2355
|
}
|
|
@@ -1805,6 +2363,13 @@ async function runRemove(target, opts, onRemoved) {
|
|
|
1805
2363
|
process.exitCode = 1;
|
|
1806
2364
|
return;
|
|
1807
2365
|
}
|
|
2366
|
+
const service = serviceRunningFrom(path, nativeCanonicalPath);
|
|
2367
|
+
if (service) {
|
|
2368
|
+
console.error(chalk.red(service.argument ? `Refusing to remove ${path}: it runs the stim-server service ${service.label} (${service.argument}). ${SERVICE_IN_USE.code}` : `Refusing to remove ${path}: could not read ${service.plist} to check whether the stim-server service ${service.label} runs from it. ${SERVICE_IN_USE.code}`));
|
|
2369
|
+
console.error(chalk.dim(`Reinstall the service from another build with \`stim-server service install --label ${service.label}\`, or remove it with \`stim-server service uninstall --label ${service.label}\`, then retry.`));
|
|
2370
|
+
process.exitCode = 1;
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
1808
2373
|
const project = getProject(path);
|
|
1809
2374
|
const branch = entry.branch;
|
|
1810
2375
|
const ownsBranch = Boolean(branch && project?.worktreeBranchOwned === true && project.worktreeBranch === branch);
|
|
@@ -1835,7 +2400,10 @@ async function runRemove(target, opts, onRemoved) {
|
|
|
1835
2400
|
printRemovalRefusal(path, current);
|
|
1836
2401
|
return;
|
|
1837
2402
|
}
|
|
1838
|
-
const result = await reclaimAll(path, lockedKeys, {
|
|
2403
|
+
const result = await reclaimAll(path, lockedKeys, {
|
|
2404
|
+
preserveRootProject: true,
|
|
2405
|
+
removedBy: opts.removedBy ?? "worktree-remove"
|
|
2406
|
+
});
|
|
1839
2407
|
if (result.keptEntries.length) {
|
|
1840
2408
|
reportRetainedResources(path, result);
|
|
1841
2409
|
return;
|
|
@@ -1904,7 +2472,7 @@ async function runRemove(target, opts, onRemoved) {
|
|
|
1904
2472
|
}));
|
|
1905
2473
|
}
|
|
1906
2474
|
function registerRemove(worktree) {
|
|
1907
|
-
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) => {
|
|
2475
|
+
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; from a subfolder it reclaims only the project at or above that folder, with the projects nested under it.").option("--force", "remove even when the worktree holds uncommitted or unpushed work or initialized submodules").action(async (target, opts) => {
|
|
1908
2476
|
await removeWorktreeTarget(target, { force: opts.force });
|
|
1909
2477
|
});
|
|
1910
2478
|
}
|
|
@@ -1914,4 +2482,4 @@ function worktreeCommand(program) {
|
|
|
1914
2482
|
registerRemove(worktree);
|
|
1915
2483
|
}
|
|
1916
2484
|
//#endregion
|
|
1917
|
-
export { 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 };
|
|
2485
|
+
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 };
|