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.
Files changed (123) hide show
  1. package/dist/{activity-B2-kb7k0.mjs → activity-IRvJY_ru.mjs} +49 -16
  2. package/dist/agent-device-usage-output-BK41vnzN.mjs +411 -0
  3. package/dist/{android-Cu_L4RpL.mjs → android--GorydsK.mjs} +692 -321
  4. package/dist/{android-Db2eEMaf.mjs → android-B_YNZy61.mjs} +25 -60
  5. package/dist/{android-CYmq0jNi.mjs → android-DE4v59Mb.mjs} +12 -3
  6. package/dist/{android-cas-PnKfAdB2.mjs → android-cas-ZFpjJBL_.mjs} +6 -5
  7. package/dist/{app-install-CAAplXAt.mjs → app-install-VPkI5WEg.mjs} +52 -8
  8. package/dist/attempt-Bk2ouQVL.mjs +39 -0
  9. package/dist/{budget-CMiqghig.mjs → budget-DM984mnd.mjs} +372 -366
  10. package/dist/{build-plan-aeGMFX7e.mjs → build-plan-CmQCoyT-.mjs} +346 -88
  11. package/dist/{build-slots-Df1ApSuH.mjs → build-slots-41v_d2x9.mjs} +36 -21
  12. package/dist/{cli-0cX5s4NM.mjs → cli-Daxug5-K.mjs} +23 -17
  13. package/dist/cli.mjs +1 -1
  14. package/dist/collector-run.mjs +5 -192
  15. package/dist/{command-output-CliHHn7a.mjs → command-output-CDAIFD-8.mjs} +3 -3
  16. package/dist/{created-devices-BwVb3liH.mjs → created-devices-CpvzBL25.mjs} +1 -1
  17. package/dist/{dependency-state-BmsjKZ2H.mjs → dependency-state-CLcBHH2M.mjs} +1 -1
  18. package/dist/{deps-BFlKbOIp.mjs → deps-DNWaQs4V.mjs} +2 -2
  19. package/dist/{dev-client-CjzLoNs0.mjs → dev-client-B-XC4uRH.mjs} +3 -40
  20. package/dist/{device-BZ1IyS5A.mjs → device-C8IOoJPv.mjs} +7 -6
  21. package/dist/device-capacity-D0GWHnTM.mjs +580 -0
  22. package/dist/device-host-worker.mjs +587 -110
  23. package/dist/{device-ios-DMQBUPus.mjs → device-ios-Bc7vbGwl.mjs} +75 -44
  24. package/dist/{device-lease-W5Pi1q38.mjs → device-lease-DpoS36_u.mjs} +2 -2
  25. package/dist/{device-pool-B2r38YjS.mjs → device-lease-run-B8YltIH_.mjs} +5 -125
  26. package/dist/device-pool-3o7s5lo4.mjs +125 -0
  27. package/dist/{device-remote-9ulPsdhY.mjs → device-remote-hFSkpfQu.mjs} +13 -13
  28. package/dist/{doctor-BcE5xsBn.mjs → doctor-Cnk9d7Hm.mjs} +86 -59
  29. package/dist/{error-diagnostics-Cb0P1WLS.mjs → error-diagnostics-C-NMnhrk.mjs} +11 -15
  30. package/dist/{gc-BuL1Genr.mjs → gc-DxCL2A46.mjs} +478 -33
  31. package/dist/{gradle-CmdOBq1d.mjs → gradle-D7-L2Th3.mjs} +4 -4
  32. package/dist/{guide-cCSGRFd3.mjs → guide-DjrTQGik.mjs} +2198 -566
  33. package/dist/{guide-status-CIoWK6mG.mjs → guide-status-Du-ZQ3FD.mjs} +1 -1
  34. package/dist/hosted-android-DwLS4Mse.mjs +32 -0
  35. package/dist/hosted-client-wk1W_CNW.mjs +196 -0
  36. package/dist/hosted-ios-aMo7Cljf.mjs +15 -0
  37. package/dist/hosted-logs-DqYmWtXc.mjs +187 -0
  38. package/dist/hosted-macos-dlHXEeF5.mjs +135 -0
  39. package/dist/hosted-native-C9xJ9rpA.mjs +255 -0
  40. package/dist/{build-progress-C2Jv1AtA.mjs → idle-B2m-Ovod.mjs} +300 -9
  41. package/dist/{idle-shutdown-B5LPaNyY.mjs → idle-shutdown-_3oOV1V_.mjs} +25 -17
  42. package/dist/{in-use-CndPMilN.mjs → in-use-CvGOUn-x.mjs} +30 -10
  43. package/dist/ios-Cr1h5gnO.mjs +189 -0
  44. package/dist/{ios-Chob8ny7.mjs → ios-DE6duQoz.mjs} +819 -460
  45. package/dist/{ios-3CtCRhwr.mjs → ios-DSfnPfse.mjs} +4 -4
  46. package/dist/{ios-device-DERmMmk-.mjs → ios-device-pAN4t54-.mjs} +1 -1
  47. package/dist/ios-state-8L3lgum6.mjs +89 -0
  48. package/dist/{launch-verify-CLlofxRt.mjs → launch-verify-lTnWZikR.mjs} +3 -3
  49. package/dist/logs-DUoKlDoS.mjs +384 -0
  50. package/dist/{client-C2IS36Qf.mjs → machines-C2XiNwh9.mjs} +330 -37
  51. package/dist/{macos-BwtbUtTN.mjs → macos-32po3kUq.mjs} +23 -18
  52. package/dist/macos-run.mjs +1 -1
  53. package/dist/maintenance-run.d.mts +4 -0
  54. package/dist/maintenance-run.mjs +254 -0
  55. package/dist/{metro-Di8EN2Tb.mjs → metro-g8NqiJp-.mjs} +1 -1
  56. package/dist/metro-gateway-DmslUM30.mjs +103 -0
  57. package/dist/{named-ports-Dj_XBNaL.mjs → named-ports-C8m8EQs2.mjs} +2 -2
  58. package/dist/{native-run-DFDoIRbY.mjs → native-run-D1Q56rAD.mjs} +17 -6
  59. package/dist/{native-runtime-i6OkVg-w.mjs → native-runtime-DhJI3IVQ.mjs} +6 -6
  60. package/dist/{ndjson-CDY9IkVC.mjs → ndjson-PclOIwVY.mjs} +2 -2
  61. package/dist/offload-worker.d.mts +1 -0
  62. package/dist/offload-worker.mjs +13 -8
  63. package/dist/{teardown-BYuQafiA.mjs → ownership-BMLqyBgJ.mjs} +639 -53
  64. package/dist/{ownership-BkYxUtuj.mjs → ownership-DyRQru0q.mjs} +2 -2
  65. package/dist/{ports-DddwttKM.mjs → ports-4nYqxPWC.mjs} +2 -2
  66. package/dist/{prebuild-BvxDlD6C.mjs → prebuild-D7RUMkwN.mjs} +4 -4
  67. package/dist/preview-CxPRIEht.mjs +318 -0
  68. package/dist/{project-CV3tDFHl.mjs → project-D47Rh0ly.mjs} +72 -3
  69. package/dist/{recordings-GEXp4iBT.mjs → recordings-BNgsD1Tr.mjs} +1 -1
  70. package/dist/{reload-ChPiwvot.mjs → reload-qZei6iWn.mjs} +77 -13
  71. package/dist/{remote-cache-CO2elm_b.mjs → remote-cache-Ci4DQYYj.mjs} +4 -4
  72. package/dist/{run-B-xqRrsg.mjs → run-CkE9y0nc.mjs} +2 -2
  73. package/dist/{server-bare-X7PUgizL.mjs → server-bare-BkrSxSsZ.mjs} +1 -1
  74. package/dist/{server-expo-DhtX6bom.mjs → server-expo-CHTyXD0G.mjs} +4 -4
  75. package/dist/server-expo-CeakhI8P.mjs +2 -0
  76. package/dist/settings-BBpMcDbD.mjs +25 -0
  77. package/dist/settings-Cfd-0r2d.mjs +1440 -0
  78. package/dist/{settings-DbL6uhH8.mjs → settings-SR9OmW9h.mjs} +10 -4
  79. package/dist/settings.schema.json +472 -50
  80. package/dist/{simslim-Ciluxlq4.mjs → simslim-BWNrc3M3.mjs} +3 -3
  81. package/dist/{slot-launch-BY0BrOLh.mjs → slot-launch-DmjBmpQV.mjs} +10 -7
  82. package/dist/{spawn-entry-DncpJvWL.mjs → spawn-entry-jw19m8Nl.mjs} +1 -0
  83. package/dist/{start-CPO4wRg7.mjs → start-BopZcr4n.mjs} +1 -1
  84. package/dist/{start-CUD9nasA.mjs → start-uee1wWjY.mjs} +21 -17
  85. package/dist/{state-CaeaHbDA.mjs → state-BKa8wNN5.mjs} +1 -1
  86. package/dist/{state-BxCgeTQu.mjs → state-DyaNTgfR.mjs} +2 -2
  87. package/dist/{state-DFm77wRn.mjs → state-PeQiJuIu.mjs} +5 -5
  88. package/dist/{stats-DBLLFmKd.mjs → stats-Bq4fJOdB.mjs} +19 -5
  89. package/dist/{status-l84Gbvp2.mjs → status-CzS4bPO9.mjs} +5 -3
  90. package/dist/status-DlKvVJBE.mjs +49 -0
  91. package/dist/{status-DYXxNOT0.mjs → status-dXX2JX1C.mjs} +244 -50
  92. package/dist/{stop-CceYh1yG.mjs → stop-BCwvvs7F.mjs} +2 -2
  93. package/dist/{stop-Bad61z_2.mjs → stop-BjizrQwv.mjs} +83 -21
  94. package/dist/{stop-Bvf1MflS.mjs → stop-Cr1d8DSE.mjs} +4 -4
  95. package/dist/stop-cause-rhmGfwvt.mjs +97 -0
  96. package/dist/supervisor-run.d.mts +7 -4
  97. package/dist/supervisor-run.mjs +156 -27
  98. package/dist/{support-WLOzVKUv.mjs → support-BJPgYCu-.mjs} +61 -11
  99. package/dist/{support-CDQbDkL5.mjs → support-F9-sr0Oi.mjs} +6 -5
  100. package/dist/{toolchain-DmeYjXD9.mjs → toolchain-CNIBTIOv.mjs} +11 -11
  101. package/dist/trigger-Bemu8ytw.mjs +65 -0
  102. package/dist/trigger-CXts6qt1.mjs +2 -0
  103. package/dist/{warm-progress-DVHLN4uC.mjs → warm-progress-DF4JRvYI.mjs} +274 -5
  104. package/dist/watchman-APYjXSQa.mjs +48 -0
  105. package/dist/{web-k6w8-HnE.mjs → web-exQtGkoc.mjs} +14 -16
  106. package/dist/web-run.mjs +5 -5
  107. package/dist/{workspace-process-lock-BL7Q0HCO.mjs → workspace-process-lock-2mNzOlaB.mjs} +3 -2
  108. package/dist/{workspace-state-CL2V2Ons.mjs → workspace-state-DqcCbpE7.mjs} +34 -34
  109. package/dist/workspaces-Bcib97-6.mjs +306 -0
  110. package/dist/{worktree-BT_uULFx.mjs → worktree-Dc98cE6_.mjs} +1 -1
  111. package/dist/{worktree-qXKzADSC.mjs → worktree-ZpZR3S1k.mjs} +601 -33
  112. package/package.json +5 -5
  113. package/dist/device-capacity-DMuKC7nx.mjs +0 -136
  114. package/dist/hosted-macos-DwowNU7W.mjs +0 -447
  115. package/dist/logs-h9h6MUvE.mjs +0 -568
  116. package/dist/machine-usage-D_YYMzfR.mjs +0 -283
  117. package/dist/machines-CYtOCqcj.mjs +0 -259
  118. package/dist/merge-state-BnxLNQiM.mjs +0 -272
  119. package/dist/server-expo-KJeD109I.mjs +0 -2
  120. package/dist/settings-CduVcoxj.mjs +0 -680
  121. package/dist/stop-cause-CZ_Mr-v9.mjs +0 -665
  122. package/dist/watchman-Bx0wceoF.mjs +0 -238
  123. 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 { b as workspaceDir, i as recordWorkspaceUse, r as readWorkspaceState$1 } from "./workspace-state-CL2V2Ons.mjs";
6
- import { C as sourceCheckoutOf, S as restoreFile, T as warmWorktreePaths, _ as readWorktreeExclude, b as resolveFullRef, d as hasUncommittedWork, f as isPodInstallChurn, g as podsOutOfSync, h as locallyKnownUpstream, i as depsOutOfSync, l as hasPopulatedSubmodules, m as listWorktrees, n as cloneIgnoredEntries, o as dirtyPaths, r as deleteBranch, s as gitCommonDir, t as branchExists, u as hasRemote, v as removeWorktree, w as unpushedCommits } from "./worktree-C0c5Udwa.mjs";
7
- import { a as findProjectRoot$1, n as appProjectProblem } from "./project-CV3tDFHl.mjs";
8
- import { a as clearRemoteSession, c as readMetroTunnel, d as readRemoteSessionId, i as clearManagedMetroTunnel, u as readRemoteSession } from "./state-CaeaHbDA.mjs";
9
- import { a as formatElapsed, h as shortUdid, i as formatDuration, l as phaseLine, p as releasedLeaseFact, u as plural } from "./command-output-CliHHn7a.mjs";
10
- import { u as releaseWorkspaceLeases } from "./device-lease-W5Pi1q38.mjs";
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 { at as POOL_SETTING_REMEDY, dt as parkedMaxSetting } from "./android-Db2eEMaf.mjs";
13
- import { l as resolveProjectMetro, o as pidExists, r as killMetroTree, u as signalProcessTree } from "./metro-Di8EN2Tb.mjs";
14
- import { B as unknownSettingKeys, L as settingShapeErrors, N as resolveSettings, a as SETTING_SHAPE_REMEDY } from "./settings-CduVcoxj.mjs";
15
- import { a as teardownOwnedBrowser, i as teardownOwnedAvd, o as teardownOwnedIosSim } from "./teardown-BYuQafiA.mjs";
16
- import { t as clearNamedPorts } from "./named-ports-Dj_XBNaL.mjs";
17
- import { o as readWebRecord } from "./state-DFm77wRn.mjs";
18
- import { n as withIdleWorkspace, t as emptyWorkspaceDir } from "./in-use-CndPMilN.mjs";
19
- import { g as withManagedRemoteWorktreeRemovalLock, l as withdrawDevServerStopRequest, p as stopTunnel, s as requestDevServerStop, u as resolveSupervisorTarget, v as withManagedTunnelRemovalLock } from "./stop-cause-CZ_Mr-v9.mjs";
20
- import { n as verifyCollectorOwnership } from "./ownership-BkYxUtuj.mjs";
21
- import { C as podInstallCommand, u as HEARTBEAT_INTERVAL_MS } from "./toolchain-DmeYjXD9.mjs";
22
- import { t as stopMacosApp } from "./stop-Bvf1MflS.mjs";
23
- import { a as runPodInstall, i as runCaptured, n as podsAreStale, r as readPodState, t as DEPS_ERROR } from "./deps-BFlKbOIp.mjs";
24
- import { f as resolveEasCliBin } from "./remote-cache-CO2elm_b.mjs";
25
- import { i as endRecordedSession } from "./device-remote-9ulPsdhY.mjs";
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) console.error(chalk.dim(`${path} is inside the source checkout ${entry.path}; reclaiming its environment.`));
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, { preserveRootProject: true });
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 };