stim 1.20.2 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +141 -8
- package/dist/acquire-artifact-Co2k4f7O.mjs +171 -0
- package/dist/act-DsuqEkiM.mjs +2801 -0
- package/dist/activity-CMLmzp7o.mjs +1397 -0
- package/dist/{agent-device-usage-output-Dzgv6lgR.mjs → agent-device-usage-output-DUFaTMdd.mjs} +23 -67
- package/dist/{android-DCKZrNvV.mjs → android-ByRMUoiJ.mjs} +157 -93
- package/dist/{android-Mz_Inn3w.mjs → android-C7sVmyI9.mjs} +299 -116
- package/dist/{android-Dlzxshgp.mjs → android-DGkNqa_c.mjs} +5 -5
- package/dist/{android-HqP1_vB_.mjs → android-DR5lZ2Az.mjs} +2 -2
- package/dist/android-cas-compiler.mjs +2 -2
- package/dist/{android-cas-Bl-cLJBk.mjs → android-cas-ngferVq2.mjs} +7 -6
- package/dist/android-project-BCAAmMwz.mjs +12 -0
- package/dist/android-project-DVWyCbWU.d.mts +391 -0
- package/dist/api.d.mts +810 -64
- package/dist/api.mjs +782 -1
- package/dist/{artifact-HSwkIWEQ.mjs → artifact-8EY8SAPw.mjs} +240 -213
- package/dist/{artifact-D3czlUvT.mjs → artifact-DJPy518Y.mjs} +203 -164
- package/dist/artifact-platform-CXcmX_j3.mjs +27 -0
- package/dist/{browser-web-CA-F1GQo.mjs → browser-web-Bcv99PBM.mjs} +194 -64
- package/dist/budget-Cvwn54qV.mjs +9285 -0
- package/dist/{build-DYDbfkk5.mjs → build-CabC3G23.mjs} +153 -98
- package/dist/build-cache-Dv7RN200.mjs +602 -0
- package/dist/build-plan-DUC8Ka9y.mjs +248 -0
- package/dist/{cache-manifest-XH-fvDUj.mjs → cache-manifest-DSeQjt-Q.mjs} +2 -2
- package/dist/cache-manifest.mjs +1 -1
- package/dist/{ccache-zdKiYQ3E.mjs → ccache-Btwc36-I.mjs} +10 -9
- package/dist/{cdp-Dq_ypAlJ.mjs → cdp-3fwICjzi.mjs} +18 -5
- package/dist/{chrome-BnzZMqsR.mjs → chrome-Bbw689-6.mjs} +7 -4
- package/dist/{cli-CLP3diBm.mjs → cli-CHZa9Lvk.mjs} +20 -20
- package/dist/cli.mjs +1 -1
- package/dist/client-OyRoEuSr.mjs +1577 -0
- package/dist/collector-run.d.mts +4 -40
- package/dist/collector-run.mjs +9 -8
- package/dist/{command-output-BTy8hZ0F.mjs → command-output-gwpm28fu.mjs} +1 -1
- package/dist/config-DlrsduwG.mjs +955 -0
- package/dist/{created-devices-Dzd5C2LF.mjs → created-devices-x9rN6bhs.mjs} +14 -4
- package/dist/{dependency-state-DG0p31-y.mjs → dependency-state-1I9KZT1Y.mjs} +20 -5
- package/dist/{detached-entry-B9C_zeQC.mjs → detached-entry-BwNEtIrP.mjs} +1 -1
- package/dist/{dev-client-DAWVIGBu.mjs → dev-client-BSDR5h3F.mjs} +2 -2
- package/dist/{device-BECOiufl.mjs → device-B-L3Fbxy.mjs} +91 -34
- package/dist/device-DklWZLqb.mjs +1068 -0
- package/dist/{device-capacity-CBUAcRyQ.mjs → device-capacity-DK5_EwRS.mjs} +42 -13
- package/dist/device-host-worker.mjs +76 -79
- package/dist/{device-ios-COSi6oS1.mjs → device-ios-DoB6KFCO.mjs} +77 -51
- package/dist/{device-lease-run-Cw2M4z51.mjs → device-lease-run-xJPQ_QQG.mjs} +14 -26
- package/dist/{device-pool-Bf-pOhSw.mjs → device-pool-BMkdsefq.mjs} +5 -5
- package/dist/{doctor-kfMBnUw6.mjs → doctor-Bo7BXLux.mjs} +271 -320
- package/dist/{doctor-CB8oZKCk.mjs → doctor-D5_EzwfA.mjs} +186 -91
- package/dist/doctor-fingerprint-DZLoYFVb.mjs +16 -0
- package/dist/doctor-fingerprint.mjs +19 -0
- package/dist/doctor-thjWAV0z.mjs +2 -0
- package/dist/{doctor-watchman-BRyV2adA.mjs → doctor-watchman-N2wDotwQ.mjs} +40 -21
- package/dist/{eas-build-JyfqyHfC.mjs → eas-build-CwknaQYj.mjs} +53 -20
- package/dist/{build-plan-B7SZsc2N.mjs → eas-fallback-CSgAulr5.mjs} +41 -214
- package/dist/{watchman-CPQFZLXY.mjs → eas-session-ledger-DlY_serQ.mjs} +230 -11
- package/dist/{error-diagnostics-BR677IDM.mjs → error-diagnostics-DBWO2rxE.mjs} +172 -82
- package/dist/exec-DEoAhcze.mjs +858 -0
- package/dist/exec-DnYc4HGH.d.mts +78 -0
- package/dist/{gc-qWOMdnER.mjs → gc-CXxN-4SE.mjs} +1091 -1029
- package/dist/gc-DnNDQyaP.mjs +3 -0
- package/dist/{guide-G9cAbVAc.mjs → guide-Br0LEfKy.mjs} +1165 -297
- package/dist/{hosted-android-C3MHGld1.mjs → hosted-android-B-9f-jkd.mjs} +10 -8
- package/dist/{hosted-client-ZEKpS9vw.mjs → hosted-client-DZAPrcro.mjs} +34 -19
- package/dist/{hosted-ios-l-o3iM9J.mjs → hosted-ios-CsCS21hn.mjs} +1 -1
- package/dist/{hosted-logs-EzQM7-3Z.mjs → hosted-logs-FIRx9X2f.mjs} +77 -43
- package/dist/{hosted-macos-BACVAVqE.mjs → hosted-macos-_awYSpfv.mjs} +35 -22
- package/dist/{hosted-native-A4s1QPXe.mjs → hosted-native-BdzE668-.mjs} +49 -27
- package/dist/idle-C4cZ757K.d.mts +24 -0
- package/dist/{idle-shutdown-DtxhPTKe.mjs → idle-shutdown-DDg3SdvY.mjs} +11 -10
- package/dist/ios-CDvlmUs7.d.mts +24 -0
- package/dist/{ios-BwVCF7oA.mjs → ios-Ds1clbIt.mjs} +8 -8
- package/dist/{ios-device-D7pi2Sca.mjs → ios-device--h_-8ENa.mjs} +51 -26
- package/dist/{ios-device-B5veSDDQ.mjs → ios-device-BXZ0c7Kb.mjs} +1 -1
- package/dist/{ios-RkW4pnQf.mjs → ios-hcBpnEGk.mjs} +200 -93
- package/dist/{ios-Ce7bc7br.mjs → ios-pm4nL6By.mjs} +160 -108
- package/dist/{ios-project-K9hY_Jab.mjs → ios-project-CZhKODF3.mjs} +13 -13
- package/dist/ios-signing-BtAffm0A.mjs +489 -0
- package/dist/{ios-state-CBLe1xYw.mjs → ios-state-Dtw5K2aI.mjs} +1 -1
- package/dist/{ios-nOLJGVdy.mjs → ios-xtlRHaeB.mjs} +1 -1
- package/dist/{js-swap-Bb3nsS07.mjs → js-swap-Bwhh4S7V.mjs} +30 -25
- package/dist/{launch-C3qJ6Agb.mjs → launch-BjxemNpK.mjs} +85 -543
- package/dist/{launch-F4sOqAhB.mjs → launch-CCzXlc8r.mjs} +91 -90
- package/dist/logs-BfwwadsZ.mjs +479 -0
- package/dist/logs-jTS_lHSo.mjs +248 -0
- package/dist/{macos-D7uDvO-z.mjs → macos-DVlSSyUg.mjs} +1 -1
- package/dist/macos-NfV2D7dO.mjs +694 -0
- package/dist/macos-run.mjs +1 -1
- package/dist/maintenance-run.mjs +123 -248
- package/dist/{metro-DnCEjCxs.mjs → metro-DiXtA3Ty.mjs} +107 -30
- package/dist/metro-gateway-CKZqKqx7.mjs +209 -0
- package/dist/{named-ports-4WopRPg-.mjs → named-ports-DkEzOfzO.mjs} +42 -90
- package/dist/native-android-CqgnOgsT.mjs +645 -0
- package/dist/native-gradle-artifact-inputs-DHFnmrVf.mjs +270 -0
- package/dist/native-gradle-inputs-Dnh84Zbd.mjs +211 -0
- package/dist/native-inputs-TRTTTRxt.mjs +91 -0
- package/dist/native-run-NwUuxbkg.mjs +276 -0
- package/dist/{native-runtime-k8dTIrbK.mjs → native-runtime-B7KIafpM.mjs} +11 -9
- package/dist/native-source-Y0yLhFyQ.mjs +133 -0
- package/dist/{native-xcode-inputs-CykopJdM.mjs → native-xcode-inputs-D_VOfFL7.mjs} +26 -18
- package/dist/{native-xcode-ios-Bbf30Gm0.mjs → native-xcode-ios-BYDOHWLN.mjs} +104 -60
- package/dist/{ndjson-GOnbyvso.mjs → ndjson-CgE_o3Dl.mjs} +3 -3
- package/dist/offload-worker.d.mts +59 -41
- package/dist/offload-worker.mjs +478 -115
- package/dist/operation-exec-BW0em61G.mjs +171 -0
- package/dist/{ownership-DCtw_2IA.mjs → ownership-C5fo5Edb.mjs} +3 -3
- package/dist/{page-DRHy329N.mjs → page-DBL2PiUU.mjs} +12 -5
- package/dist/paths-BsJMoPP2.mjs +12 -0
- package/dist/paths-DQtTgsBf.mjs +42 -0
- package/dist/{placement-log-MOfPVgaM.mjs → placement-log-BnGgJRRZ.mjs} +5 -5
- package/dist/{plan-placement-V0TdzYXp.mjs → plan-placement-DIsMtN-d.mjs} +15 -11
- package/dist/{ports-DdSUicVo.mjs → ports-Cy1RkmuQ.mjs} +3 -3
- package/dist/prebuild-D4JoEAEs.mjs +484 -0
- package/dist/{project-_phNIJfs.mjs → project-wM_XFTvE.mjs} +65 -37
- package/dist/{pull-request-CQ8sPbZa.mjs → pull-request-W1IuLXDt.mjs} +102 -20
- package/dist/pull-request-eODyIqZF.d.mts +124 -0
- package/dist/pull-requests.d.mts +2 -120
- package/dist/pull-requests.mjs +2 -2
- package/dist/{react-native-android-CM1XRDwv.mjs → react-native-android-6OZ7MSM7.mjs} +121 -72
- package/dist/{gradle-DSrXb6lG.mjs → react-native-build-Dvd8hC21.mjs} +292 -15
- package/dist/react-native-doctor-DsoSF1Y9.mjs +234 -0
- package/dist/{react-native-ios-Dhq_eiYQ.mjs → react-native-ios-4u1Rn-GR.mjs} +71 -39
- package/dist/{recordings-D0d5aQt0.mjs → recordings-CQmfzAh7.mjs} +12 -9
- package/dist/registration-BcRgF6Ox.mjs +51 -0
- package/dist/{reload-C8JhxjSh.mjs → reload-BzkYabB-.mjs} +94 -47
- package/dist/reload-D7AVlb8N.mjs +2 -0
- package/dist/{remote-BiR6aYjy.mjs → remote-DjGDmlTZ.mjs} +26 -18
- package/dist/result-D2Ykz_iD.mjs +435 -0
- package/dist/{run-BtPgpKks.mjs → run-DidHghD1.mjs} +5 -5
- package/dist/selection-DSMbWEX2.mjs +620 -0
- package/dist/{server-bare-R2m_gsH8.mjs → server-bare-CafzuyFB.mjs} +3 -4
- package/dist/{server-command-CTtCkmcQ.mjs → server-command-C6QjeupS.mjs} +14 -7
- package/dist/{server-expo-DsguMGUF.mjs → server-expo-CrjB29pq.mjs} +54 -11
- package/dist/server-expo-D58c9L0M.mjs +2 -0
- package/dist/{settings-B717odTD.mjs → settings-C7O8A6ZI.mjs} +309 -119
- package/dist/settings-DpR-1Wnn.mjs +114 -0
- package/dist/settings-write-CzD60rOD.mjs +360 -0
- package/dist/settings.schema.json +176 -16
- package/dist/{simslim-CCr3rE-A.mjs → simslim-BNckWpXi.mjs} +14 -12
- package/dist/{slot-launch-DPt0m9Nd.mjs → slot-launch-CbfmNgM6.mjs} +15 -20
- package/dist/{spawn-entry-Dcb7sDUz.mjs → spawn-entry-jw19m8Nl.mjs} +0 -1
- package/dist/stage-Bps5oius.mjs +200 -0
- package/dist/{start-TXeMmmO2.mjs → start-Br-oxqMD.mjs} +1 -1
- package/dist/start-EojN4WFz.mjs +1364 -0
- package/dist/{state-D1Icyjlu.mjs → state--smjFSTL.mjs} +55 -13
- package/dist/{state-CG_OFoaA.mjs → state-08I3hw3_.mjs} +3 -2
- package/dist/{stats-BoY5sEPU.mjs → stats-D3GerlFW.mjs} +7 -13
- package/dist/stats-report-BzaPrRm6.mjs +16 -0
- package/dist/{status-BTA7kKiQ.mjs → status-BRROykC8.mjs} +7 -5
- package/dist/status-EvrK72L2.mjs +2 -0
- package/dist/{status-WgFbkRzj.mjs → status-WBDajew2.mjs} +554 -452
- package/dist/{status-watch-C23ILtf9.mjs → status-watch-Bg_u-pwR.mjs} +222 -25
- package/dist/status-worker.d.mts +1 -0
- package/dist/status-worker.mjs +19 -0
- package/dist/stim-desktop-D5OGpYZM.mjs +112 -0
- package/dist/{stop-DR6VoDIk.mjs → stop-DTZ9uTw3.mjs} +2 -2
- package/dist/{stop-DPEhunlw.mjs → stop-Dky10AEJ.mjs} +33 -23
- package/dist/{stop-BNmBMi1F.mjs → stop-Mv6W9FAk.mjs} +137 -98
- package/dist/supervisor-run.d.mts +8 -17
- package/dist/supervisor-run.mjs +56 -28
- package/dist/{support-CwjbFs9H.mjs → support-CNioewsd.mjs} +14 -14
- package/dist/{support-DMTqj3Hm.mjs → support-yUiOjnVY.mjs} +20 -13
- package/dist/swiftpm-artifact-inputs-DFcGsXDP.mjs +153 -0
- package/dist/swiftpm-macos-BIak-Rxv.mjs +532 -0
- package/dist/toolchain-BY8X8HXx.mjs +2 -0
- package/dist/toolchain-DpJMHq0n.mjs +876 -0
- package/dist/{trigger-CigAYjeL.mjs → trigger-Cbyc41d_.mjs} +21 -6
- package/dist/trigger-yb_nFzPF.mjs +2 -0
- package/dist/{warm-progress-BuY-Fvfn.mjs → warm-progress-CA3TgT3-.mjs} +263 -156
- package/dist/web-CZ07gd77.mjs +2 -0
- package/dist/{web-BDBMxDpx.mjs → web-DKx6yoFA.mjs} +20 -14
- package/dist/web-run.mjs +13 -12
- package/dist/windows-report.d.mts +1 -0
- package/dist/windows-report.mjs +18 -0
- package/dist/windows-report.ps1 +43 -0
- package/dist/{workspace-state-DJt_MMvf.mjs → workspace-state-U08dEzhG.mjs} +62 -58
- package/dist/{workspaces-C36s2jb9.mjs → workspaces-CpANmBlq.mjs} +1723 -1286
- package/dist/{worktree-CgvJYJ-Q.mjs → worktree-CHecZ4Oq.mjs} +1097 -547
- package/dist/worktree-ZqZVjD9K.mjs +2 -0
- package/dist/xcode-BXn9-xYx.mjs +896 -0
- package/package.json +7 -5
- package/shim/android-audio/Main.java +24 -0
- package/shim/android-audio/README.md +17 -0
- package/shim/android-audio.jar +0 -0
- package/shim/android-optimizations.gradle +7 -5
- package/dist/activity-BSV745Kz.mjs +0 -629
- package/dist/api-qvOzWyfO.mjs +0 -154
- package/dist/api-run.mjs +0 -294
- package/dist/app-install-BIKtMfPq.mjs +0 -896
- package/dist/artifact-lifecycle-DzPpBYvK.mjs +0 -30
- package/dist/budget-f7516Zk0.mjs +0 -4106
- package/dist/build-facts-DcLv2HSd.d.mts +0 -111
- package/dist/cancellation-C_22VQTH.mjs +0 -24
- package/dist/client-qQAaXSUs.mjs +0 -1771
- package/dist/config-BYDxlMY3.mjs +0 -562
- package/dist/device-DuiPJQSn.mjs +0 -290
- package/dist/device-lease-BX1sJQ2E.mjs +0 -1224
- package/dist/device-remote-xRT0Vmo9.mjs +0 -1272
- package/dist/eas-session-ledger-Cm0Gwq2F.mjs +0 -207
- package/dist/exec-DqKo9GKJ.mjs +0 -268
- package/dist/git-DDfFzkMm.mjs +0 -45
- package/dist/launch-verify-B6Q8hZP-.mjs +0 -428
- package/dist/logs-BGrEUaEs.mjs +0 -383
- package/dist/macos-Ch5VJmcS.mjs +0 -584
- package/dist/metro-gateway-CX9Xlkq2.mjs +0 -103
- package/dist/miss-reason-mKzZ4VvN.mjs +0 -238
- package/dist/native-android-D5qc3-_v.mjs +0 -226
- package/dist/native-gradle-inputs-BsLRrl7u.mjs +0 -349
- package/dist/ndjson-Du3j0AQ9.d.mts +0 -17
- package/dist/plan-C4TjdPsR.mjs +0 -260
- package/dist/prebuild-DYbucPXy.mjs +0 -212
- package/dist/preview-78a9kR_-.mjs +0 -1011
- package/dist/react-native-build-CtCccJtb.mjs +0 -239
- package/dist/react-native-doctor-CfRnFfjs.mjs +0 -78
- package/dist/remote-cache-OyOyVIS3.mjs +0 -526
- package/dist/result-CaJg5Iat.mjs +0 -186
- package/dist/server-expo-Bigriskl.mjs +0 -2
- package/dist/settings-BORNaaCO.mjs +0 -402
- package/dist/settings-vk3mJGSm.d.mts +0 -13
- package/dist/spawn-claims-DVS5RkMS.mjs +0 -72
- package/dist/stage-KNTQopqv.mjs +0 -157
- package/dist/start-BWqzODpb.mjs +0 -1116
- package/dist/stim-desktop-BzBN2Em2.mjs +0 -63
- package/dist/swiftpm-macos-DRNVJWlb.mjs +0 -119
- package/dist/tailnet-Dh-qiYzG.mjs +0 -265
- package/dist/toolchain-BFkAcstd.mjs +0 -2
- package/dist/toolchain-DxSsNssu.mjs +0 -1657
- package/dist/trigger-9_USgmxz.mjs +0 -2
- package/dist/web-Dq-i3wzJ.mjs +0 -2
- package/dist/worktree-DpLdCHga.mjs +0 -2
- package/dist/{api-run.d.mts → doctor-fingerprint.d.mts} +0 -0
- package/dist/{errors-DzkMYh8Q.mjs → errors-ccaPc4ox.mjs} +2 -2
|
@@ -1,43 +1,209 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { i as recordWorkspaceUse, r as readWorkspaceState$1
|
|
7
|
-
import { a as
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import { f as
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import { n as hasInstalledDependencies, r as installedNpmTreeIsValid, t as dependencyState } from "./dependency-state-
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import { LAST_BUILD_KEYS, archiveDir, archiveEnabled, archiveLock, archiveRoot, archivedUsage, countErrorEntries, endedAgentSessionOf, isJsonObject, listRecordedDevices, loadConfig, queryLogs, readArchives, readBuildHistory, readJsonObject, readLastBuilds, readMacosRecord, readPullRequestCache, readWorkspaceState, recordingEnabled, sortByTs } from "@stim-cli/core/state";
|
|
34
|
-
import { existsSync as existsSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1, readlinkSync as readlinkSync$1, realpathSync as realpathSync$1, renameSync as renameSync$1, rmSync as rmSync$1, symlinkSync as symlinkSync$1, unlinkSync as unlinkSync$1, writeFileSync as writeFileSync$1 } from "fs";
|
|
35
|
-
import { withDirLock, workspaceName } from "@stim-cli/core";
|
|
1
|
+
import { c as declareSpawnsOn, f as stopDeclaringSpawnsOn, g as process_identity_exports, m as operationState, n as isTimeoutError, r as WorkspaceProcessCleanupError, t as getExecutor, x as ownership_claim_exports } from "./exec-DEoAhcze.mjs";
|
|
2
|
+
import { d as workspaceDir, l as workspaceAgentDeviceDir } from "./paths-DQtTgsBf.mjs";
|
|
3
|
+
import { E as upsertProject, G as projectDeviceSlots, L as measuredDirectorySizeAsync, S as removeProject, _ as loadConfig$1, a as clearDevice, d as getConfigDir, g as isPathPrefix, j as acquireAvdClaim, p as getProject } from "./config-DlrsduwG.mjs";
|
|
4
|
+
import { a as formatElapsed, h as shortUdid, i as formatDuration, l as phaseLine, p as releasedLeaseFact, u as plural } from "./command-output-gwpm28fu.mjs";
|
|
5
|
+
import { c as gitQuiet, o as git, r as childEnvironment, s as gitAsync, t as runOperationFile } from "./operation-exec-BW0em61G.mjs";
|
|
6
|
+
import { i as recordWorkspaceUse, r as readWorkspaceState$1 } from "./workspace-state-U08dEzhG.mjs";
|
|
7
|
+
import { At as readWorktreeExclude, Bt as warmWorktreePaths, Ct as isPodInstallChurn, Dt as locallyKnownUpstream, Et as listWorktreesAsync, Ft as resolveFullRefAsync, K as resolveIosProjectDir, Lt as restoreFileAsync, Pt as resolveFullRef, Rt as sourceCheckoutOf, St as hasUncommittedWorkAsync, Y as resolveSettings, a as SETTING_SHAPE_REMEDY, bt as hasPopulatedSubmodulesAsync, dt as branchExistsAsync, et as settingShapeErrors, ft as cloneIgnoredEntries, gt as dirtyPathsAsync, it as unknownSettingKeys, jt as removeWorktreeAsync, kt as podsOutOfSync, mt as depsOutOfSync, pt as deleteBranchAsync, q as resolveProjectSettings, vt as gitCommonDirAsync, xt as hasRemoteAsync, yt as gitCommonDirOnDisk, z as readCommittedSettings, zt as unpushedCommitsAsync } from "./settings-C7O8A6ZI.mjs";
|
|
8
|
+
import { F as readRemoteSessionId, M as readMetroTunnel, O as clearManagedMetroTunnel, P as readRemoteSession, T as MODE_COMMAND, i as findProjectRoot$1, k as clearRemoteSession, o as isMobileProject } from "./project-wM_XFTvE.mjs";
|
|
9
|
+
import { t as macosAppPresent } from "./state-08I3hw3_.mjs";
|
|
10
|
+
import { c as pidExists, d as resolveProjectMetro, p as signalProcessTreeAsync, r as killMetroTreeAsync } from "./metro-DiXtA3Ty.mjs";
|
|
11
|
+
import { o as readWebRecord } from "./state--smjFSTL.mjs";
|
|
12
|
+
import { In as runProcessOperation, S as withdrawDevServerStopRequest, b as requestDevServerStop, br as discoverCaches, c as readVolumeSpace, l as readVolumeSpaceAsync, nt as planCacheEmptying, o as machineNumber, qn as resolveEasCliBin, u as resolveBudget, zt as endRecordedSession } from "./budget-Cvwn54qV.mjs";
|
|
13
|
+
import { t as canonicalPath } from "./paths-BsJMoPP2.mjs";
|
|
14
|
+
import { p as cacheUser } from "./native-run-NwUuxbkg.mjs";
|
|
15
|
+
import { D as teardownOwnedAvd, O as teardownOwnedBrowser, S as withManagedTunnelRemovalLock, _ as stopTunnel, a as collectWorkspaceOutputs, b as withManagedRemoteWorktreeRemovalLock, d as emptyWorkspaceDir, f as withIdleWorkspace, h as resolveSupervisorTarget, k as teardownOwnedIosSim } from "./workspaces-CpANmBlq.mjs";
|
|
16
|
+
import { D as releaseWorkspaceLeases, t as agentDeviceDaemon } from "./activity-CMLmzp7o.mjs";
|
|
17
|
+
import { _t as parkedMaxSetting, dt as POOL_SETTING_REMEDY } from "./android-C7sVmyI9.mjs";
|
|
18
|
+
import { F as readHostMemoryPressureAsync, P as readHostMemoryPressure } from "./ios-hcBpnEGk.mjs";
|
|
19
|
+
import { a as workspaceLinkLine, o as workspaceLinks } from "./stim-desktop-D5OGpYZM.mjs";
|
|
20
|
+
import { t as clearNamedPorts } from "./named-ports-DkEzOfzO.mjs";
|
|
21
|
+
import { r as HEARTBEAT_INTERVAL_MS, x as podInstallCommand } from "./xcode-BXn9-xYx.mjs";
|
|
22
|
+
import { t as resolveMaintenanceSettings } from "./settings-Djl-mpjX.mjs";
|
|
23
|
+
import { _ as runCaptured, h as readPodState, m as podsAreStale, p as DEPS_ERROR, v as runPodInstall } from "./toolchain-DpJMHq0n.mjs";
|
|
24
|
+
import { i as stopHostedAndroid } from "./hosted-android-B-9f-jkd.mjs";
|
|
25
|
+
import { r as stopHostedIos } from "./hosted-ios-CsCS21hn.mjs";
|
|
26
|
+
import { n as stopMacosApp } from "./stop-Dky10AEJ.mjs";
|
|
27
|
+
import { n as verifyCollectorOwnership } from "./ownership-C5fo5Edb.mjs";
|
|
28
|
+
import { a as discoverXcodeProject } from "./react-native-build-Dvd8hC21.mjs";
|
|
29
|
+
import { n as hasInstalledDependencies, r as installedNpmTreeIsValid, t as dependencyState } from "./dependency-state-1I9KZT1Y.mjs";
|
|
30
|
+
import { a as forgetStatusMeasures, p as mergeStateAsync, t as startWarmProgress } from "./warm-progress-CA3TgT3-.mjs";
|
|
31
|
+
import { o as pullRequestLookupAsync, r as endedPullRequest } from "./pull-request-W1IuLXDt.mjs";
|
|
32
|
+
import { r as workspaceRecordingEnabled } from "./recordings-CQmfzAh7.mjs";
|
|
36
33
|
import { claimRemoveCommand, clearFreeClaimSet, readClaimSet, releaseClaim, tryAcquireClaim } from "@stim-cli/core/ownership-claim";
|
|
37
34
|
import { createHash, randomUUID } from "node:crypto";
|
|
38
|
-
import {
|
|
35
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
36
|
+
import { cpus, freemem, homedir, loadavg, totalmem } from "node:os";
|
|
37
|
+
import { basename, dirname, join, relative } from "node:path";
|
|
38
|
+
import { buildCacheRoot, configDir, metroCacheRoot, unitLines, unitWords, withDirLockTransaction, workspaceName } from "@stim-cli/core";
|
|
39
|
+
import { LAST_BUILD_KEYS, archiveDir, archiveEnabled, archiveLock, archiveRoot, archivedUsage, buildWorkerRoot, countErrorEntries, endedAgentSessionOf, isJsonObject, listRecordedDevicesAsync, loadConfig, queryLogs, readArchivesAsync, readBuildHistory, readJsonObject, readLastBuilds, readMacosRecord, readPullRequestCache, readWorkspaceState, recordingEnabled, sharedCcache, sharedCompilationCache, sortByTs } from "@stim-cli/core/state";
|
|
40
|
+
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";
|
|
41
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
39
42
|
import chalk from "chalk";
|
|
43
|
+
import { existsSync as existsSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1, readlinkSync as readlinkSync$1, realpathSync as realpathSync$1, renameSync as renameSync$1, rmSync as rmSync$1, symlinkSync as symlinkSync$1, unlinkSync as unlinkSync$1, writeFileSync as writeFileSync$1 } from "fs";
|
|
44
|
+
import { chmod, lstat, mkdir, readdir, rename, rm, stat, utimes, writeFile } from "node:fs/promises";
|
|
40
45
|
import { createHash as createHash$1 } from "crypto";
|
|
46
|
+
//#region src/maintenance/pressure.ts
|
|
47
|
+
/**
|
|
48
|
+
* The free-disk floor of each level in MiB. Level 2 has no floor of its own: it runs when level 1 left free disk
|
|
49
|
+
* under the budget floor, and stops once free disk is back above it. Its urgent threshold only sets the reported level.
|
|
50
|
+
*/
|
|
51
|
+
function levelFloors(budget, settings) {
|
|
52
|
+
const floor = Math.max(budget.minFreeDiskMb, budget.hardFloorDiskMb);
|
|
53
|
+
return {
|
|
54
|
+
1: floor,
|
|
55
|
+
2: floor,
|
|
56
|
+
3: Math.max(settings.criticalFreeDiskGb * 1024, budget.hardFloorDiskMb),
|
|
57
|
+
urgent: settings.urgentFreeDiskGb * 1024
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function diskPressureLevel(freeMb, budget, settings) {
|
|
61
|
+
const floors = levelFloors(budget, settings);
|
|
62
|
+
if (freeMb < floors[3]) return 3;
|
|
63
|
+
if (freeMb < floors.urgent) return 2;
|
|
64
|
+
return freeMb < floors[1] ? 1 : 0;
|
|
65
|
+
}
|
|
66
|
+
function lowestFree(disk) {
|
|
67
|
+
return disk.length ? Math.min(...disk.map((volume) => volume.freeMb)) : Infinity;
|
|
68
|
+
}
|
|
69
|
+
/** Whether an action planned at a pressure level may still run with this much free disk. */
|
|
70
|
+
function levelStillShort(action, freeMb, budget, settings) {
|
|
71
|
+
return action.level === void 0 || freeMb < levelFloors(budget, settings)[action.level];
|
|
72
|
+
}
|
|
73
|
+
/** The protection window in milliseconds: protectRecentHours, or maintenance.pressureProtectMinutes for actions of level 2 and 3. */
|
|
74
|
+
function protectWindowMs(action, settings) {
|
|
75
|
+
return action.level === void 0 ? settings.protectRecentHours * 36e5 : settings.pressureProtectMinutes * 6e4;
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/maintenance/measure.ts
|
|
79
|
+
function measureDisk() {
|
|
80
|
+
return readVolumeSpace([
|
|
81
|
+
configDir(),
|
|
82
|
+
...Object.keys(loadConfig()?.projects ?? {}),
|
|
83
|
+
buildWorkerRoot(loadConfig())
|
|
84
|
+
]);
|
|
85
|
+
}
|
|
86
|
+
function measureDiskAsync(operation) {
|
|
87
|
+
return readVolumeSpaceAsync([
|
|
88
|
+
configDir(),
|
|
89
|
+
...Object.keys(loadConfig()?.projects ?? {}),
|
|
90
|
+
buildWorkerRoot(loadConfig())
|
|
91
|
+
], operation.signal);
|
|
92
|
+
}
|
|
93
|
+
/** The disk pressure level of the fullest volume Stim writes to, measured now. */
|
|
94
|
+
function currentDiskLevel(disk = measureDisk()) {
|
|
95
|
+
return diskPressureLevel(lowestFree(disk), resolveBudget().budget, resolveMaintenanceSettings());
|
|
96
|
+
}
|
|
97
|
+
function measurePressure(settings, previous, now) {
|
|
98
|
+
return pressureFromObservations(settings, previous, now, readHostMemoryPressure(), measureDisk());
|
|
99
|
+
}
|
|
100
|
+
async function measurePressureAsync(settings, previous, now, operation) {
|
|
101
|
+
return pressureFromObservations(settings, previous, now, await readHostMemoryPressureAsync(void 0, void 0, operation.signal, operation), await readVolumeSpaceAsync([
|
|
102
|
+
configDir(),
|
|
103
|
+
...Object.keys(loadConfig()?.projects ?? {}),
|
|
104
|
+
buildWorkerRoot(loadConfig())
|
|
105
|
+
], operation.signal));
|
|
106
|
+
}
|
|
107
|
+
function pressureFromObservations(settings, previous, now, level, disk) {
|
|
108
|
+
const availableBytes = process.platform === "darwin" ? null : freemem();
|
|
109
|
+
const warningSince = level === "warning" ? previous?.warningSince ?? now : null;
|
|
110
|
+
return {
|
|
111
|
+
disk,
|
|
112
|
+
memory: {
|
|
113
|
+
level,
|
|
114
|
+
availableBytes,
|
|
115
|
+
pressured: settings.memoryPressureLevel !== "off" && (level === "critical" || settings.memoryPressureLevel === "warning" && warningSince !== null && now - warningSince >= settings.memoryWarningMinutes * 6e4 || availableBytes !== null && availableBytes < (settings.minAvailableMemoryGb === void 0 ? totalmem() * .1 : settings.minAvailableMemoryGb * 1024 ** 3))
|
|
116
|
+
},
|
|
117
|
+
warningSince
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function sizeScanDeferred(settings) {
|
|
121
|
+
return loadavg()[0] / Math.max(1, cpus().length) > settings.maxLoadPerCore;
|
|
122
|
+
}
|
|
123
|
+
function cacheBlocked(entry, discovered = discoverCaches(), env = process.env) {
|
|
124
|
+
return planCacheEmptying([{
|
|
125
|
+
...entry,
|
|
126
|
+
note: "",
|
|
127
|
+
prune: "entries",
|
|
128
|
+
...discovered.some((cache) => canonicalPath(cache.dir) === canonicalPath(entry.dir) && cache.source === "registered") ? { source: "registered" } : {}
|
|
129
|
+
}], false, env)[0]?.machineGlobal ?? (entry.category === "compilation-cache" ? cacheUser(entry.dir) : void 0);
|
|
130
|
+
}
|
|
131
|
+
async function measureSizes(now, failure) {
|
|
132
|
+
const sizes = [];
|
|
133
|
+
const outputs = await collectWorkspaceOutputs({
|
|
134
|
+
olderThan: null,
|
|
135
|
+
now,
|
|
136
|
+
sizeTimeoutMs: 12e4
|
|
137
|
+
});
|
|
138
|
+
for (const entry of outputs.workspaces) {
|
|
139
|
+
if (entry.bytes === null) {
|
|
140
|
+
failure(entry.dir, entry.projectRoot ?? void 0);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
sizes.push({
|
|
144
|
+
name: "workspace build outputs",
|
|
145
|
+
category: "workspace-outputs",
|
|
146
|
+
dir: entry.dir,
|
|
147
|
+
bytes: entry.bytes,
|
|
148
|
+
measuredAt: now,
|
|
149
|
+
...entry.projectRoot ? { workspace: canonicalPath(entry.projectRoot) } : {},
|
|
150
|
+
idleDays: entry.idleDays,
|
|
151
|
+
...!entry.willClear ? { blocked: entry.keptReason ?? "workspace ownership is unresolved" } : {}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
const known = [
|
|
155
|
+
{
|
|
156
|
+
name: "shared build cache",
|
|
157
|
+
dir: buildCacheRoot(),
|
|
158
|
+
category: "build-cache"
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: "Metro transform caches",
|
|
162
|
+
dir: metroCacheRoot(),
|
|
163
|
+
category: "metro-cache"
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: "ccache",
|
|
167
|
+
dir: sharedCcache(),
|
|
168
|
+
category: "ccache"
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: "Swift compilation cache",
|
|
172
|
+
dir: sharedCompilationCache(),
|
|
173
|
+
category: "compilation-cache"
|
|
174
|
+
}
|
|
175
|
+
];
|
|
176
|
+
const discovered = discoverCaches();
|
|
177
|
+
for (const cache of discovered) {
|
|
178
|
+
if (cache.source !== "registered" && !canonicalPath(cache.dir).startsWith(`${canonicalPath(configDir())}/`)) continue;
|
|
179
|
+
if (cache.prune === "report-only") continue;
|
|
180
|
+
const dir = canonicalPath(cache.dir);
|
|
181
|
+
if (known.some((item) => dir === canonicalPath(item.dir) || dir.startsWith(`${canonicalPath(item.dir)}/`))) continue;
|
|
182
|
+
const category = cache.name.startsWith("Metro transform cache") ? "metro-cache" : /^(Expo build cache|native build cache|Build cache)$/.test(cache.name) ? "build-cache" : "other";
|
|
183
|
+
known.push({
|
|
184
|
+
name: cache.name,
|
|
185
|
+
dir: cache.dir,
|
|
186
|
+
category
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
for (const entry of known) {
|
|
190
|
+
if (!existsSync(entry.dir)) continue;
|
|
191
|
+
const bytes = await measuredDirectorySizeAsync(entry.dir, { timeoutMs: 12e4 });
|
|
192
|
+
if (bytes === null) {
|
|
193
|
+
failure(entry.dir);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const blocked = cacheBlocked(entry, discovered);
|
|
197
|
+
sizes.push({
|
|
198
|
+
...entry,
|
|
199
|
+
bytes,
|
|
200
|
+
measuredAt: now,
|
|
201
|
+
...blocked ? { blocked } : {}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
return sizes;
|
|
205
|
+
}
|
|
206
|
+
//#endregion
|
|
41
207
|
//#region src/workspace/pods-relocate.ts
|
|
42
208
|
const CHECKSUMS_HEADER = "SPEC CHECKSUMS:\n";
|
|
43
209
|
const GENERATED_DIRS = [
|
|
@@ -230,7 +396,7 @@ const KIND_PATH = {
|
|
|
230
396
|
recordings: "recordings",
|
|
231
397
|
agentActions: "agent-device/sessions"
|
|
232
398
|
};
|
|
233
|
-
function archiveSettings() {
|
|
399
|
+
function archiveSettings(env = process.env) {
|
|
234
400
|
const config = loadConfig();
|
|
235
401
|
return Object.fromEntries([
|
|
236
402
|
"maxAgeDays",
|
|
@@ -242,57 +408,63 @@ function archiveSettings() {
|
|
|
242
408
|
"recordings.maxTotalGb",
|
|
243
409
|
"agentActions.maxAgeDays"
|
|
244
410
|
].map((key) => {
|
|
245
|
-
const result = machineNumber(`archive.${key}`, config,
|
|
411
|
+
const result = machineNumber(`archive.${key}`, config, env);
|
|
246
412
|
if (result.error) throw new Error(result.error);
|
|
247
413
|
return [key, result.value];
|
|
248
414
|
}));
|
|
249
415
|
}
|
|
250
|
-
function enabled(root) {
|
|
416
|
+
function enabled(root, env) {
|
|
251
417
|
const config = loadConfig();
|
|
252
418
|
const project = config?.projects?.[root];
|
|
253
419
|
const present = existsSync(root);
|
|
254
420
|
const common = present ? gitCommonDirOnDisk(root) : null;
|
|
255
421
|
if (!present && Object.values(config?.repos ?? {}).some((repo) => repo.settings?.archive?.enabled === false)) return false;
|
|
256
|
-
return archiveEnabled(
|
|
422
|
+
return archiveEnabled(env, [
|
|
257
423
|
project?.settings,
|
|
258
424
|
common ? config?.repos?.[common]?.settings : void 0,
|
|
259
425
|
present ? readCommittedSettings(root) : void 0,
|
|
260
426
|
config
|
|
261
427
|
]);
|
|
262
428
|
}
|
|
263
|
-
function files(dir) {
|
|
429
|
+
async function files(dir, signal) {
|
|
430
|
+
signal?.throwIfAborted();
|
|
264
431
|
if (!existsSync(dir)) return [];
|
|
265
|
-
|
|
432
|
+
const found = [];
|
|
433
|
+
for (const name of await readdir(dir)) {
|
|
434
|
+
signal?.throwIfAborted();
|
|
266
435
|
const path = join(dir, name);
|
|
267
|
-
const
|
|
268
|
-
if (
|
|
269
|
-
|
|
436
|
+
const entry = await lstat(path);
|
|
437
|
+
if (entry.isSymbolicLink()) throw new Error(`refusing symlink ${path}`);
|
|
438
|
+
if (entry.isDirectory()) for (const file of await files(path, signal)) found.push(file);
|
|
439
|
+
else found.push({
|
|
270
440
|
path,
|
|
271
|
-
bytes:
|
|
272
|
-
at:
|
|
273
|
-
}
|
|
274
|
-
}
|
|
441
|
+
bytes: entry.size,
|
|
442
|
+
at: entry.mtimeMs
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
return found;
|
|
275
446
|
}
|
|
276
|
-
function secureTree(dir) {
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
for (const name of
|
|
447
|
+
async function secureTree(dir) {
|
|
448
|
+
if ((await lstat(dir)).isSymbolicLink()) throw new Error(`refusing symlink ${dir}`);
|
|
449
|
+
await chmod(dir, 448);
|
|
450
|
+
for (const name of await readdir(dir)) {
|
|
280
451
|
const path = join(dir, name);
|
|
281
|
-
const
|
|
282
|
-
if (
|
|
283
|
-
if (
|
|
284
|
-
else
|
|
452
|
+
const entry = await lstat(path);
|
|
453
|
+
if (entry.isSymbolicLink()) throw new Error(`refusing symlink ${path}`);
|
|
454
|
+
if (entry.isDirectory()) await secureTree(path);
|
|
455
|
+
else await chmod(path, 384);
|
|
285
456
|
}
|
|
286
457
|
}
|
|
287
|
-
function writeRecord(dir, record) {
|
|
288
|
-
|
|
458
|
+
async function writeRecord(dir, record) {
|
|
459
|
+
let small = 0;
|
|
460
|
+
for (const name of [
|
|
289
461
|
"state.json",
|
|
290
462
|
"ended-agents.json",
|
|
291
463
|
"log-error-index.json"
|
|
292
|
-
]
|
|
464
|
+
]) {
|
|
293
465
|
const path = join(dir, name);
|
|
294
|
-
|
|
295
|
-
}
|
|
466
|
+
if (existsSync(path)) small += (await stat(path)).size;
|
|
467
|
+
}
|
|
296
468
|
for (let pass = 0; pass < 8; pass++) {
|
|
297
469
|
record.bytes.total = KINDS.reduce((sum, kind) => sum + record.bytes[kind], record.bytes.record);
|
|
298
470
|
const bytes = small + Buffer.byteLength(`${JSON.stringify(record)}\n`);
|
|
@@ -302,59 +474,70 @@ function writeRecord(dir, record) {
|
|
|
302
474
|
record.bytes.total = KINDS.reduce((sum, kind) => sum + record.bytes[kind], record.bytes.record);
|
|
303
475
|
const tmp = join(dir, `.archive-${randomUUID()}.tmp`);
|
|
304
476
|
try {
|
|
305
|
-
|
|
306
|
-
|
|
477
|
+
await writeFile(tmp, `${JSON.stringify(record)}\n`, { mode: 384 });
|
|
478
|
+
await rename(tmp, join(dir, "archive.json"));
|
|
307
479
|
} finally {
|
|
308
|
-
|
|
480
|
+
await rm(tmp, { force: true });
|
|
309
481
|
}
|
|
310
482
|
const now = /* @__PURE__ */ new Date();
|
|
311
|
-
|
|
483
|
+
await utimes(archiveRoot(), now, now);
|
|
312
484
|
}
|
|
313
|
-
function removeRecord(id) {
|
|
485
|
+
async function removeRecord(id) {
|
|
314
486
|
const dir = archiveDir(id);
|
|
315
487
|
if (!existsSync(dir)) return;
|
|
316
488
|
const removing = join(archiveRoot(), `.removing-${id}`);
|
|
317
|
-
|
|
318
|
-
|
|
489
|
+
await rename(dir, removing);
|
|
490
|
+
await rm(removing, {
|
|
319
491
|
recursive: true,
|
|
320
492
|
force: true
|
|
321
493
|
});
|
|
322
494
|
}
|
|
323
|
-
function sweepArchiveStaging(remove, waitMs = 0) {
|
|
495
|
+
async function sweepArchiveStaging(remove, waitMs = 0, signal) {
|
|
496
|
+
signal?.throwIfAborted();
|
|
324
497
|
if (!existsSync(archiveRoot())) return [];
|
|
325
|
-
return
|
|
326
|
-
const
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
root
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
})
|
|
346
|
-
|
|
498
|
+
return withDirLockTransaction(archiveLock(), async () => {
|
|
499
|
+
const entries = [];
|
|
500
|
+
for (const name of await readdir(archiveRoot())) {
|
|
501
|
+
signal?.throwIfAborted();
|
|
502
|
+
if (!(name.startsWith(".incoming-") && (!name.endsWith(".claims") || !existsSync(join(archiveRoot(), name.slice(0, -7)))) || name.startsWith(".removing-"))) continue;
|
|
503
|
+
const path = join(archiveRoot(), name.endsWith(".claims") ? name.slice(0, -7) : name);
|
|
504
|
+
const root = `${path}.claims`;
|
|
505
|
+
const survey = name.startsWith(".incoming-") ? readClaimSet(root) : null;
|
|
506
|
+
const unresolved = survey?.unresolved[0];
|
|
507
|
+
const kept = Boolean(survey && (survey.live.length || survey.unresolved.length));
|
|
508
|
+
const entry = {
|
|
509
|
+
path,
|
|
510
|
+
kept,
|
|
511
|
+
reason: unresolved?.reason ?? (kept ? "live archive writer" : null),
|
|
512
|
+
removeCommand: unresolved ? claimRemoveCommand(root) : null
|
|
513
|
+
};
|
|
514
|
+
if (remove && !kept) {
|
|
515
|
+
if (!survey || clearFreeClaimSet({
|
|
516
|
+
root,
|
|
517
|
+
label: "archive staging"
|
|
518
|
+
}).status === "cleared") {
|
|
519
|
+
signal?.throwIfAborted();
|
|
520
|
+
await rm(path, {
|
|
521
|
+
recursive: true,
|
|
522
|
+
force: true
|
|
523
|
+
});
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
347
526
|
}
|
|
527
|
+
entries.push(entry);
|
|
348
528
|
}
|
|
349
|
-
return
|
|
350
|
-
}
|
|
529
|
+
return entries;
|
|
530
|
+
}, {
|
|
531
|
+
waitMs,
|
|
532
|
+
signal
|
|
533
|
+
});
|
|
351
534
|
}
|
|
352
535
|
function stripPids(value) {
|
|
353
536
|
if (Array.isArray(value)) return value.map(stripPids);
|
|
354
537
|
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).filter(([key]) => !/pids?$/i.test(key)).map(([key, child]) => [key, stripPids(child)]));
|
|
355
538
|
return value;
|
|
356
539
|
}
|
|
357
|
-
function gitFacts(root) {
|
|
540
|
+
async function gitFacts(root, operation) {
|
|
358
541
|
const empty = {
|
|
359
542
|
repository: null,
|
|
360
543
|
branch: null,
|
|
@@ -364,32 +547,55 @@ function gitFacts(root) {
|
|
|
364
547
|
pullRequest: null
|
|
365
548
|
};
|
|
366
549
|
if (!existsSync(root)) return cachedFacts(root) ?? empty;
|
|
367
|
-
const
|
|
550
|
+
const execution = {
|
|
551
|
+
...childEnvironment(operation?.env ?? process.env),
|
|
552
|
+
signal: operation?.signal,
|
|
553
|
+
declareFor: operation,
|
|
554
|
+
cwd: operation?.cwd,
|
|
555
|
+
killSignal: "SIGKILL"
|
|
556
|
+
};
|
|
557
|
+
const run = async (args) => {
|
|
558
|
+
try {
|
|
559
|
+
const result = await gitAsync(root, args, {
|
|
560
|
+
...execution,
|
|
561
|
+
timeoutMs: 1e3
|
|
562
|
+
});
|
|
563
|
+
operation?.signal.throwIfAborted();
|
|
564
|
+
return result;
|
|
565
|
+
} catch (error) {
|
|
566
|
+
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
567
|
+
operation?.signal.throwIfAborted();
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
};
|
|
368
571
|
const common = gitCommonDirOnDisk(root);
|
|
369
|
-
const summary = run([
|
|
572
|
+
const summary = (await run([
|
|
370
573
|
"show",
|
|
371
574
|
"-s",
|
|
372
575
|
"--format=%H%n%s",
|
|
373
576
|
"HEAD"
|
|
374
|
-
])?.split("\n");
|
|
375
|
-
const location = run([
|
|
577
|
+
]))?.split("\n");
|
|
578
|
+
const location = (await run([
|
|
376
579
|
"rev-parse",
|
|
377
580
|
"--show-toplevel",
|
|
378
581
|
"--abbrev-ref",
|
|
379
582
|
"HEAD"
|
|
380
|
-
])?.split("\n");
|
|
583
|
+
]))?.split("\n");
|
|
381
584
|
const branch = location?.[1] && location[1] !== "HEAD" ? location[1] : null;
|
|
382
|
-
const defaultRef = run([
|
|
585
|
+
const defaultRef = await run([
|
|
383
586
|
"symbolic-ref",
|
|
384
587
|
"--quiet",
|
|
385
588
|
"refs/remotes/origin/HEAD"
|
|
386
589
|
]);
|
|
387
590
|
let merged = null;
|
|
388
591
|
if (defaultRef?.startsWith("refs/remotes/")) {
|
|
389
|
-
const state =
|
|
592
|
+
const state = await mergeStateAsync(root, {
|
|
390
593
|
ref: defaultRef,
|
|
391
594
|
name: defaultRef.slice(13)
|
|
392
|
-
}, {
|
|
595
|
+
}, {
|
|
596
|
+
...execution,
|
|
597
|
+
timeoutMs: 2e3
|
|
598
|
+
});
|
|
393
599
|
merged = state.merged ? true : state.unknown ? null : false;
|
|
394
600
|
}
|
|
395
601
|
let prPath = root;
|
|
@@ -430,35 +636,48 @@ function cachedFacts(root) {
|
|
|
430
636
|
}
|
|
431
637
|
return null;
|
|
432
638
|
}
|
|
433
|
-
function
|
|
434
|
-
|
|
639
|
+
function archiveWarning(message, operation) {
|
|
640
|
+
if (operation) operation.output("stderr", `${message}\n`);
|
|
641
|
+
else console.error(message);
|
|
642
|
+
}
|
|
643
|
+
/** Publishes history once its first move starts; cancellation then skips retention and further cleanup. */
|
|
644
|
+
async function archiveWorkspace(root, removedBy, state = readWorkspaceState(root), operation) {
|
|
645
|
+
operation?.signal.throwIfAborted();
|
|
646
|
+
await writeArchive(root, removedBy, state, operation);
|
|
647
|
+
operation?.signal.throwIfAborted();
|
|
435
648
|
try {
|
|
436
|
-
enforceArchiveRetention();
|
|
649
|
+
await enforceArchiveRetention(void 0, operation);
|
|
437
650
|
} catch (error) {
|
|
438
|
-
|
|
651
|
+
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
652
|
+
operation?.signal.throwIfAborted();
|
|
653
|
+
archiveWarning(`could not enforce archive retention: ${String(error?.message ?? error).split("\n")[0]}`, operation);
|
|
439
654
|
}
|
|
440
655
|
}
|
|
441
|
-
function writeArchive(root, removedBy, state) {
|
|
656
|
+
async function writeArchive(root, removedBy, state, operation) {
|
|
657
|
+
const env = operation?.env ?? process.env;
|
|
442
658
|
let staging = null;
|
|
443
659
|
let claim;
|
|
444
660
|
try {
|
|
445
661
|
const dir = workspaceDir(root);
|
|
446
|
-
if (!existsSync(dir) ||
|
|
662
|
+
if (!existsSync(dir) || (await lstat(dir)).isSymbolicLink()) return;
|
|
447
663
|
if (existsSync(root)) root = realpathSync(root);
|
|
448
664
|
try {
|
|
449
|
-
sweepArchiveStaging(true, 5e3);
|
|
450
|
-
} catch {
|
|
451
|
-
|
|
452
|
-
|
|
665
|
+
await sweepArchiveStaging(true, 5e3, operation?.signal);
|
|
666
|
+
} catch {
|
|
667
|
+
operation?.signal.throwIfAborted();
|
|
668
|
+
}
|
|
669
|
+
if (!enabled(root, env)) return;
|
|
670
|
+
const limits = archiveSettings(env);
|
|
453
671
|
if (!limits.maxAgeDays || !limits.maxCount) return;
|
|
454
672
|
const logs = join(dir, "logs");
|
|
455
|
-
if (!existsSync(join(dir, "state.json")) && !Object.values(readBuildHistory(state)).some((entries) => entries?.length) && (!existsSync(logs) ||
|
|
456
|
-
const worktree = gitFacts(root);
|
|
457
|
-
|
|
673
|
+
if (!existsSync(join(dir, "state.json")) && !Object.values(readBuildHistory(state)).some((entries) => entries?.length) && (!existsSync(logs) || (await readdir(logs)).length === 0)) return;
|
|
674
|
+
const worktree = await gitFacts(root, operation);
|
|
675
|
+
operation?.signal.throwIfAborted();
|
|
676
|
+
await mkdir(archiveRoot(), {
|
|
458
677
|
recursive: true,
|
|
459
678
|
mode: 448
|
|
460
679
|
});
|
|
461
|
-
|
|
680
|
+
await chmod(archiveRoot(), 448);
|
|
462
681
|
const token = randomUUID();
|
|
463
682
|
staging = join(archiveRoot(), `.incoming-${token}`);
|
|
464
683
|
const attempt = tryAcquireClaim({
|
|
@@ -471,49 +690,56 @@ function writeArchive(root, removedBy, state) {
|
|
|
471
690
|
releaseClaim(attempt.pending);
|
|
472
691
|
throw new Error("archive staging claim is held");
|
|
473
692
|
}
|
|
474
|
-
|
|
475
|
-
|
|
693
|
+
await withDirLockTransaction(archiveLock(), async () => {
|
|
694
|
+
operation?.signal.throwIfAborted();
|
|
695
|
+
let movedHistory = false;
|
|
696
|
+
const moveHistory = async (from, to) => {
|
|
697
|
+
if (!movedHistory) operation?.signal.throwIfAborted();
|
|
698
|
+
await rename(from, to);
|
|
699
|
+
movedHistory = true;
|
|
700
|
+
};
|
|
701
|
+
await mkdir(staging, { mode: 448 });
|
|
476
702
|
let at = Date.now();
|
|
477
703
|
const workspace = workspaceName(root);
|
|
478
704
|
while (existsSync(archiveDir(`${workspace}--${at}`))) at++;
|
|
479
705
|
const id = `${workspace}--${at}`;
|
|
480
706
|
if (state) {
|
|
481
707
|
const { supervisor: _supervisor, collectors: _collectors, warm: _warm, ...stable } = state;
|
|
482
|
-
|
|
708
|
+
await writeFile(join(staging, "state.json"), `${JSON.stringify(stripPids(stable))}\n`, { mode: 384 });
|
|
483
709
|
}
|
|
484
710
|
for (const name of ["ended-agents.json", "log-error-index.json"]) {
|
|
485
711
|
const data = readJsonObject(join(dir, name));
|
|
486
|
-
if (data)
|
|
712
|
+
if (data) await writeFile(join(staging, name), `${JSON.stringify(stripPids(data))}\n`, { mode: 384 });
|
|
487
713
|
}
|
|
488
|
-
const move = (kind) => {
|
|
714
|
+
const move = async (kind) => {
|
|
489
715
|
const from = join(dir, KIND_PATH[kind]);
|
|
490
716
|
if (!limits[`${kind}.maxAgeDays`] || !existsSync(from)) return;
|
|
491
717
|
const to = join(staging, KIND_PATH[kind]);
|
|
492
|
-
|
|
718
|
+
await mkdir(join(to, ".."), {
|
|
493
719
|
recursive: true,
|
|
494
720
|
mode: 448
|
|
495
721
|
});
|
|
496
|
-
|
|
497
|
-
secureTree(to);
|
|
722
|
+
await moveHistory(from, to);
|
|
723
|
+
await secureTree(to);
|
|
498
724
|
};
|
|
499
|
-
move("logs");
|
|
500
|
-
move("agentActions");
|
|
725
|
+
await move("logs");
|
|
726
|
+
await move("agentActions");
|
|
501
727
|
const config = loadConfig();
|
|
502
|
-
if (limits["recordings.maxAgeDays"] && workspaceRecordingEnabled(root, config?.projects?.[root] ?? {}, config,
|
|
728
|
+
if (limits["recordings.maxAgeDays"] && workspaceRecordingEnabled(root, config?.projects?.[root] ?? {}, config, env)) {
|
|
503
729
|
const recordingDir = join(dir, "recordings");
|
|
504
|
-
if (existsSync(recordingDir) &&
|
|
505
|
-
for (const device of
|
|
506
|
-
if (
|
|
730
|
+
if (existsSync(recordingDir) && (await lstat(recordingDir)).isSymbolicLink()) throw new Error(`refusing symlink ${recordingDir}`);
|
|
731
|
+
for (const device of await listRecordedDevicesAsync(recordingDir, false, movedHistory ? void 0 : operation?.signal)) {
|
|
732
|
+
if ((await lstat(device.dir)).isSymbolicLink()) throw new Error(`refusing symlink ${device.dir}`);
|
|
507
733
|
const to = join(staging, "recordings", `${device.platform}-${device.slot}`);
|
|
508
734
|
for (const segment of device.segments.filter((entry) => !entry.open)) {
|
|
509
|
-
|
|
735
|
+
await mkdir(to, {
|
|
510
736
|
recursive: true,
|
|
511
737
|
mode: 448
|
|
512
738
|
});
|
|
513
739
|
try {
|
|
514
|
-
if (
|
|
515
|
-
|
|
516
|
-
|
|
740
|
+
if ((await lstat(segment.file)).isSymbolicLink()) throw new Error(`refusing symlink ${segment.file}`);
|
|
741
|
+
await moveHistory(segment.file, join(to, basename(segment.file)));
|
|
742
|
+
await chmod(join(to, basename(segment.file)), 384);
|
|
517
743
|
} catch (error) {
|
|
518
744
|
if (error.code !== "ENOENT") throw error;
|
|
519
745
|
}
|
|
@@ -542,7 +768,7 @@ function writeArchive(root, removedBy, state) {
|
|
|
542
768
|
record: new Date(at + limits.maxAgeDays * DAY).toISOString()
|
|
543
769
|
};
|
|
544
770
|
for (const kind of KINDS) {
|
|
545
|
-
bytes[kind] = files(join(staging, KIND_PATH[kind])).reduce((sum, file) => sum + file.bytes, 0);
|
|
771
|
+
bytes[kind] = (await files(join(staging, KIND_PATH[kind]))).reduce((sum, file) => sum + file.bytes, 0);
|
|
546
772
|
if (bytes[kind]) expires[kind] = new Date(at + limits[`${kind}.maxAgeDays`] * DAY).toISOString();
|
|
547
773
|
}
|
|
548
774
|
const record = {
|
|
@@ -564,102 +790,144 @@ function writeArchive(root, removedBy, state) {
|
|
|
564
790
|
expires,
|
|
565
791
|
version: 1
|
|
566
792
|
};
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
793
|
+
if (!movedHistory) operation?.signal.throwIfAborted();
|
|
794
|
+
await writeRecord(staging, record);
|
|
795
|
+
await rename(staging, archiveDir(id));
|
|
796
|
+
}, {
|
|
797
|
+
waitMs: 3e4,
|
|
798
|
+
signal: operation?.signal
|
|
799
|
+
});
|
|
570
800
|
releaseClaim(claim);
|
|
571
801
|
claim = void 0;
|
|
572
802
|
staging = null;
|
|
573
803
|
} catch (error) {
|
|
804
|
+
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
574
805
|
if (staging) try {
|
|
575
|
-
|
|
806
|
+
await rm(staging, {
|
|
576
807
|
recursive: true,
|
|
577
808
|
force: true
|
|
578
809
|
});
|
|
579
810
|
} catch {}
|
|
580
|
-
|
|
811
|
+
operation?.signal.throwIfAborted();
|
|
812
|
+
archiveWarning(`could not archive: ${String(error?.message ?? error).split("\n")[0]}`, operation);
|
|
581
813
|
} finally {
|
|
582
814
|
releaseClaim(claim);
|
|
583
815
|
}
|
|
584
816
|
}
|
|
585
|
-
function removeKind(record, kind) {
|
|
586
|
-
|
|
817
|
+
async function removeKind(record, kind) {
|
|
818
|
+
await rm(join(archiveDir(record.id), KIND_PATH[kind]), {
|
|
587
819
|
recursive: true,
|
|
588
820
|
force: true
|
|
589
821
|
});
|
|
590
822
|
record.bytes[kind] = 0;
|
|
591
823
|
record.expires[kind] = null;
|
|
592
|
-
writeRecord(archiveDir(record.id), record);
|
|
824
|
+
await writeRecord(archiveDir(record.id), record);
|
|
593
825
|
}
|
|
594
826
|
function logRemovalOrder(path) {
|
|
595
827
|
return /\.ndjson\.\d+$/.test(path) ? 0 : /^build-.*\.ndjson$/.test(basename(path)) ? 1 : 2;
|
|
596
828
|
}
|
|
597
|
-
function enforceArchiveRetention(now = Date.now()) {
|
|
829
|
+
async function enforceArchiveRetention(now = Date.now(), operation) {
|
|
830
|
+
const signal = operation?.signal;
|
|
831
|
+
const env = operation?.env ?? process.env;
|
|
832
|
+
signal?.throwIfAborted();
|
|
598
833
|
if (!existsSync(archiveRoot())) return;
|
|
599
834
|
let limits;
|
|
600
835
|
try {
|
|
601
|
-
limits = archiveSettings();
|
|
836
|
+
limits = archiveSettings(env);
|
|
602
837
|
} catch (error) {
|
|
603
|
-
|
|
838
|
+
archiveWarning(`could not enforce archive retention: ${String(error.message).split("\n")[0]}`, operation);
|
|
604
839
|
return;
|
|
605
840
|
}
|
|
606
|
-
|
|
607
|
-
const listed =
|
|
841
|
+
await withDirLockTransaction(archiveLock(), async () => {
|
|
842
|
+
const listed = (await readArchivesAsync(signal)).toReversed();
|
|
608
843
|
const records = listed.filter((record, index) => limits.maxAgeDays > 0 && now - Date.parse(record.removedAt) < limits.maxAgeDays * DAY && index >= listed.length - limits.maxCount);
|
|
609
|
-
for (const record of listed)
|
|
610
|
-
|
|
844
|
+
for (const record of listed) {
|
|
845
|
+
signal?.throwIfAborted();
|
|
846
|
+
if (!records.includes(record)) await removeRecord(record.id);
|
|
847
|
+
}
|
|
848
|
+
const recordings = recordingEnabled(env, [loadConfig()]);
|
|
611
849
|
for (const record of records) {
|
|
850
|
+
signal?.throwIfAborted();
|
|
612
851
|
for (const kind of [
|
|
613
852
|
"recordings",
|
|
614
853
|
"agentActions",
|
|
615
854
|
"logs"
|
|
616
855
|
]) {
|
|
856
|
+
signal?.throwIfAborted();
|
|
617
857
|
const age = limits[`${kind}.maxAgeDays`];
|
|
618
|
-
if (record.bytes[kind] && (!age || now - Date.parse(record.removedAt) >= age * DAY || kind === "recordings" && !recordings)) removeKind(record, kind);
|
|
858
|
+
if (record.bytes[kind] && (!age || now - Date.parse(record.removedAt) >= age * DAY || kind === "recordings" && !recordings)) await removeKind(record, kind);
|
|
619
859
|
}
|
|
620
860
|
const cap = limits["logs.maxMbPerWorkspace"] * 1024 ** 2;
|
|
621
861
|
if (record.bytes.logs > cap) {
|
|
622
|
-
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);
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
862
|
+
const entries = (await files(join(archiveDir(record.id), "logs"), signal)).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);
|
|
863
|
+
let removed = false;
|
|
864
|
+
try {
|
|
865
|
+
for (const file of entries) {
|
|
866
|
+
signal?.throwIfAborted();
|
|
867
|
+
if (record.bytes.logs <= cap) break;
|
|
868
|
+
await rm(file.path);
|
|
869
|
+
record.bytes.logs = Math.max(0, record.bytes.logs - file.bytes);
|
|
870
|
+
removed = true;
|
|
871
|
+
}
|
|
872
|
+
} finally {
|
|
873
|
+
if (removed) {
|
|
874
|
+
if (!record.bytes.logs) record.expires.logs = null;
|
|
875
|
+
await writeRecord(archiveDir(record.id), record);
|
|
876
|
+
}
|
|
627
877
|
}
|
|
628
|
-
if (!record.bytes.logs) record.expires.logs = null;
|
|
629
|
-
writeRecord(archiveDir(record.id), record);
|
|
630
878
|
}
|
|
631
879
|
}
|
|
632
880
|
const totalCap = limits.maxTotalGb * 1024 ** 3;
|
|
633
881
|
const recordingCap = limits["recordings.maxTotalGb"] * 1024 ** 3;
|
|
634
|
-
for (const record of records)
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
882
|
+
for (const record of records) {
|
|
883
|
+
signal?.throwIfAborted();
|
|
884
|
+
for (;;) {
|
|
885
|
+
signal?.throwIfAborted();
|
|
886
|
+
const usage = archivedUsage(records);
|
|
887
|
+
const totalOver = usage.bytes > totalCap;
|
|
888
|
+
const recordingsOver = usage.byKind.recordings > recordingCap;
|
|
889
|
+
if (!totalOver && !recordingsOver) break;
|
|
890
|
+
const candidates = KINDS.filter((kind) => record.bytes[kind] > 0 && (totalOver || kind === "recordings")).toSorted((a, b) => record.bytes[b] - record.bytes[a]);
|
|
891
|
+
if (!candidates.length) break;
|
|
892
|
+
await removeKind(record, candidates[0]);
|
|
893
|
+
}
|
|
642
894
|
}
|
|
643
895
|
for (const record of records) {
|
|
896
|
+
signal?.throwIfAborted();
|
|
644
897
|
const before = JSON.stringify(record.expires);
|
|
645
898
|
for (const kind of KINDS) record.expires[kind] = record.bytes[kind] ? new Date(Date.parse(record.removedAt) + limits[`${kind}.maxAgeDays`] * DAY).toISOString() : null;
|
|
646
899
|
record.expires.record = new Date(Date.parse(record.removedAt) + limits.maxAgeDays * DAY).toISOString();
|
|
647
|
-
if (JSON.stringify(record.expires) !== before) writeRecord(archiveDir(record.id), record);
|
|
900
|
+
if (JSON.stringify(record.expires) !== before) await writeRecord(archiveDir(record.id), record);
|
|
648
901
|
}
|
|
649
|
-
}, {
|
|
902
|
+
}, {
|
|
903
|
+
waitMs: 3e4,
|
|
904
|
+
signal
|
|
905
|
+
});
|
|
650
906
|
}
|
|
651
|
-
function deleteSelectedArchives(ids, kind) {
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
907
|
+
async function deleteSelectedArchives(ids, kind, signal) {
|
|
908
|
+
signal?.throwIfAborted();
|
|
909
|
+
await withDirLockTransaction(archiveLock(), async () => {
|
|
910
|
+
for (const record of (await readArchivesAsync(signal)).filter((entry) => ids.includes(entry.id))) {
|
|
911
|
+
signal?.throwIfAborted();
|
|
912
|
+
if (kind) await removeKind(record, kind);
|
|
913
|
+
else await removeRecord(record.id);
|
|
914
|
+
}
|
|
915
|
+
}, {
|
|
916
|
+
waitMs: 3e4,
|
|
917
|
+
signal
|
|
918
|
+
});
|
|
656
919
|
}
|
|
657
920
|
//#endregion
|
|
658
921
|
//#region src/devices/reclaim.ts
|
|
659
|
-
|
|
922
|
+
function throwIfCleanupInterrupted(error, operation) {
|
|
923
|
+
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
924
|
+
operation?.signal.throwIfAborted();
|
|
925
|
+
}
|
|
926
|
+
async function reapCollectors(root, { verify = verifyCollectorOwnership, collectors, operation }) {
|
|
660
927
|
const skippedDevices = [];
|
|
661
928
|
const failedDevices = [];
|
|
662
929
|
for (const [platform, record] of Object.entries(collectors)) {
|
|
930
|
+
operation?.signal.throwIfAborted();
|
|
663
931
|
const rec = record;
|
|
664
932
|
const pid = rec?.pid;
|
|
665
933
|
if (typeof pid !== "number" || pid <= 0 || pid === process.pid || !pidExists(pid)) continue;
|
|
@@ -683,7 +951,7 @@ async function reapCollectors(root, { verify = verifyCollectorOwnership, collect
|
|
|
683
951
|
}
|
|
684
952
|
try {
|
|
685
953
|
process.kill(pid, "SIGTERM");
|
|
686
|
-
if (await (0, process_identity_exports.waitForProcessExit)(rec, 5e3)) continue;
|
|
954
|
+
if (await (0, process_identity_exports.waitForProcessExit)(rec, 5e3, operation?.signal)) continue;
|
|
687
955
|
} catch (error) {
|
|
688
956
|
if (error.code === "ESRCH") continue;
|
|
689
957
|
}
|
|
@@ -700,7 +968,7 @@ async function reapCollectors(root, { verify = verifyCollectorOwnership, collect
|
|
|
700
968
|
failedDevices
|
|
701
969
|
};
|
|
702
970
|
}
|
|
703
|
-
function reclaimRemoteSession(root, { stopSession = defaultStopSession } = {}) {
|
|
971
|
+
async function reclaimRemoteSession(root, { stopSession = defaultStopSession, operation } = {}) {
|
|
704
972
|
const sessionId = readRemoteSessionId(root);
|
|
705
973
|
if (!sessionId) return {
|
|
706
974
|
stopped: null,
|
|
@@ -708,8 +976,10 @@ function reclaimRemoteSession(root, { stopSession = defaultStopSession } = {}) {
|
|
|
708
976
|
};
|
|
709
977
|
let result;
|
|
710
978
|
try {
|
|
711
|
-
result = stopSession(root, sessionId);
|
|
979
|
+
result = await stopSession(root, sessionId, operation);
|
|
980
|
+
operation?.signal.throwIfAborted();
|
|
712
981
|
} catch (err) {
|
|
982
|
+
throwIfCleanupInterrupted(err, operation);
|
|
713
983
|
result = {
|
|
714
984
|
status: "failed",
|
|
715
985
|
reason: String(err?.message ?? err)
|
|
@@ -739,17 +1009,18 @@ function reclaimRemoteSession(root, { stopSession = defaultStopSession } = {}) {
|
|
|
739
1009
|
}
|
|
740
1010
|
};
|
|
741
1011
|
}
|
|
742
|
-
function defaultStopSession(root, sessionId) {
|
|
1012
|
+
function defaultStopSession(root, sessionId, operation) {
|
|
743
1013
|
return endRecordedSession({
|
|
744
1014
|
root,
|
|
745
1015
|
sessionId,
|
|
746
|
-
easBin: resolveEasCliBin(root)?.file ?? null
|
|
1016
|
+
easBin: resolveEasCliBin(root)?.file ?? null,
|
|
1017
|
+
operation
|
|
747
1018
|
});
|
|
748
1019
|
}
|
|
749
|
-
function defaultStopMetroTunnel(record) {
|
|
750
|
-
return stopTunnel(record);
|
|
1020
|
+
function defaultStopMetroTunnel(record, operation) {
|
|
1021
|
+
return stopTunnel(record, { operation });
|
|
751
1022
|
}
|
|
752
|
-
async function reclaimMetroTunnel(root, { stopMetroTunnel = defaultStopMetroTunnel } = {}) {
|
|
1023
|
+
async function reclaimMetroTunnel(root, { stopMetroTunnel = defaultStopMetroTunnel, operation } = {}) {
|
|
753
1024
|
const record = readMetroTunnel(root);
|
|
754
1025
|
if (record?.kind !== "managed") return {
|
|
755
1026
|
stopped: null,
|
|
@@ -757,8 +1028,10 @@ async function reclaimMetroTunnel(root, { stopMetroTunnel = defaultStopMetroTunn
|
|
|
757
1028
|
};
|
|
758
1029
|
let result;
|
|
759
1030
|
try {
|
|
760
|
-
result = await stopMetroTunnel(record);
|
|
1031
|
+
result = await stopMetroTunnel(record, operation);
|
|
1032
|
+
operation?.signal.throwIfAborted();
|
|
761
1033
|
} catch (err) {
|
|
1034
|
+
throwIfCleanupInterrupted(err, operation);
|
|
762
1035
|
result = {
|
|
763
1036
|
status: "failed",
|
|
764
1037
|
reason: String(err?.message ?? err)
|
|
@@ -814,7 +1087,7 @@ function parkRequest(project, projectPath, slot, simslimManaged) {
|
|
|
814
1087
|
cacheKey: lastBuildCacheKey(projectPath, "ios")
|
|
815
1088
|
};
|
|
816
1089
|
}
|
|
817
|
-
function reclaimOwnedDevices(project, projectPath, { park = false } = {}) {
|
|
1090
|
+
async function reclaimOwnedDevices(project, projectPath, { park = false, operation } = {}) {
|
|
818
1091
|
const deletedDevices = [];
|
|
819
1092
|
const parkedDevices = [];
|
|
820
1093
|
const evictedDevices = [];
|
|
@@ -822,11 +1095,13 @@ function reclaimOwnedDevices(project, projectPath, { park = false } = {}) {
|
|
|
822
1095
|
const skippedDevices = [];
|
|
823
1096
|
const failedDevices = [];
|
|
824
1097
|
for (const { slot, platforms } of projectDeviceSlots(project)) {
|
|
1098
|
+
operation?.signal.throwIfAborted();
|
|
825
1099
|
const ios = platforms.ios;
|
|
826
1100
|
if (ios?.owned && ios.deviceUdid) {
|
|
827
1101
|
const udid = ios.deviceUdid;
|
|
828
1102
|
const label = ios.deviceName || udid;
|
|
829
|
-
const r = teardownOwnedIosSim(udid, {
|
|
1103
|
+
const r = await teardownOwnedIosSim(udid, {
|
|
1104
|
+
operation,
|
|
830
1105
|
del: true,
|
|
831
1106
|
label,
|
|
832
1107
|
...park ? { park: parkRequest(project, projectPath, slot, Boolean(ios.simslimManaged)) } : {}
|
|
@@ -856,9 +1131,11 @@ function reclaimOwnedDevices(project, projectPath, { park = false } = {}) {
|
|
|
856
1131
|
}
|
|
857
1132
|
}
|
|
858
1133
|
const android = platforms.android;
|
|
1134
|
+
operation?.signal.throwIfAborted();
|
|
859
1135
|
if (android?.owned && android.avdName) {
|
|
860
1136
|
const bound = parkedMaxSetting("android");
|
|
861
|
-
const r = teardownOwnedAvd(android.avdName, {
|
|
1137
|
+
const r = await teardownOwnedAvd(android.avdName, {
|
|
1138
|
+
operation,
|
|
862
1139
|
del: true,
|
|
863
1140
|
owner: {
|
|
864
1141
|
projectPath,
|
|
@@ -909,6 +1186,7 @@ async function reclaimProject(path, options = {}) {
|
|
|
909
1186
|
const dir = workspaceDir(path);
|
|
910
1187
|
const existed = existsSync(dir);
|
|
911
1188
|
const run = await withIdleWorkspace(path, () => reclaimIdleProject(path, existed, options), {
|
|
1189
|
+
operation: options.operation,
|
|
912
1190
|
purpose: "workspace removal",
|
|
913
1191
|
hosted: false,
|
|
914
1192
|
supervisor: false,
|
|
@@ -939,10 +1217,52 @@ async function reclaimProject(path, options = {}) {
|
|
|
939
1217
|
failedWorkspaceDirs: []
|
|
940
1218
|
};
|
|
941
1219
|
}
|
|
942
|
-
|
|
943
|
-
|
|
1220
|
+
function verifyAvdClaims(project, operation) {
|
|
1221
|
+
for (const { platforms } of projectDeviceSlots(project)) {
|
|
1222
|
+
operation?.signal.throwIfAborted();
|
|
1223
|
+
if (platforms.android?.owned && platforms.android.avdName) (0, ownership_claim_exports.releaseClaim)(acquireAvdClaim(platforms.android.avdName));
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
async function removeWorkspaceOutput(path, dir, archive, state, operation) {
|
|
1227
|
+
const stateDir = workspaceAgentDeviceDir(path);
|
|
1228
|
+
if (!["none", "dead"].includes(agentDeviceDaemon(stateDir))) {
|
|
1229
|
+
const exec = getExecutor();
|
|
1230
|
+
try {
|
|
1231
|
+
if (!exec.findExecutable("agent-device")) throw new Error("agent-device is not on PATH");
|
|
1232
|
+
await runOperationFile(operation, exec, "agent-device", [
|
|
1233
|
+
"daemon",
|
|
1234
|
+
"stop",
|
|
1235
|
+
"--state-dir",
|
|
1236
|
+
stateDir,
|
|
1237
|
+
"--clean"
|
|
1238
|
+
], {
|
|
1239
|
+
cwd: operation?.cwd,
|
|
1240
|
+
timeoutMs: 2e4,
|
|
1241
|
+
killSignal: "SIGKILL",
|
|
1242
|
+
env: { AGENT_DEVICE_NO_UPDATE_NOTIFIER: "1" }
|
|
1243
|
+
});
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
throwIfCleanupInterrupted(error, operation);
|
|
1246
|
+
const message = `${phaseLine("agent-device", `daemon could not be stopped: ${error.message}; run agent-device daemon stop --state-dir "${stateDir}" --clean`)}\n`;
|
|
1247
|
+
if (operation) operation.output("stderr", message);
|
|
1248
|
+
else process.stderr.write(message);
|
|
1249
|
+
throw error;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
operation?.signal.throwIfAborted();
|
|
1253
|
+
if (archive) await archiveWorkspace(path, archive.removedBy, state, operation);
|
|
1254
|
+
await emptyWorkspaceDir(dir, operation?.signal);
|
|
1255
|
+
}
|
|
1256
|
+
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" }, operation }) {
|
|
1257
|
+
const signal = operation?.signal;
|
|
1258
|
+
signal?.throwIfAborted();
|
|
1259
|
+
await clearNamedPorts(path, operation ? {
|
|
1260
|
+
signal,
|
|
1261
|
+
log: (line) => operation.output("stderr", `${line}\n`)
|
|
1262
|
+
} : void 0);
|
|
1263
|
+
signal?.throwIfAborted();
|
|
944
1264
|
const project = getProject(path);
|
|
945
|
-
|
|
1265
|
+
verifyAvdClaims(project, operation);
|
|
946
1266
|
const dereferenced = describeDereferenced(project);
|
|
947
1267
|
const initialState = readWorkspaceState$1(path);
|
|
948
1268
|
let killedPid = null;
|
|
@@ -960,9 +1280,9 @@ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwned
|
|
|
960
1280
|
const orphanIdentity = supervisor.status === "stale" && typeof orphanServer.processToken === "string" ? (0, process_identity_exports.inspectProcessIdentity)(orphanServer) : null;
|
|
961
1281
|
if (supervisor.status === "ours") {
|
|
962
1282
|
requestDevServerStop(path, supervisor.processToken, stopRequester);
|
|
963
|
-
const signalled =
|
|
1283
|
+
const signalled = await killMetroTreeAsync(supervisor.pid, supervisor.processToken, operation);
|
|
964
1284
|
if (!signalled) withdrawDevServerStopRequest(path, supervisor.processToken);
|
|
965
|
-
if (signalled && await (0, process_identity_exports.waitForProcessExit)(supervisor, 1e4)) killedPid = supervisor.pid;
|
|
1285
|
+
if (signalled && await (0, process_identity_exports.waitForProcessExit)(supervisor, 1e4, signal)) killedPid = supervisor.pid;
|
|
966
1286
|
else {
|
|
967
1287
|
skippedMetro = `could not confirm supervisor pid ${supervisor.pid} exited`;
|
|
968
1288
|
supervisorHeld = true;
|
|
@@ -973,9 +1293,14 @@ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwned
|
|
|
973
1293
|
} else if (orphanIdentity === "same") {
|
|
974
1294
|
let signalled = false;
|
|
975
1295
|
try {
|
|
976
|
-
signalled =
|
|
977
|
-
|
|
978
|
-
|
|
1296
|
+
signalled = await signalProcessTreeAsync(orphanServer.pid, "SIGTERM", {
|
|
1297
|
+
group: initialState?.supervisor?.mode === MODE_COMMAND,
|
|
1298
|
+
operation
|
|
1299
|
+
});
|
|
1300
|
+
} catch (error) {
|
|
1301
|
+
throwIfCleanupInterrupted(error, operation);
|
|
1302
|
+
}
|
|
1303
|
+
if (signalled ? await (0, process_identity_exports.waitForProcessExit)(orphanServer, 1e4, signal) : ["gone", "different"].includes((0, process_identity_exports.inspectProcessIdentity)(orphanServer))) killedPid = orphanServer.pid;
|
|
979
1304
|
else {
|
|
980
1305
|
skippedMetro = `could not confirm dev server pid ${orphanServer.pid} left by the supervisor exited`;
|
|
981
1306
|
supervisorHeld = true;
|
|
@@ -988,9 +1313,15 @@ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwned
|
|
|
988
1313
|
}
|
|
989
1314
|
const { skippedDevices: skippedCollectors, failedDevices: failedCollectors } = await reapCollectors(path, {
|
|
990
1315
|
verify: verifyCollector,
|
|
991
|
-
collectors: initialState?.collectors ?? {}
|
|
1316
|
+
collectors: initialState?.collectors ?? {},
|
|
1317
|
+
operation
|
|
992
1318
|
});
|
|
993
|
-
|
|
1319
|
+
signal?.throwIfAborted();
|
|
1320
|
+
const browser = await teardownBrowser(path, {
|
|
1321
|
+
deleteProfile: true,
|
|
1322
|
+
operation
|
|
1323
|
+
});
|
|
1324
|
+
signal?.throwIfAborted();
|
|
994
1325
|
if (browser.status === "failed" || browser.status === "skipped") {
|
|
995
1326
|
const entry = {
|
|
996
1327
|
name: "owned Chrome",
|
|
@@ -1008,7 +1339,10 @@ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwned
|
|
|
1008
1339
|
}
|
|
1009
1340
|
}
|
|
1010
1341
|
retainReplacementProcesses();
|
|
1011
|
-
const { deletedDevices, parkedDevices, evictedDevices, poolNotes, skippedDevices, failedDevices } = deleteOwnedDevices && !supervisorHeld && failedCollectors.length === 0 ? reclaimOwnedDevices(project, path, {
|
|
1342
|
+
const { deletedDevices, parkedDevices, evictedDevices, poolNotes, skippedDevices, failedDevices } = deleteOwnedDevices && !supervisorHeld && failedCollectors.length === 0 ? await reclaimOwnedDevices(project, path, {
|
|
1343
|
+
park: parkOwnedDevices,
|
|
1344
|
+
operation
|
|
1345
|
+
}) : {
|
|
1012
1346
|
deletedDevices: [],
|
|
1013
1347
|
parkedDevices: [],
|
|
1014
1348
|
evictedDevices: [],
|
|
@@ -1018,27 +1352,37 @@ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwned
|
|
|
1018
1352
|
};
|
|
1019
1353
|
skippedDevices.push(...skippedCollectors);
|
|
1020
1354
|
failedDevices.push(...failedCollectors);
|
|
1021
|
-
for (const platform of ["ios", "android"])
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1355
|
+
for (const platform of ["ios", "android"]) {
|
|
1356
|
+
signal?.throwIfAborted();
|
|
1357
|
+
try {
|
|
1358
|
+
await (platform === "ios" ? stopHostedIos : stopHostedAndroid)(path, void 0, signal);
|
|
1359
|
+
} catch (error) {
|
|
1360
|
+
throwIfCleanupInterrupted(error, operation);
|
|
1361
|
+
const failed = {
|
|
1362
|
+
platform,
|
|
1363
|
+
name: `hosted ${platform} device`,
|
|
1364
|
+
reason: error.message
|
|
1365
|
+
};
|
|
1366
|
+
failedDevices.push(failed);
|
|
1367
|
+
skippedDevices.push(failed);
|
|
1368
|
+
}
|
|
1031
1369
|
}
|
|
1032
|
-
|
|
1370
|
+
signal?.throwIfAborted();
|
|
1371
|
+
const remote = await reclaimRemoteSession(path, {
|
|
1372
|
+
stopSession,
|
|
1373
|
+
operation
|
|
1374
|
+
});
|
|
1033
1375
|
if (remote.failed) {
|
|
1034
1376
|
skippedDevices.push(remote.failed);
|
|
1035
1377
|
failedDevices.push(remote.failed);
|
|
1036
1378
|
}
|
|
1037
1379
|
if (macosAppPresent(path)) {
|
|
1380
|
+
signal?.throwIfAborted();
|
|
1038
1381
|
const hosted = readMacosRecord(path)?.host;
|
|
1039
1382
|
try {
|
|
1040
|
-
await stopMacosApp(path);
|
|
1383
|
+
await stopMacosApp(path, operation);
|
|
1041
1384
|
} catch (error) {
|
|
1385
|
+
throwIfCleanupInterrupted(error, operation);
|
|
1042
1386
|
const failed = {
|
|
1043
1387
|
name: hosted ? `hosted macOS session ${hosted.session} on ${hosted.machine}` : "macOS app",
|
|
1044
1388
|
reason: String(error?.message ?? error)
|
|
@@ -1047,52 +1391,39 @@ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwned
|
|
|
1047
1391
|
failedDevices.push(failed);
|
|
1048
1392
|
}
|
|
1049
1393
|
}
|
|
1050
|
-
|
|
1394
|
+
signal?.throwIfAborted();
|
|
1395
|
+
const tunnel = await reclaimMetroTunnel(path, {
|
|
1396
|
+
stopMetroTunnel,
|
|
1397
|
+
operation
|
|
1398
|
+
});
|
|
1051
1399
|
if (tunnel.failed) {
|
|
1052
1400
|
skippedDevices.push(tunnel.failed);
|
|
1053
1401
|
failedDevices.push(tunnel.failed);
|
|
1054
1402
|
}
|
|
1055
1403
|
let releasedLeases = [];
|
|
1404
|
+
signal?.throwIfAborted();
|
|
1056
1405
|
try {
|
|
1057
1406
|
releasedLeases = releaseLeases(path);
|
|
1058
|
-
} catch {
|
|
1407
|
+
} catch (error) {
|
|
1408
|
+
throwIfCleanupInterrupted(error, operation);
|
|
1059
1409
|
releasedLeases = [];
|
|
1060
1410
|
}
|
|
1061
1411
|
retainReplacementProcesses();
|
|
1062
1412
|
const removedWorkspaceDirs = [];
|
|
1063
1413
|
const failedWorkspaceDirs = [];
|
|
1064
1414
|
if (failedDevices.length === 0 && !supervisorHeld) {
|
|
1415
|
+
signal?.throwIfAborted();
|
|
1065
1416
|
const dir = workspaceDir(path);
|
|
1066
1417
|
if (existsSync(dir)) try {
|
|
1067
|
-
|
|
1068
|
-
if (!["none", "dead"].includes(agentDeviceDaemon(stateDir))) {
|
|
1069
|
-
const exec = getExecutor();
|
|
1070
|
-
try {
|
|
1071
|
-
if (!exec.findExecutable("agent-device")) throw new Error("agent-device is not on PATH");
|
|
1072
|
-
exec.runFile("agent-device", [
|
|
1073
|
-
"daemon",
|
|
1074
|
-
"stop",
|
|
1075
|
-
"--state-dir",
|
|
1076
|
-
stateDir,
|
|
1077
|
-
"--clean"
|
|
1078
|
-
], {
|
|
1079
|
-
timeoutMs: 2e4,
|
|
1080
|
-
killSignal: "SIGKILL",
|
|
1081
|
-
env: { AGENT_DEVICE_NO_UPDATE_NOTIFIER: "1" }
|
|
1082
|
-
});
|
|
1083
|
-
} catch (error) {
|
|
1084
|
-
process.stderr.write(`${phaseLine("agent-device", `daemon could not be stopped: ${error.message}; run agent-device daemon stop --state-dir "${stateDir}" --clean`)}\n`);
|
|
1085
|
-
throw error;
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
if (archive) archiveWorkspace(path, archive.removedBy, initialState);
|
|
1089
|
-
emptyWorkspaceDir(dir);
|
|
1418
|
+
await removeWorkspaceOutput(path, dir, archive, initialState, operation);
|
|
1090
1419
|
if (workspaceExisted) removedWorkspaceDirs.push(dir);
|
|
1091
|
-
} catch {
|
|
1420
|
+
} catch (error) {
|
|
1421
|
+
throwIfCleanupInterrupted(error, operation);
|
|
1092
1422
|
failedWorkspaceDirs.push(dir);
|
|
1093
1423
|
}
|
|
1094
1424
|
}
|
|
1095
1425
|
const keptEntry = supervisorHeld || failedDevices.length > 0 || failedWorkspaceDirs.length > 0;
|
|
1426
|
+
signal?.throwIfAborted();
|
|
1096
1427
|
if (project && !keptEntry && !preserveProjectRecord) removeProject(path);
|
|
1097
1428
|
return {
|
|
1098
1429
|
path,
|
|
@@ -1117,11 +1448,14 @@ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwned
|
|
|
1117
1448
|
//#endregion
|
|
1118
1449
|
//#region src/workspace/service-guard.ts
|
|
1119
1450
|
const SERVICE_IN_USE = { code: "STIM_WORKTREE_SERVICE" };
|
|
1451
|
+
function inside(folder, canonical) {
|
|
1452
|
+
return (value) => typeof value === "string" && value.startsWith("/") && isPathPrefix(folder, canonical(value));
|
|
1453
|
+
}
|
|
1120
1454
|
function serviceUseOf(plist, file, folder, canonical) {
|
|
1121
1455
|
if (typeof plist !== "object" || plist === null) return null;
|
|
1122
1456
|
const job = plist;
|
|
1123
1457
|
if (typeof job.StimService !== "object" || job.StimService === null || !Array.isArray(job.ProgramArguments)) return null;
|
|
1124
|
-
const argument = job.ProgramArguments.find((
|
|
1458
|
+
const argument = job.ProgramArguments.find(inside(folder, canonical));
|
|
1125
1459
|
if (argument === void 0) return null;
|
|
1126
1460
|
return {
|
|
1127
1461
|
label: typeof job.Label === "string" ? job.Label : file,
|
|
@@ -1129,9 +1463,61 @@ function serviceUseOf(plist, file, folder, canonical) {
|
|
|
1129
1463
|
argument
|
|
1130
1464
|
};
|
|
1131
1465
|
}
|
|
1132
|
-
/** The
|
|
1133
|
-
function
|
|
1134
|
-
|
|
1466
|
+
/** The launch argument inside `folder` of a unit `stim-server service install` wrote, or null when it has none. */
|
|
1467
|
+
function unitArgument(text, folder, canonical) {
|
|
1468
|
+
const lines = unitLines(text);
|
|
1469
|
+
if (!lines.some((line) => line.section === "X-Stim" && line.key === "Managed" && line.value === "true")) return null;
|
|
1470
|
+
const exec = lines.findLast((line) => line.section === "Service" && line.key === "ExecStart")?.value;
|
|
1471
|
+
const args = exec === void 0 ? null : unitWords(exec, true);
|
|
1472
|
+
if (!args) return void 0;
|
|
1473
|
+
return args.find(inside(folder, canonical)) ?? null;
|
|
1474
|
+
}
|
|
1475
|
+
function unitRunningFrom(folder, canonical, unitsDir) {
|
|
1476
|
+
let names;
|
|
1477
|
+
try {
|
|
1478
|
+
names = readdirSync(unitsDir).filter((name) => name.endsWith(".service"));
|
|
1479
|
+
} catch (error) {
|
|
1480
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT" ? null : {
|
|
1481
|
+
label: "unknown",
|
|
1482
|
+
plist: unitsDir,
|
|
1483
|
+
argument: null
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
for (const name of names) {
|
|
1487
|
+
const file = join(unitsDir, name);
|
|
1488
|
+
const label = name.replace(/\.service$/, "");
|
|
1489
|
+
let text;
|
|
1490
|
+
try {
|
|
1491
|
+
text = readFileSync(file, "utf8");
|
|
1492
|
+
} catch {
|
|
1493
|
+
return {
|
|
1494
|
+
label,
|
|
1495
|
+
plist: file,
|
|
1496
|
+
argument: null
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
if (!text.includes("[X-Stim]")) continue;
|
|
1500
|
+
const argument = unitArgument(text, folder, canonical);
|
|
1501
|
+
if (argument === void 0) return {
|
|
1502
|
+
label,
|
|
1503
|
+
plist: file,
|
|
1504
|
+
argument: null
|
|
1505
|
+
};
|
|
1506
|
+
if (argument) return {
|
|
1507
|
+
label,
|
|
1508
|
+
plist: file,
|
|
1509
|
+
argument
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
return null;
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* The first installed stim-server service that runs from inside `folder`, or null: a LaunchAgent on macOS, a systemd
|
|
1516
|
+
* user unit on Linux. A service file that cannot be read counts as one, with a null `argument`.
|
|
1517
|
+
*/
|
|
1518
|
+
function serviceRunningFrom(folder, canonical, agentsDir = join(homedir(), "Library", "LaunchAgents"), unitsDir = join(homedir(), ".config", "systemd", "user"), platform = process.platform) {
|
|
1519
|
+
if (platform === "linux") return unitRunningFrom(folder, canonical, unitsDir);
|
|
1520
|
+
if (platform !== "darwin") return null;
|
|
1135
1521
|
let names;
|
|
1136
1522
|
try {
|
|
1137
1523
|
names = readdirSync(agentsDir).filter((name) => name.endsWith(".plist"));
|
|
@@ -1206,7 +1592,7 @@ function warmCommand(phase) {
|
|
|
1206
1592
|
function asHolder(holder) {
|
|
1207
1593
|
return {
|
|
1208
1594
|
pid: holder.owner.pid,
|
|
1209
|
-
phase: holder.mode === "exclusive" ? "refresh" : "copy"
|
|
1595
|
+
phase: holder.details.phase === "copy" ? "copy" : holder.mode === "exclusive" ? "refresh" : "copy"
|
|
1210
1596
|
};
|
|
1211
1597
|
}
|
|
1212
1598
|
function waitingLine(holder, elapsedMs) {
|
|
@@ -1292,7 +1678,7 @@ function timeoutError(holder, elapsedMs, root) {
|
|
|
1292
1678
|
error.code = "STIM_LOCK_TIMEOUT";
|
|
1293
1679
|
return error;
|
|
1294
1680
|
}
|
|
1295
|
-
async function acquireWarmClaim({ repositoryRoot, phase, mode = phase === "refresh" ? "exclusive" : "shared", now = Date.now, sleep =
|
|
1681
|
+
async function acquireWarmClaim({ signal, declareSpawnsFor, repositoryRoot, phase, mode = phase === "refresh" ? "exclusive" : "shared", now = Date.now, sleep = (ms) => setTimeout$1(ms, void 0, { signal }), pollMs = WARM_CLAIM_POLL_MS, progressMs = WARM_CLAIM_PROGRESS_MS, ceilingMs = WARM_CLAIM_CEILING_MS, out = () => {} }) {
|
|
1296
1682
|
const root = warmClaimPath(repositoryRoot);
|
|
1297
1683
|
const started = now();
|
|
1298
1684
|
let lastProgress = started;
|
|
@@ -1300,26 +1686,31 @@ async function acquireWarmClaim({ repositoryRoot, phase, mode = phase === "refre
|
|
|
1300
1686
|
let pending = null;
|
|
1301
1687
|
try {
|
|
1302
1688
|
for (;;) {
|
|
1689
|
+
signal?.throwIfAborted();
|
|
1303
1690
|
const attempt = pending ? (0, ownership_claim_exports.settleClaim)(pending) : (0, ownership_claim_exports.tryAcquireClaim)({
|
|
1304
1691
|
root,
|
|
1305
1692
|
mode,
|
|
1306
|
-
label: WARM_CLAIM_LABEL
|
|
1693
|
+
label: WARM_CLAIM_LABEL,
|
|
1694
|
+
details: { phase }
|
|
1307
1695
|
});
|
|
1308
1696
|
const acquired = attempt.acquired;
|
|
1309
1697
|
if (acquired) {
|
|
1698
|
+
if (declareSpawnsFor) declareSpawnsOn(declareSpawnsFor, acquired);
|
|
1310
1699
|
const installer = installerClaim(acquired, {
|
|
1311
1700
|
now,
|
|
1312
1701
|
progressMs,
|
|
1313
1702
|
out
|
|
1314
1703
|
});
|
|
1315
1704
|
return {
|
|
1705
|
+
claim: acquired,
|
|
1316
1706
|
wait: {
|
|
1317
1707
|
waitedMs: now() - started,
|
|
1318
1708
|
holder: waitedOn
|
|
1319
1709
|
},
|
|
1320
1710
|
installer,
|
|
1321
|
-
release: () => {
|
|
1322
|
-
if (
|
|
1711
|
+
release: (retained = false) => {
|
|
1712
|
+
if (declareSpawnsFor) stopDeclaringSpawnsOn(declareSpawnsFor, acquired);
|
|
1713
|
+
if (retained || installer.holding()) return;
|
|
1323
1714
|
(0, ownership_claim_exports.releaseClaim)(acquired);
|
|
1324
1715
|
}
|
|
1325
1716
|
};
|
|
@@ -1348,10 +1739,14 @@ async function acquireWarmClaim({ repositoryRoot, phase, mode = phase === "refre
|
|
|
1348
1739
|
}
|
|
1349
1740
|
async function withWarmClaim(options, fn) {
|
|
1350
1741
|
const { release, ...hold } = await acquireWarmClaim(options);
|
|
1742
|
+
let retained = false;
|
|
1351
1743
|
try {
|
|
1352
1744
|
return await fn(hold);
|
|
1745
|
+
} catch (cause) {
|
|
1746
|
+
retained = cause instanceof WorkspaceProcessCleanupError && cause.claims.includes(hold.claim);
|
|
1747
|
+
throw cause;
|
|
1353
1748
|
} finally {
|
|
1354
|
-
release();
|
|
1749
|
+
release(retained);
|
|
1355
1750
|
}
|
|
1356
1751
|
}
|
|
1357
1752
|
//#endregion
|
|
@@ -1880,7 +2275,7 @@ function dependencyInstallCommand(target, dir = ".") {
|
|
|
1880
2275
|
else if (existsSync$1(resolve$1(installRoot, "package-lock.json"))) command = "npm ci";
|
|
1881
2276
|
return `cd '${installRoot.replaceAll("'", "'\\''")}' && ${command}`;
|
|
1882
2277
|
}
|
|
1883
|
-
function relocateCarriedPods(root, target, copied) {
|
|
2278
|
+
function relocateCarriedPods(root, target, copied, out) {
|
|
1884
2279
|
const canRelocate = depsOutOfSync(root, target, copied).length === 0;
|
|
1885
2280
|
for (const rel of copied) {
|
|
1886
2281
|
if (rel !== "Pods" && !rel.endsWith("/Pods")) continue;
|
|
@@ -1893,41 +2288,40 @@ function relocateCarriedPods(root, target, copied) {
|
|
|
1893
2288
|
targetRoot: target,
|
|
1894
2289
|
canRelocate
|
|
1895
2290
|
});
|
|
1896
|
-
if (result.ok)
|
|
1897
|
-
else if (result.withheld)
|
|
2291
|
+
if (result.ok) out(chalk.dim(phaseLine("carry", result.pods.length > 0 ? `moved ${rel} to this checkout's path; ${result.pods.join(", ")} checksum depends on the checkout path, so pod install is not needed` : `moved ${rel} to this checkout's path; the locks match, so pod install is not needed`)));
|
|
2292
|
+
else if (result.withheld) out(chalk.yellow(phaseLine("carry", `${rel} names the source checkout's path; removed ${rel}/Manifest.lock so pod install runs`)));
|
|
1898
2293
|
} catch (error) {
|
|
1899
|
-
|
|
2294
|
+
out(chalk.yellow(phaseLine("carry", `could not move ${rel} to this checkout's path: ${error.message}. pod install will run`)));
|
|
1900
2295
|
}
|
|
1901
2296
|
}
|
|
1902
2297
|
}
|
|
1903
|
-
function reportCarriedStateHealth(root, target, copied) {
|
|
2298
|
+
function reportCarriedStateHealth(root, target, copied, out) {
|
|
1904
2299
|
const outOfSync = depsOutOfSync(root, target, copied);
|
|
1905
2300
|
for (const d of outOfSync.filter((dependency) => dependency.reason === "installed")) {
|
|
1906
2301
|
const where = d.dir === "." ? d.lockfile : `${d.dir}/${d.lockfile}`;
|
|
1907
2302
|
const modules = d.dir === "." ? "node_modules" : `${d.dir}/node_modules`;
|
|
1908
|
-
|
|
2303
|
+
out(chalk.yellow(phaseLine("carry", `carried ${modules} was installed from a different ${d.lockfile} than the ${where} on disk here. node_modules is gitignored and cloned from the source checkout; ${d.lockfile} is tracked, so the two can disagree. Run \`${dependencyInstallCommand(target, d.dir)}\` before building, or pod install and native builds see the wrong package versions.`)));
|
|
1909
2304
|
}
|
|
1910
2305
|
const staleDeps = outOfSync.filter((dependency) => dependency.reason === "lockfile");
|
|
1911
2306
|
if (staleDeps.length) {
|
|
1912
2307
|
const manifests = staleDeps.map((d) => d.dir === "." ? d.lockfile : `${d.dir}/${d.lockfile}`).join(", ");
|
|
1913
2308
|
const remedies = [...new Set(staleDeps.map((dependency) => dependencyInstallCommand(target, dependency.dir)))];
|
|
1914
|
-
|
|
2309
|
+
out(chalk.yellow(phaseLine("carry", `carried dependencies may be stale: they do not match ${manifests}. Run ${remedies.map((command) => `\`${command}\``).join(" and ")} before building.`)));
|
|
1915
2310
|
}
|
|
1916
2311
|
for (const p of podsOutOfSync(target, copied)) {
|
|
1917
2312
|
const where = p.dir === "." ? "Podfile.lock" : `${p.dir}/Podfile.lock`;
|
|
1918
2313
|
const pod = podInstallCommand(p.dir === "." ? target : resolve$1(target, p.dir, ".."));
|
|
1919
|
-
|
|
2314
|
+
out(chalk.yellow(phaseLine("carry", p.reason === "missing" ? `carried ${p.dir === "." ? "Pods" : `${p.dir}/Pods`} but there is no ${where}. Run \`${pod}\` before building.` : `carried ${p.dir === "." ? "Pods" : `${p.dir}/Pods`} does not match the ${where} on disk here. Pods are gitignored and cloned; Podfile.lock is tracked, so the two can disagree. Run \`${pod}\` before building, or xcodebuild fails with "sandbox is not in sync" only after every pod has compiled.`)));
|
|
1920
2315
|
}
|
|
1921
2316
|
}
|
|
1922
|
-
function reportRefreshFailure(failure) {
|
|
1923
|
-
|
|
1924
|
-
for (const line of failure.lines)
|
|
1925
|
-
if (failure.remedy)
|
|
1926
|
-
|
|
1927
|
-
process.exitCode = 1;
|
|
2317
|
+
function reportRefreshFailure(failure, out) {
|
|
2318
|
+
out(chalk.red(failure.message));
|
|
2319
|
+
for (const line of failure.lines) out(chalk.dim(` ${line}`));
|
|
2320
|
+
if (failure.remedy) out(chalk.dim(failure.remedy));
|
|
2321
|
+
out(chalk.red(`failed: ${failure.code}`));
|
|
1928
2322
|
}
|
|
1929
|
-
function mainCheckoutAppDir(root, target) {
|
|
1930
|
-
const app = findProjectRoot(
|
|
2323
|
+
function mainCheckoutAppDir(root, target, cwd) {
|
|
2324
|
+
const app = findProjectRoot$1(cwd);
|
|
1931
2325
|
if (!app) return root;
|
|
1932
2326
|
const rel = relative$1(target, app);
|
|
1933
2327
|
if (!rel || rel.startsWith("..") || isAbsolute$1(rel)) return root;
|
|
@@ -1939,7 +2333,7 @@ function mainCheckoutAppDir(root, target) {
|
|
|
1939
2333
|
* root can declare react-native without being the app.
|
|
1940
2334
|
*/
|
|
1941
2335
|
function warmedWorkspace(root, target, cwd) {
|
|
1942
|
-
const nearest = findProjectRoot(cwd);
|
|
2336
|
+
const nearest = findProjectRoot$1(cwd);
|
|
1943
2337
|
const app = nearest && isPathPrefix(target, nearest) && isMobileProject(nearest) ? nearest : null;
|
|
1944
2338
|
if (app && app !== target) return app;
|
|
1945
2339
|
const apps = /* @__PURE__ */ new Set();
|
|
@@ -1951,150 +2345,204 @@ function warmedWorkspace(root, target, cwd) {
|
|
|
1951
2345
|
}
|
|
1952
2346
|
return apps.size === 1 ? [...apps][0] : app;
|
|
1953
2347
|
}
|
|
1954
|
-
function registerWarmedWorkspace(workspace) {
|
|
2348
|
+
function registerWarmedWorkspace(workspace, out) {
|
|
1955
2349
|
try {
|
|
1956
2350
|
if (!getProject(workspace)) upsertProject(workspace, {});
|
|
1957
2351
|
} catch (error) {
|
|
1958
|
-
|
|
2352
|
+
out(chalk.yellow(`Could not register ${workspace} for status: ${error?.message || String(error)}`));
|
|
1959
2353
|
}
|
|
1960
2354
|
}
|
|
1961
2355
|
function registerWarm(worktree) {
|
|
1962
2356
|
worktree.command("warm").description("Copy missing ignored paths from the source checkout into the current linked worktree.").option("--refresh", "Before copying, fast-forward the source checkout to its upstream and install what the new commits moved.").action(async (opts) => {
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
workspace = warmedWorkspace(root, target, process.cwd());
|
|
1970
|
-
if (workspace && workspace !== app) recordWorkspaceUse(workspace);
|
|
1971
|
-
if (!workspace) console.error(chalk.dim(phaseLine("project", "no single app to register here; run warm from the app directory to list it in status while it warms")));
|
|
1972
|
-
else {
|
|
1973
|
-
registerWarmedWorkspace(workspace);
|
|
1974
|
-
progress = startWarmProgress(workspace, opts.refresh ? "refresh" : "copy", { note: (line) => console.error(chalk.yellow(line)) });
|
|
1975
|
-
}
|
|
1976
|
-
const readSettings = () => {
|
|
1977
|
-
const settings = resolveSettings({
|
|
1978
|
-
gitCommonDir: common,
|
|
1979
|
-
repoRoot: root
|
|
1980
|
-
});
|
|
1981
|
-
const shapeErrors = settingShapeErrors(settings);
|
|
1982
|
-
if (shapeErrors.length) {
|
|
1983
|
-
for (const message of shapeErrors) console.error(chalk.red(message));
|
|
1984
|
-
console.error(chalk.dim(SETTING_SHAPE_REMEDY));
|
|
1985
|
-
return null;
|
|
1986
|
-
}
|
|
1987
|
-
for (const key of unknownSettingKeys(settings)) console.error(chalk.yellow(`Warning: setting "${key}" is not read by Stim and will be ignored.`));
|
|
1988
|
-
return settings;
|
|
1989
|
-
};
|
|
1990
|
-
const copy = (wait) => {
|
|
1991
|
-
progress?.step("copy");
|
|
1992
|
-
if (wait?.holder) console.error(warmClaimAcquiredLine(wait));
|
|
1993
|
-
const incomplete = incompleteInstallRefusal(root, mainCheckoutAppDir(root, target));
|
|
1994
|
-
if (incomplete) {
|
|
1995
|
-
reportRefreshFailure(incomplete);
|
|
1996
|
-
return;
|
|
1997
|
-
}
|
|
1998
|
-
const settings = readSettings();
|
|
1999
|
-
if (!settings) {
|
|
2000
|
-
process.exitCode = 1;
|
|
2001
|
-
return;
|
|
2002
|
-
}
|
|
2003
|
-
const excluded = readWorktreeExclude(root);
|
|
2004
|
-
const patterns = excluded?.length ? excluded : settings.worktree?.exclude || [];
|
|
2005
|
-
const result = cloneIgnoredEntries({
|
|
2006
|
-
root,
|
|
2007
|
-
target,
|
|
2008
|
-
patterns
|
|
2357
|
+
const { value, interrupted } = await runProcessOperation(process.cwd(), async (operation) => {
|
|
2358
|
+
try {
|
|
2359
|
+
return await warmWorktreeTarget({
|
|
2360
|
+
cwd: operation.cwd,
|
|
2361
|
+
refresh: opts.refresh,
|
|
2362
|
+
operation
|
|
2009
2363
|
});
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
console.error(chalk.
|
|
2014
|
-
|
|
2015
|
-
|
|
2364
|
+
} catch (error) {
|
|
2365
|
+
if (!operation.signal.aborted || error instanceof WorkspaceProcessCleanupError) {
|
|
2366
|
+
const code = error?.code;
|
|
2367
|
+
console.error(chalk.red(`Could not warm this worktree: ${error.message}`));
|
|
2368
|
+
const claim = (0, ownership_claim_exports.claimFailure)(error, `stim worktree warm${opts.refresh ? " --refresh" : ""}`);
|
|
2369
|
+
if (claim) console.error(chalk.dim(claim.remedy));
|
|
2370
|
+
if (typeof code === "string" && code.startsWith("STIM_")) console.error(chalk.red(`failed: ${code}`));
|
|
2016
2371
|
}
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2372
|
+
return { exitCode: 1 };
|
|
2373
|
+
}
|
|
2374
|
+
});
|
|
2375
|
+
if (interrupted !== null) process.exitCode = interrupted;
|
|
2376
|
+
else if (value.exitCode) process.exitCode = 1;
|
|
2377
|
+
});
|
|
2378
|
+
}
|
|
2379
|
+
async function warmWorktreeTarget(opts) {
|
|
2380
|
+
let exitCode = 0;
|
|
2381
|
+
const operation = opts.operation;
|
|
2382
|
+
const out = (line) => operation.output("stderr", `${line}\n`);
|
|
2383
|
+
let progress = null;
|
|
2384
|
+
let workspace = null;
|
|
2385
|
+
try {
|
|
2386
|
+
const { root, target, common } = await warmWorktreePaths(opts.cwd, operation);
|
|
2387
|
+
operation.signal.throwIfAborted();
|
|
2388
|
+
const app = findProjectRoot$1(opts.cwd);
|
|
2389
|
+
workspace = warmedWorkspace(root, target, opts.cwd);
|
|
2390
|
+
const scope = /* @__PURE__ */ new Set([
|
|
2391
|
+
...reclaimKeys(target),
|
|
2392
|
+
...app ? [app] : [],
|
|
2393
|
+
...workspace ? [workspace] : []
|
|
2394
|
+
]);
|
|
2395
|
+
for (const key of scope) opts.beforeWarm?.(nativeCanonicalPath(key));
|
|
2396
|
+
if (app) recordWorkspaceUse(app, /* @__PURE__ */ new Date(), operation.env, opts.cwd);
|
|
2397
|
+
if (workspace && workspace !== app) recordWorkspaceUse(workspace, /* @__PURE__ */ new Date(), operation.env, opts.cwd);
|
|
2398
|
+
if (!workspace) out(chalk.dim(phaseLine("project", "no single app to register here; run warm from the app directory to list it in status while it warms")));
|
|
2399
|
+
else {
|
|
2400
|
+
registerWarmedWorkspace(workspace, out);
|
|
2401
|
+
progress = startWarmProgress(workspace, opts.refresh ? "refresh" : "copy", { note: (line) => out(chalk.yellow(line)) });
|
|
2402
|
+
}
|
|
2403
|
+
const readSettings = () => {
|
|
2404
|
+
const settings = resolveSettings({
|
|
2405
|
+
gitCommonDir: common,
|
|
2406
|
+
repoRoot: root
|
|
2407
|
+
});
|
|
2408
|
+
const shapeErrors = settingShapeErrors(settings);
|
|
2409
|
+
if (shapeErrors.length) {
|
|
2410
|
+
for (const message of shapeErrors) out(chalk.red(message));
|
|
2411
|
+
out(chalk.dim(SETTING_SHAPE_REMEDY));
|
|
2412
|
+
return null;
|
|
2413
|
+
}
|
|
2414
|
+
for (const key of unknownSettingKeys(settings)) out(chalk.yellow(`Warning: setting "${key}" is not read by Stim and will be ignored.`));
|
|
2415
|
+
return settings;
|
|
2416
|
+
};
|
|
2417
|
+
const copy = async (wait) => {
|
|
2418
|
+
const state = operationState(operation);
|
|
2419
|
+
const wasDraining = state.draining;
|
|
2420
|
+
state.draining = true;
|
|
2421
|
+
try {
|
|
2422
|
+
await withWarmClaim({
|
|
2423
|
+
repositoryRoot: target,
|
|
2424
|
+
phase: "copy",
|
|
2425
|
+
mode: "exclusive",
|
|
2426
|
+
out,
|
|
2427
|
+
signal: operation.signal,
|
|
2428
|
+
declareSpawnsFor: operation
|
|
2429
|
+
}, async (hold) => {
|
|
2430
|
+
if (hold.wait.holder) out(warmClaimAcquiredLine(hold.wait));
|
|
2431
|
+
operation.signal.throwIfAborted();
|
|
2432
|
+
progress?.step("copy");
|
|
2433
|
+
if (wait?.holder) out(warmClaimAcquiredLine(wait));
|
|
2434
|
+
const incomplete = incompleteInstallRefusal(root, mainCheckoutAppDir(root, target, opts.cwd));
|
|
2435
|
+
if (incomplete) {
|
|
2436
|
+
reportRefreshFailure(incomplete, out);
|
|
2437
|
+
exitCode = 1;
|
|
2438
|
+
return;
|
|
2439
|
+
}
|
|
2029
2440
|
const settings = readSettings();
|
|
2030
2441
|
if (!settings) {
|
|
2031
|
-
|
|
2442
|
+
exitCode = 1;
|
|
2032
2443
|
return;
|
|
2033
2444
|
}
|
|
2034
|
-
const
|
|
2035
|
-
const
|
|
2445
|
+
const excluded = readWorktreeExclude(root);
|
|
2446
|
+
const patterns = excluded?.length ? excluded : settings.worktree?.exclude || [];
|
|
2447
|
+
const result = await cloneIgnoredEntries({
|
|
2036
2448
|
root,
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
emit: (line) => lines.push(line),
|
|
2041
|
-
inspectOnly: true
|
|
2449
|
+
target,
|
|
2450
|
+
patterns,
|
|
2451
|
+
operation
|
|
2042
2452
|
});
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2453
|
+
for (const entry of result.skipped) out(chalk.dim(phaseLine("carry", `kept ${entry.file} (${entry.reason})`)));
|
|
2454
|
+
for (const entry of result.failed) out(chalk.yellow(phaseLine("carry", `could not copy ${entry.file}: ${entry.error}`)));
|
|
2455
|
+
if (result.copied.length) {
|
|
2456
|
+
out(chalk.dim(phaseLine("carry", `copied ${carriedFileList(result.copied)} from ${root}`)));
|
|
2457
|
+
relocateCarriedPods(root, target, result.copied, out);
|
|
2458
|
+
reportCarriedStateHealth(root, target, result.copied, out);
|
|
2049
2459
|
}
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2460
|
+
out(phaseLine("carry", `${result.failed.length ? "incomplete" : "complete"}: ${result.copied.length} ignored entries copied, ${result.skipped.length} kept, ${result.failed.length} failed`));
|
|
2461
|
+
if (result.failed.length) exitCode = 1;
|
|
2462
|
+
});
|
|
2463
|
+
} finally {
|
|
2464
|
+
state.draining = wasDraining;
|
|
2465
|
+
}
|
|
2466
|
+
};
|
|
2467
|
+
if (opts.refresh) {
|
|
2468
|
+
const mutation = await withWarmClaim({
|
|
2469
|
+
repositoryRoot: root,
|
|
2470
|
+
phase: "refresh",
|
|
2471
|
+
mode: "shared",
|
|
2472
|
+
signal: operation.signal,
|
|
2473
|
+
declareSpawnsFor: operation,
|
|
2474
|
+
out
|
|
2475
|
+
}, async (hold) => {
|
|
2476
|
+
const settings = readSettings();
|
|
2477
|
+
if (!settings) {
|
|
2478
|
+
exitCode = 1;
|
|
2479
|
+
return null;
|
|
2053
2480
|
}
|
|
2054
|
-
const
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
process.exitCode = 1;
|
|
2063
|
-
return null;
|
|
2064
|
-
}
|
|
2065
|
-
return refreshMainCheckout({
|
|
2066
|
-
root,
|
|
2067
|
-
appDir: mainCheckoutAppDir(root, target),
|
|
2068
|
-
settings,
|
|
2069
|
-
iosProject: appIosProjectDir(mainCheckoutAppDir(root, target)),
|
|
2070
|
-
emit: (line) => console.error(line),
|
|
2071
|
-
installer: hold.installer
|
|
2072
|
-
});
|
|
2481
|
+
const lines = [];
|
|
2482
|
+
const result = await refreshMainCheckout({
|
|
2483
|
+
root,
|
|
2484
|
+
appDir: mainCheckoutAppDir(root, target, opts.cwd),
|
|
2485
|
+
settings,
|
|
2486
|
+
iosProject: appIosProjectDir(mainCheckoutAppDir(root, target, opts.cwd)),
|
|
2487
|
+
emit: (line) => lines.push(line),
|
|
2488
|
+
inspectOnly: true
|
|
2073
2489
|
});
|
|
2074
|
-
if (
|
|
2075
|
-
|
|
2076
|
-
|
|
2490
|
+
if (!result || !("mutation" in result)) {
|
|
2491
|
+
out(warmClaimAcquiredLine(hold.wait, result ? "shared" : "shared (seed current)"));
|
|
2492
|
+
for (const line of lines) out(line);
|
|
2493
|
+
if (result) {
|
|
2494
|
+
reportRefreshFailure(result, out);
|
|
2495
|
+
exitCode = 1;
|
|
2496
|
+
} else await copy(null);
|
|
2497
|
+
return null;
|
|
2077
2498
|
}
|
|
2078
|
-
|
|
2079
|
-
}
|
|
2080
|
-
|
|
2499
|
+
return result.mutation;
|
|
2500
|
+
});
|
|
2501
|
+
if (mutation === null) return { exitCode };
|
|
2502
|
+
const failure = await withWarmClaim({
|
|
2081
2503
|
repositoryRoot: root,
|
|
2082
|
-
phase: "
|
|
2083
|
-
out
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2504
|
+
phase: "refresh",
|
|
2505
|
+
out,
|
|
2506
|
+
signal: operation.signal
|
|
2507
|
+
}, async (hold) => {
|
|
2508
|
+
out(warmClaimAcquiredLine(hold.wait, `exclusive (${mutation})`));
|
|
2509
|
+
const settings = readSettings();
|
|
2510
|
+
if (!settings) {
|
|
2511
|
+
exitCode = 1;
|
|
2512
|
+
return null;
|
|
2513
|
+
}
|
|
2514
|
+
return refreshMainCheckout({
|
|
2515
|
+
root,
|
|
2516
|
+
appDir: mainCheckoutAppDir(root, target, opts.cwd),
|
|
2517
|
+
settings,
|
|
2518
|
+
iosProject: appIosProjectDir(mainCheckoutAppDir(root, target, opts.cwd)),
|
|
2519
|
+
emit: (line) => out(line),
|
|
2520
|
+
installer: hold.installer
|
|
2521
|
+
});
|
|
2522
|
+
});
|
|
2523
|
+
if (failure) {
|
|
2524
|
+
reportRefreshFailure(failure, out);
|
|
2525
|
+
exitCode = 1;
|
|
2526
|
+
return { exitCode };
|
|
2527
|
+
}
|
|
2528
|
+
if (exitCode) return { exitCode };
|
|
2096
2529
|
}
|
|
2097
|
-
|
|
2530
|
+
await withWarmClaim({
|
|
2531
|
+
repositoryRoot: root,
|
|
2532
|
+
phase: "copy",
|
|
2533
|
+
out,
|
|
2534
|
+
signal: operation.signal,
|
|
2535
|
+
declareSpawnsFor: operation
|
|
2536
|
+
}, (hold) => copy(hold.wait));
|
|
2537
|
+
} catch (error) {
|
|
2538
|
+
exitCode = 1;
|
|
2539
|
+
throw error;
|
|
2540
|
+
} finally {
|
|
2541
|
+
progress?.finish(!exitCode && !operation.signal.aborted);
|
|
2542
|
+
const links = workspace && !exitCode && !operation.signal.aborted ? await workspaceLinks(workspace) : void 0;
|
|
2543
|
+
if (links) out(chalk.dim(workspaceLinkLine(links)));
|
|
2544
|
+
}
|
|
2545
|
+
return { exitCode };
|
|
2098
2546
|
}
|
|
2099
2547
|
function carriedFileList(files) {
|
|
2100
2548
|
const shown = files.slice(0, 3).join(", ");
|
|
@@ -2180,6 +2628,11 @@ function nativeCanonicalPath(path) {
|
|
|
2180
2628
|
missing.unshift(basename$1(existing));
|
|
2181
2629
|
}
|
|
2182
2630
|
}
|
|
2631
|
+
/** Other worktrees git lists inside `path`, which removing `path` would delete with it. */
|
|
2632
|
+
function nestedWorktrees(entries, path) {
|
|
2633
|
+
const root = nativeCanonicalPath(path);
|
|
2634
|
+
return entries.filter((candidate) => !candidate.bare && nativeCanonicalPath(candidate.path) !== root).filter((candidate) => isPathPrefix(root, nativeCanonicalPath(candidate.path))).map((candidate) => candidate.path);
|
|
2635
|
+
}
|
|
2183
2636
|
function matchWorktreeEntry(entries, path) {
|
|
2184
2637
|
let best = null;
|
|
2185
2638
|
const target = nativeCanonicalPath(path);
|
|
@@ -2239,12 +2692,16 @@ function reclaimKeys(rootPath) {
|
|
|
2239
2692
|
}
|
|
2240
2693
|
return [...keys].toSorted();
|
|
2241
2694
|
}
|
|
2242
|
-
async function withReclaimLocks(rootPath, fn) {
|
|
2695
|
+
async function withReclaimLocks(rootPath, fn, operation, beforeRemove) {
|
|
2243
2696
|
const keys = reclaimKeys(rootPath);
|
|
2244
|
-
|
|
2697
|
+
for (const key of keys) beforeRemove?.(nativeCanonicalPath(key));
|
|
2698
|
+
const acquire = (index) => index === keys.length ? fn(keys) : withManagedTunnelRemovalLock(keys[index], () => acquire(index + 1), {
|
|
2699
|
+
signal: operation?.signal,
|
|
2700
|
+
declareSpawnsFor: operation
|
|
2701
|
+
});
|
|
2245
2702
|
return acquire(0);
|
|
2246
2703
|
}
|
|
2247
|
-
async function reclaimAll(rootPath, keys = reclaimKeys(rootPath), { preserveRootProject = false, removedBy } = {}) {
|
|
2704
|
+
async function reclaimAll(rootPath, keys = reclaimKeys(rootPath), { preserveRootProject = false, removedBy, operation } = {}) {
|
|
2248
2705
|
const dereferenced = [];
|
|
2249
2706
|
const killedPids = [];
|
|
2250
2707
|
const deletedDevices = [];
|
|
@@ -2259,17 +2716,23 @@ async function reclaimAll(rootPath, keys = reclaimKeys(rootPath), { preserveRoot
|
|
|
2259
2716
|
const releasedLeases = [];
|
|
2260
2717
|
const removedWorkspaceDirs = [];
|
|
2261
2718
|
const failedWorkspaceDirs = [];
|
|
2719
|
+
operation?.signal.throwIfAborted();
|
|
2720
|
+
const pressure = currentDiskLevel(operation ? await measureDiskAsync(operation) : void 0);
|
|
2262
2721
|
for (const key of keys) {
|
|
2722
|
+
operation?.signal.throwIfAborted();
|
|
2263
2723
|
let r;
|
|
2264
2724
|
try {
|
|
2265
2725
|
r = await reclaimProject(key, {
|
|
2266
2726
|
archive: removedBy ? { removedBy } : void 0,
|
|
2267
2727
|
deleteOwnedDevices: true,
|
|
2268
|
-
parkOwnedDevices:
|
|
2728
|
+
parkOwnedDevices: pressure < 2,
|
|
2269
2729
|
preserveProjectRecord: preserveRootProject && key === rootPath,
|
|
2270
|
-
stopRequester: { by: "stim worktree remove" }
|
|
2730
|
+
stopRequester: { by: "stim worktree remove" },
|
|
2731
|
+
operation
|
|
2271
2732
|
});
|
|
2272
2733
|
} catch (error) {
|
|
2734
|
+
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
2735
|
+
operation?.signal.throwIfAborted();
|
|
2273
2736
|
keptEntries.push(key);
|
|
2274
2737
|
retainedResources.push({
|
|
2275
2738
|
name: "ownership state",
|
|
@@ -2298,6 +2761,7 @@ async function reclaimAll(rootPath, keys = reclaimKeys(rootPath), { preserveRoot
|
|
|
2298
2761
|
});
|
|
2299
2762
|
}
|
|
2300
2763
|
}
|
|
2764
|
+
if (pressure >= 2 && deletedDevices.length) poolNotes.push(`disk pressure level ${pressure}: deleted the owned devices instead of parking them`);
|
|
2301
2765
|
for (const key of keys) {
|
|
2302
2766
|
if (keptEntries.includes(key)) continue;
|
|
2303
2767
|
const tunnel = readMetroTunnel(key);
|
|
@@ -2350,18 +2814,18 @@ function poolLines(result) {
|
|
|
2350
2814
|
for (const note of result.poolNotes) lines.push(chalk.yellow(phaseLine("device", note)));
|
|
2351
2815
|
return lines;
|
|
2352
2816
|
}
|
|
2353
|
-
function reportRetainedResources(root, result) {
|
|
2354
|
-
|
|
2817
|
+
function reportRetainedResources(root, result, output) {
|
|
2818
|
+
output.error(chalk.red(`Refusing to remove ${root}: Stim could not release owned resources.`));
|
|
2355
2819
|
for (const resource of result.retainedResources) {
|
|
2356
|
-
|
|
2357
|
-
|
|
2820
|
+
output.error(chalk.yellow(` - Stim still tracks ${describeKeptDevice(resource)} for ${resource.project}`));
|
|
2821
|
+
output.error(chalk.dim(` ${resource.reason}`));
|
|
2358
2822
|
}
|
|
2359
2823
|
for (const kept of result.keptEntries) {
|
|
2360
2824
|
if (result.retainedResources.some((resource) => resource.project === kept)) continue;
|
|
2361
|
-
|
|
2825
|
+
output.error(chalk.yellow(` - retained Stim ownership state for ${kept}`));
|
|
2362
2826
|
}
|
|
2363
|
-
|
|
2364
|
-
|
|
2827
|
+
output.error(chalk.dim("Fix the reported cause, then run `stim worktree remove` again."));
|
|
2828
|
+
output.fail();
|
|
2365
2829
|
}
|
|
2366
2830
|
function hasRegisteredProjectUnder(rootPath) {
|
|
2367
2831
|
const cfg = loadConfig$1();
|
|
@@ -2371,28 +2835,40 @@ function projectAtOrAbove(root, path) {
|
|
|
2371
2835
|
const here = nativeCanonicalPath(path);
|
|
2372
2836
|
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;
|
|
2373
2837
|
}
|
|
2374
|
-
async function reclaimEnvironment(root, why) {
|
|
2838
|
+
async function reclaimEnvironment(root, why, output, operation, beforeRemove) {
|
|
2375
2839
|
await withManagedRemoteWorktreeRemovalLock(root, () => withReclaimLocks(root, async (lockedKeys) => {
|
|
2376
|
-
const result = await reclaimAll(root, lockedKeys, {
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
for (const
|
|
2381
|
-
for (const
|
|
2382
|
-
for (const
|
|
2383
|
-
for (const
|
|
2384
|
-
for (const
|
|
2385
|
-
|
|
2840
|
+
const result = await reclaimAll(root, lockedKeys, {
|
|
2841
|
+
removedBy: "worktree-remove",
|
|
2842
|
+
operation
|
|
2843
|
+
});
|
|
2844
|
+
for (const line of poolLines(result)) output.error(line);
|
|
2845
|
+
for (const device of result.deletedDevices) output.error(chalk.dim(phaseLine("device", `deleted ${device}`)));
|
|
2846
|
+
for (const session of result.stoppedSessions) output.error(chalk.dim(phaseLine("device", `stopped remote session ${session}`)));
|
|
2847
|
+
for (const tunnel of result.stoppedTunnels) output.error(chalk.dim(phaseLine("lan", `stopped the ${tunnel} tunnel`)));
|
|
2848
|
+
for (const lease of result.releasedLeases) output.error(chalk.dim(phaseLine("lease", releasedLeaseFact(lease))));
|
|
2849
|
+
for (const pid of result.killedPids) output.error(chalk.dim(phaseLine("metro", `killed pid ${pid}`)));
|
|
2850
|
+
for (const dir of result.removedWorkspaceDirs) output.error(chalk.dim(phaseLine("workspace", `removed ${dir}`)));
|
|
2851
|
+
for (const dir of result.failedWorkspaceDirs) output.error(chalk.yellow(phaseLine("workspace", `could not remove ${dir}`)));
|
|
2852
|
+
if (result.dereferenced.length) output.error(chalk.dim(phaseLine("workspace", `no longer referenced: ${result.dereferenced.join(", ")}`)));
|
|
2386
2853
|
if (result.keptEntries.length) {
|
|
2387
|
-
reportRetainedResources(root, result);
|
|
2854
|
+
reportRetainedResources(root, result, output);
|
|
2388
2855
|
return;
|
|
2389
2856
|
}
|
|
2390
|
-
for (const s of result.skippedDevices)
|
|
2391
|
-
|
|
2392
|
-
})
|
|
2857
|
+
for (const s of result.skippedDevices) output.error(chalk.yellow(phaseLine("device", `kept ${describeKeptDevice(s)} (${s.reason})`)));
|
|
2858
|
+
output.error(chalk.green(`Reclaimed the environment; the working tree stays (${why}).`));
|
|
2859
|
+
}, operation, beforeRemove), {
|
|
2860
|
+
signal: operation?.signal,
|
|
2861
|
+
declareSpawnsFor: operation
|
|
2862
|
+
});
|
|
2393
2863
|
}
|
|
2394
|
-
|
|
2395
|
-
|
|
2864
|
+
const REMOVAL_OUTPUT = {
|
|
2865
|
+
error: (line) => console.error(line),
|
|
2866
|
+
fail: () => {
|
|
2867
|
+
process.exitCode = 1;
|
|
2868
|
+
}
|
|
2869
|
+
};
|
|
2870
|
+
function removalPath(target, cwd = process.cwd()) {
|
|
2871
|
+
return canonicalExistingPath(target ?? cwd);
|
|
2396
2872
|
}
|
|
2397
2873
|
function canonicalExistingPath(target) {
|
|
2398
2874
|
const resolved = resolve$1(target);
|
|
@@ -2406,9 +2882,9 @@ function canonicalExistingPath(target) {
|
|
|
2406
2882
|
}
|
|
2407
2883
|
return resolve$1(realpathSync$1(existing), ...missing);
|
|
2408
2884
|
}
|
|
2409
|
-
function inspectRemoval(path, mergedHead) {
|
|
2885
|
+
async function inspectRemoval(path, mergedHead, operation) {
|
|
2410
2886
|
try {
|
|
2411
|
-
return inspectGit(path, mergedHead);
|
|
2887
|
+
return await inspectGit(path, mergedHead, operation);
|
|
2412
2888
|
} catch (error) {
|
|
2413
2889
|
if (!isTimeoutError(error)) throw error;
|
|
2414
2890
|
return timedOutInspection(error);
|
|
@@ -2424,18 +2900,24 @@ function timedOutInspection(error) {
|
|
|
2424
2900
|
timedOut: true
|
|
2425
2901
|
};
|
|
2426
2902
|
}
|
|
2427
|
-
function inspectGit(path, mergedHead) {
|
|
2428
|
-
const
|
|
2429
|
-
|
|
2903
|
+
async function inspectGit(path, mergedHead, operation) {
|
|
2904
|
+
const options = {
|
|
2905
|
+
removal: true,
|
|
2906
|
+
env: operation?.env,
|
|
2907
|
+
signal: operation?.signal,
|
|
2908
|
+
declareFor: operation
|
|
2909
|
+
};
|
|
2910
|
+
const gitAnswered = await hasUncommittedWorkAsync(path, options);
|
|
2911
|
+
const allDirty = gitAnswered ? await dirtyPathsAsync(path, { limit: Infinity }, options) : [];
|
|
2430
2912
|
const { lines: withoutCookies, cookies } = excludeWatchmanCookies(allDirty ?? []);
|
|
2431
2913
|
const { lines: dirtyLines, restore: podChurn } = excludePodChurn(withoutCookies, customIosDirs(path));
|
|
2432
2914
|
const dirty = gitAnswered === null || allDirty === null ? null : dirtyLines.length > 0;
|
|
2433
|
-
const unpushed =
|
|
2915
|
+
const unpushed = await unpushedCommitsAsync(path, options);
|
|
2434
2916
|
const blockers = removalBlockers({
|
|
2435
2917
|
dirty,
|
|
2436
|
-
unpushed: Boolean(mergedHead) &&
|
|
2918
|
+
unpushed: Boolean(mergedHead) && await resolveFullRefAsync(path, "HEAD", options) === mergedHead && unpushed ? [] : unpushed
|
|
2437
2919
|
});
|
|
2438
|
-
const submodules =
|
|
2920
|
+
const submodules = await hasPopulatedSubmodulesAsync(path, options);
|
|
2439
2921
|
if (submodules === null) blockers.push("could not check for initialized submodules; re-run with --force to override");
|
|
2440
2922
|
else if (submodules) blockers.push("initialized submodules, which git removes only with --force");
|
|
2441
2923
|
return {
|
|
@@ -2446,43 +2928,50 @@ function inspectGit(path, mergedHead) {
|
|
|
2446
2928
|
blockers
|
|
2447
2929
|
};
|
|
2448
2930
|
}
|
|
2449
|
-
function printRemovalRefusal(path, inspection) {
|
|
2931
|
+
async function printRemovalRefusal(path, inspection, output, operation) {
|
|
2450
2932
|
const { blockers, dirtyLines, unpushed } = inspection;
|
|
2451
|
-
|
|
2452
|
-
for (const blocker of blockers)
|
|
2933
|
+
output.error(chalk.red(`Refusing to remove ${path}:`));
|
|
2934
|
+
for (const blocker of blockers) output.error(chalk.red(` - ${blocker}`));
|
|
2453
2935
|
if (inspection.timedOut) {
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2936
|
+
output.error(chalk.dim("Git did not answer in time, so Stim cannot tell whether the worktree holds unsaved work."));
|
|
2937
|
+
output.error(chalk.dim(`Retry once git responds; check with: git -C ${path} status`));
|
|
2938
|
+
output.fail();
|
|
2457
2939
|
return;
|
|
2458
2940
|
}
|
|
2459
|
-
if (unpushed && unpushed.length && !
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2941
|
+
if (unpushed && unpushed.length && !await hasRemoteAsync(path, {
|
|
2942
|
+
env: operation?.env,
|
|
2943
|
+
signal: operation?.signal
|
|
2944
|
+
})) output.error(chalk.dim(" (no remote is configured for this worktree, so every commit no other local branch reaches counts as unpushed)"));
|
|
2945
|
+
for (const line of dirtyLines.slice(0, 10)) output.error(chalk.dim(` ${line}`));
|
|
2946
|
+
if (dirtyLines.length) output.error(chalk.dim(` (git -C ${path} status -s for the full list)`));
|
|
2947
|
+
for (const line of removalRemedy(dirtyLines, { worktree: path })) output.error(chalk.dim(line));
|
|
2948
|
+
output.error(chalk.dim("Otherwise: commit or push the branch (only commits found nowhere else are counted,"));
|
|
2949
|
+
output.error(chalk.dim("so pushing publishes nothing but this worktree's own work). --force is a last"));
|
|
2950
|
+
output.error(chalk.dim("resort -- it discards uncommitted changes and untracked files permanently; committed"));
|
|
2951
|
+
output.error(chalk.dim("work stays on the branch."));
|
|
2952
|
+
output.fail();
|
|
2468
2953
|
}
|
|
2469
2954
|
function deleteWatchmanCookies(path, cookies) {
|
|
2470
2955
|
for (const cookie of cookies) rmSync$1(join$1(path, cookie), { force: true });
|
|
2471
2956
|
}
|
|
2472
|
-
function restorePodChurn(path, files) {
|
|
2473
|
-
for (const file of files) if (
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
for (const
|
|
2482
|
-
for (const
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
for (const
|
|
2957
|
+
async function restorePodChurn(path, files, output, operation) {
|
|
2958
|
+
for (const file of files) if (await restoreFileAsync(path, file, {
|
|
2959
|
+
env: operation?.env,
|
|
2960
|
+
signal: operation?.signal,
|
|
2961
|
+
declareFor: operation
|
|
2962
|
+
})) output.error(chalk.dim(` restored ${file} (pod install churn; the worktree is being removed)`));
|
|
2963
|
+
else output.error(chalk.yellow(` could not restore ${file}; git may refuse to remove the worktree`));
|
|
2964
|
+
}
|
|
2965
|
+
function printRemovalCleanup(result, failed, output) {
|
|
2966
|
+
for (const line of poolLines(result)) output.error(line);
|
|
2967
|
+
for (const device of result.deletedDevices) output.error(chalk.dim(phaseLine("device", `deleted ${device}`)));
|
|
2968
|
+
for (const session of result.stoppedSessions) output.error(chalk.dim(phaseLine("device", `stopped remote session ${session}`)));
|
|
2969
|
+
for (const tunnel of result.stoppedTunnels) output.error(chalk.dim(phaseLine("lan", `stopped the ${tunnel} tunnel`)));
|
|
2970
|
+
for (const lease of result.releasedLeases) output.error(chalk.dim(phaseLine("lease", releasedLeaseFact(lease))));
|
|
2971
|
+
for (const pid of result.killedPids) output.error(chalk.dim(phaseLine("metro", `killed pid ${pid}`)));
|
|
2972
|
+
if (!failed) for (const dir of result.removedWorkspaceDirs) output.error(chalk.dim(phaseLine("workspace", `removed ${dir}`)));
|
|
2973
|
+
if (result.dereferenced.length) output.error(chalk.dim(phaseLine("workspace", `no longer referenced: ${result.dereferenced.join(", ")}`)));
|
|
2974
|
+
for (const skipped of result.skippedDevices) output.error((failed ? chalk.dim : chalk.yellow)(phaseLine("device", `kept ${describeKeptDevice(skipped)} (${skipped.reason})`)));
|
|
2486
2975
|
}
|
|
2487
2976
|
async function removeWorktreeTarget(target, opts = {}) {
|
|
2488
2977
|
let removed = false;
|
|
@@ -2492,200 +2981,245 @@ async function removeWorktreeTarget(target, opts = {}) {
|
|
|
2492
2981
|
return removed;
|
|
2493
2982
|
}
|
|
2494
2983
|
async function runRemove(target, opts, onRemoved) {
|
|
2984
|
+
const output = opts.output ?? REMOVAL_OUTPUT;
|
|
2495
2985
|
try {
|
|
2496
|
-
await removeOrRefuse(target, opts, onRemoved);
|
|
2986
|
+
await removeOrRefuse(target, opts, onRemoved, output);
|
|
2497
2987
|
} catch (error) {
|
|
2988
|
+
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
2989
|
+
opts.operation?.signal.throwIfAborted();
|
|
2498
2990
|
if (!isTimeoutError(error)) throw error;
|
|
2499
|
-
printRemovalRefusal(removalPath(target), timedOutInspection(error));
|
|
2991
|
+
await printRemovalRefusal(removalPath(target, opts.operation?.cwd), timedOutInspection(error), output, opts.operation);
|
|
2500
2992
|
}
|
|
2501
2993
|
}
|
|
2502
|
-
async function removeOrRefuse(target, opts, onRemoved) {
|
|
2994
|
+
async function removeOrRefuse(target, opts, onRemoved, output) {
|
|
2995
|
+
const options = {
|
|
2996
|
+
removal: true,
|
|
2997
|
+
env: opts.operation?.env,
|
|
2998
|
+
signal: opts.operation?.signal,
|
|
2999
|
+
declareFor: opts.operation
|
|
3000
|
+
};
|
|
3001
|
+
options.signal?.throwIfAborted();
|
|
2503
3002
|
const poolError = parkedMaxSetting("ios").error || parkedMaxSetting("android").error;
|
|
2504
3003
|
if (poolError) {
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
3004
|
+
output.error(chalk.red(poolError));
|
|
3005
|
+
output.error(chalk.dim(POOL_SETTING_REMEDY));
|
|
3006
|
+
output.fail();
|
|
2508
3007
|
return;
|
|
2509
3008
|
}
|
|
2510
|
-
let path = removalPath(target);
|
|
3009
|
+
let path = removalPath(target, opts.operation?.cwd);
|
|
2511
3010
|
if (!existsSync$1(path)) {
|
|
3011
|
+
opts.beforeRemove?.(path);
|
|
2512
3012
|
const pending = getProject(path);
|
|
2513
3013
|
if (pending?.worktreeRemovalComplete === true && pending.worktreeBranchOwned === true && pending.worktreeBranch && pending.worktreeMainRoot && pending.worktreePendingBranchSha) {
|
|
2514
3014
|
const branch = pending.worktreeBranch;
|
|
2515
3015
|
const mainRoot = pending.worktreeMainRoot;
|
|
2516
3016
|
if (!existsSync$1(mainRoot)) {
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
3017
|
+
output.error(chalk.red(`Cannot finish branch cleanup for ${path}: source checkout ${mainRoot} is missing.`));
|
|
3018
|
+
output.error(chalk.dim(`Delete ${branch} from the repository, then run \`stim gc --delete\`.`));
|
|
3019
|
+
output.fail();
|
|
2520
3020
|
return;
|
|
2521
3021
|
}
|
|
2522
|
-
|
|
3022
|
+
opts.beforeRemove?.(removalPath(mainRoot));
|
|
3023
|
+
if (!await branchExistsAsync(mainRoot, branch, options)) {
|
|
2523
3024
|
removeProject(path);
|
|
2524
|
-
|
|
3025
|
+
output.error(chalk.dim(phaseLine("branch", `${branch} is already absent; cleared its pending cleanup record`)));
|
|
2525
3026
|
return;
|
|
2526
3027
|
}
|
|
2527
|
-
const checkedOutAt =
|
|
3028
|
+
const checkedOutAt = (await listWorktreesAsync(mainRoot, options)).find((candidate) => candidate.branch === branch && resolve$1(candidate.path) !== resolve$1(path))?.path;
|
|
2528
3029
|
if (checkedOutAt) {
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
3030
|
+
output.error(chalk.red(`Refusing pending cleanup for ${branch}: it is checked out at ${checkedOutAt}.`));
|
|
3031
|
+
output.error(chalk.dim("Switch that worktree to another branch, then retry this command."));
|
|
3032
|
+
output.fail();
|
|
2532
3033
|
return;
|
|
2533
3034
|
}
|
|
2534
|
-
const currentSha =
|
|
3035
|
+
const currentSha = await resolveFullRefAsync(mainRoot, branch, options);
|
|
2535
3036
|
if (currentSha !== pending.worktreePendingBranchSha) {
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
3037
|
+
output.error(chalk.red(`Refusing pending cleanup for ${branch}: its tip changed from ${pending.worktreePendingBranchSha} to ${currentSha || "an unknown commit"}.`));
|
|
3038
|
+
output.error(chalk.dim("The branch can contain new work. Inspect it and delete it manually if appropriate."));
|
|
3039
|
+
output.fail();
|
|
2539
3040
|
return;
|
|
2540
3041
|
}
|
|
2541
3042
|
try {
|
|
2542
|
-
|
|
3043
|
+
await deleteBranchAsync(mainRoot, branch, pending.worktreePendingBranchSha, options);
|
|
2543
3044
|
removeProject(path);
|
|
2544
|
-
|
|
3045
|
+
output.error(chalk.dim(phaseLine("branch", `deleted ${branch}; cleared its pending cleanup record`)));
|
|
2545
3046
|
} catch (error) {
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
3047
|
+
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
3048
|
+
output.error(chalk.red(`Could not delete branch ${branch}: ${String(error?.message || error)}`));
|
|
3049
|
+
output.error(chalk.dim(`Retry with: stim worktree remove ${path}`));
|
|
3050
|
+
output.fail();
|
|
2549
3051
|
}
|
|
2550
3052
|
return;
|
|
2551
3053
|
}
|
|
2552
|
-
|
|
2553
|
-
|
|
3054
|
+
output.error(chalk.red(`No such worktree: ${path}`));
|
|
3055
|
+
output.fail();
|
|
2554
3056
|
return;
|
|
2555
3057
|
}
|
|
2556
|
-
const worktrees =
|
|
3058
|
+
const worktrees = await listWorktreesAsync(path, options);
|
|
2557
3059
|
const entry = matchWorktreeEntry(worktrees, path);
|
|
2558
3060
|
if (!entry) {
|
|
2559
|
-
if (!opts.linkedOnly &&
|
|
2560
|
-
await reclaimEnvironment(path, "it is not a git repository");
|
|
3061
|
+
if (!opts.linkedOnly && await gitCommonDirAsync(path, options) === null && hasRegisteredProjectUnder(path)) {
|
|
3062
|
+
await reclaimEnvironment(path, "it is not a git repository", output, opts.operation, opts.beforeRemove);
|
|
2561
3063
|
return;
|
|
2562
3064
|
}
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
3065
|
+
output.error(chalk.red(`Refusing to remove ${path}: it is not inside any worktree known to git.`));
|
|
3066
|
+
output.error(chalk.dim(" Run it from inside the worktree, or pass the worktree root path, e.g. as printed by `git worktree list`."));
|
|
3067
|
+
output.fail();
|
|
2566
3068
|
return;
|
|
2567
3069
|
}
|
|
2568
3070
|
if (entry.bare) {
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
3071
|
+
output.error(chalk.red(`Refusing to remove ${path}: ${entry.path} is the bare repository, not a worktree.`));
|
|
3072
|
+
output.error(chalk.dim(" Pass a worktree path, e.g. as printed by `git worktree list`."));
|
|
3073
|
+
output.fail();
|
|
2572
3074
|
return;
|
|
2573
3075
|
}
|
|
2574
3076
|
const source = sourceCheckoutOf(worktrees);
|
|
2575
3077
|
if ("refusal" in source) {
|
|
2576
|
-
|
|
2577
|
-
|
|
3078
|
+
output.error(chalk.red(`Refusing to remove ${path}: ${source.refusal}`));
|
|
3079
|
+
output.fail();
|
|
2578
3080
|
return;
|
|
2579
3081
|
}
|
|
2580
3082
|
if (entry.path === source.path && opts.linkedOnly) {
|
|
2581
|
-
|
|
2582
|
-
|
|
3083
|
+
output.error(chalk.red(`Refusing to remove ${path}: ${entry.path} is the source checkout.`));
|
|
3084
|
+
output.fail();
|
|
2583
3085
|
return;
|
|
2584
3086
|
}
|
|
2585
3087
|
if (entry.path === source.path) {
|
|
2586
3088
|
if (nativeCanonicalPath(entry.path) !== nativeCanonicalPath(path)) {
|
|
2587
3089
|
const project = projectAtOrAbove(entry.path, path);
|
|
2588
3090
|
if (!project) {
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
3091
|
+
output.error(chalk.red(`Refusing to reclaim from ${path}: no Stim project is registered at or above it.`));
|
|
3092
|
+
output.error(chalk.dim(` Run it from the project folder, or from ${entry.path} to reclaim every project under it.`));
|
|
3093
|
+
output.fail();
|
|
2592
3094
|
return;
|
|
2593
3095
|
}
|
|
2594
|
-
|
|
2595
|
-
await reclaimEnvironment(project, "it is the source checkout");
|
|
3096
|
+
output.error(chalk.dim(`${path} is inside the source checkout ${entry.path}; reclaiming only the project ${project}.`));
|
|
3097
|
+
await reclaimEnvironment(project, "it is the source checkout", output, opts.operation, opts.beforeRemove);
|
|
2596
3098
|
return;
|
|
2597
3099
|
}
|
|
2598
|
-
await reclaimEnvironment(entry.path, "it is the source checkout");
|
|
3100
|
+
await reclaimEnvironment(entry.path, "it is the source checkout", output, opts.operation, opts.beforeRemove);
|
|
2599
3101
|
return;
|
|
2600
3102
|
}
|
|
2601
3103
|
if (entry.path !== path) {
|
|
2602
|
-
|
|
3104
|
+
output.error(chalk.dim(`${path} is inside the worktree ${entry.path}; removing that.`));
|
|
2603
3105
|
path = entry.path;
|
|
2604
3106
|
}
|
|
2605
3107
|
if (entry.locked) {
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
3108
|
+
output.error(chalk.red(`Refusing to remove ${path}: git has it locked.`));
|
|
3109
|
+
output.error(chalk.dim(`Unlock it, then retry: git -C ${source.path} worktree unlock ${path}`));
|
|
3110
|
+
output.fail();
|
|
3111
|
+
return;
|
|
3112
|
+
}
|
|
3113
|
+
const nested = nestedWorktrees(worktrees, entry.path);
|
|
3114
|
+
if (nested.length) {
|
|
3115
|
+
output.error(chalk.red(`Refusing to remove ${path}: it contains the worktree ${nested.join(", ")}.`));
|
|
3116
|
+
output.error(chalk.dim("Remove the nested worktree first, then retry."));
|
|
3117
|
+
output.fail();
|
|
2609
3118
|
return;
|
|
2610
3119
|
}
|
|
2611
3120
|
const service = serviceRunningFrom(path, nativeCanonicalPath);
|
|
2612
3121
|
if (service) {
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
3122
|
+
output.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}`));
|
|
3123
|
+
output.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.`));
|
|
3124
|
+
output.fail();
|
|
2616
3125
|
return;
|
|
2617
3126
|
}
|
|
2618
3127
|
const project = getProject(path);
|
|
2619
3128
|
const branch = entry.branch;
|
|
2620
3129
|
const ownsBranch = Boolean(branch && project?.worktreeBranchOwned === true && project.worktreeBranch === branch);
|
|
2621
|
-
const approvedBranchSha = ownsBranch ?
|
|
3130
|
+
const approvedBranchSha = ownsBranch ? await resolveFullRefAsync(path, "HEAD", options) : null;
|
|
2622
3131
|
let mergedHead = opts.mergedHead;
|
|
2623
|
-
let inspection = inspectRemoval(path, mergedHead);
|
|
3132
|
+
let inspection = await inspectRemoval(path, mergedHead, opts.operation);
|
|
2624
3133
|
if (inspection.blockers.length && !opts.force && !mergedHead && branch && inspection.unpushed?.length && !inspection.dirtyLines.length) {
|
|
2625
|
-
const head =
|
|
2626
|
-
const pr = head ? endedPullRequest(
|
|
3134
|
+
const head = await resolveFullRefAsync(path, "HEAD", options);
|
|
3135
|
+
const pr = head ? endedPullRequest(await pullRequestLookupAsync(opts.operation)(path, branch, head)) : null;
|
|
2627
3136
|
if (head && pr?.state === "merged") {
|
|
2628
3137
|
mergedHead = head;
|
|
2629
|
-
inspection = inspectRemoval(path, mergedHead);
|
|
2630
|
-
|
|
3138
|
+
inspection = await inspectRemoval(path, mergedHead, opts.operation);
|
|
3139
|
+
output.error(chalk.dim(`HEAD is in the merged PR #${pr.number} (${pr.url}); its local-only commits stay on GitHub.`));
|
|
2631
3140
|
}
|
|
2632
3141
|
}
|
|
2633
3142
|
if (inspection.blockers.length && !opts.force) {
|
|
2634
|
-
printRemovalRefusal(path, inspection);
|
|
3143
|
+
await printRemovalRefusal(path, inspection, output, opts.operation);
|
|
2635
3144
|
return;
|
|
2636
3145
|
}
|
|
2637
3146
|
const deleteOwnedBranch = Boolean(ownsBranch && approvedBranchSha && inspection.unpushed?.length === 0);
|
|
2638
3147
|
const retainedBranchReason = !branch ? null : !ownsBranch ? "Stim did not create it" : inspection.unpushed === null ? "Stim could not verify whether it has unique commits" : inspection.unpushed.length > 0 ? `it has ${plural(inspection.unpushed.length, "unique commit")}` : null;
|
|
2639
3148
|
const branchDeleteCwd = source.path;
|
|
2640
3149
|
await withManagedRemoteWorktreeRemovalLock(path, () => withReclaimLocks(path, async (lockedKeys) => {
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
3150
|
+
options.signal?.throwIfAborted();
|
|
3151
|
+
if ((await opts.guard?.(lockedKeys))?.length) return;
|
|
3152
|
+
options.signal?.throwIfAborted();
|
|
3153
|
+
const inspectedHead = await resolveFullRefAsync(path, "HEAD", options);
|
|
3154
|
+
if (opts.expectedHead && inspectedHead !== opts.expectedHead) {
|
|
3155
|
+
output.error(chalk.red(`Refusing to remove ${path}: its HEAD moved since it was checked.`));
|
|
3156
|
+
output.fail();
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
const current = await inspectRemoval(path, mergedHead, opts.operation);
|
|
2644
3160
|
if (current.blockers.length && !opts.force) {
|
|
2645
|
-
printRemovalRefusal(path, current);
|
|
3161
|
+
await printRemovalRefusal(path, current, output, opts.operation);
|
|
2646
3162
|
return;
|
|
2647
3163
|
}
|
|
2648
3164
|
const result = await reclaimAll(path, lockedKeys, {
|
|
2649
3165
|
preserveRootProject: true,
|
|
2650
|
-
removedBy: opts.removedBy ?? "worktree-remove"
|
|
3166
|
+
removedBy: opts.removedBy ?? "worktree-remove",
|
|
3167
|
+
operation: opts.operation
|
|
2651
3168
|
});
|
|
2652
3169
|
if (result.keptEntries.length) {
|
|
2653
|
-
reportRetainedResources(path, result);
|
|
3170
|
+
reportRetainedResources(path, result, output);
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
3173
|
+
let headMoved = false;
|
|
3174
|
+
if (!opts.force) try {
|
|
3175
|
+
headMoved = await resolveFullRefAsync(path, "HEAD", options) !== inspectedHead;
|
|
3176
|
+
} catch (error) {
|
|
3177
|
+
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
3178
|
+
opts.operation?.signal.throwIfAborted();
|
|
3179
|
+
if (!isTimeoutError(error)) throw error;
|
|
3180
|
+
await printRemovalRefusal(path, timedOutInspection(error), output, opts.operation);
|
|
3181
|
+
output.error(chalk.dim(`The directory and Stim ownership record for ${path} were kept.`));
|
|
3182
|
+
printRemovalCleanup(result, true, output);
|
|
2654
3183
|
return;
|
|
2655
3184
|
}
|
|
2656
|
-
if (
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
printRemovalCleanup(result, true);
|
|
2660
|
-
|
|
3185
|
+
if (headMoved) {
|
|
3186
|
+
output.error(chalk.red(`Refusing to remove ${path}: its HEAD moved while its environment was reclaimed.`));
|
|
3187
|
+
output.error(chalk.dim(`The directory and Stim ownership record for ${path} were kept.`));
|
|
3188
|
+
printRemovalCleanup(result, true, output);
|
|
3189
|
+
output.fail();
|
|
2661
3190
|
return;
|
|
2662
3191
|
}
|
|
2663
|
-
restorePodChurn(path, current.podChurn);
|
|
3192
|
+
await restorePodChurn(path, current.podChurn, output, opts.operation);
|
|
3193
|
+
options.signal?.throwIfAborted();
|
|
2664
3194
|
deleteWatchmanCookies(path, current.cookies);
|
|
2665
3195
|
try {
|
|
2666
|
-
|
|
3196
|
+
await removeWorktreeAsync(path, {
|
|
2667
3197
|
from: source.path,
|
|
2668
|
-
force: opts.force
|
|
3198
|
+
force: opts.force,
|
|
3199
|
+
env: options.env,
|
|
3200
|
+
signal: options.signal,
|
|
3201
|
+
declareFor: opts.operation
|
|
2669
3202
|
});
|
|
2670
3203
|
} catch (error) {
|
|
3204
|
+
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
2671
3205
|
const message = String(error?.message || error);
|
|
2672
|
-
|
|
2673
|
-
if (process.platform === "win32" && /Permission denied/i.test(message))
|
|
2674
|
-
|
|
2675
|
-
printRemovalCleanup(result, true);
|
|
2676
|
-
|
|
3206
|
+
output.error(chalk.red(`git worktree remove failed: ${message}`));
|
|
3207
|
+
if (process.platform === "win32" && /Permission denied/i.test(message)) output.error(chalk.dim("Another process holds a file or directory inside the worktree. An adb server or emulator started from it keeps its working directory open; `adb kill-server` releases the server, and Sysinternals handle.exe lists other holders."));
|
|
3208
|
+
output.error(chalk.dim(`The directory and Stim ownership record for ${path} were kept.`));
|
|
3209
|
+
printRemovalCleanup(result, true, output);
|
|
3210
|
+
output.fail();
|
|
2677
3211
|
return;
|
|
2678
3212
|
}
|
|
2679
3213
|
onRemoved();
|
|
2680
3214
|
forgetStatusMeasures(path, result.reclaimedKeys);
|
|
2681
3215
|
const finish = () => {
|
|
2682
|
-
printRemovalCleanup(result, false);
|
|
2683
|
-
|
|
3216
|
+
printRemovalCleanup(result, false, output);
|
|
3217
|
+
output.error(chalk.dim(phaseLine("removed", path)));
|
|
2684
3218
|
};
|
|
2685
3219
|
if (deleteOwnedBranch && branch) {
|
|
2686
3220
|
if (!approvedBranchSha) {
|
|
2687
|
-
|
|
2688
|
-
|
|
3221
|
+
output.error(chalk.yellow(phaseLine("branch", `kept ${branch} (Stim could not record its commit before removal)`)));
|
|
3222
|
+
output.fail();
|
|
2689
3223
|
finish();
|
|
2690
3224
|
return;
|
|
2691
3225
|
}
|
|
@@ -2693,28 +3227,44 @@ async function removeOrRefuse(target, opts, onRemoved) {
|
|
|
2693
3227
|
worktreeRemovalComplete: true,
|
|
2694
3228
|
worktreePendingBranchSha: approvedBranchSha
|
|
2695
3229
|
});
|
|
2696
|
-
|
|
3230
|
+
let checkedOutAt;
|
|
3231
|
+
try {
|
|
3232
|
+
checkedOutAt = (await listWorktreesAsync(branchDeleteCwd, options)).find((candidate) => candidate.branch === branch && resolve$1(candidate.path) !== resolve$1(path))?.path;
|
|
3233
|
+
} catch (error) {
|
|
3234
|
+
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
3235
|
+
opts.operation?.signal.throwIfAborted();
|
|
3236
|
+
if (!isTimeoutError(error)) throw error;
|
|
3237
|
+
output.error(chalk.yellow(phaseLine("branch", `kept ${branch} (git did not list the worktrees in time to check it)`)));
|
|
3238
|
+
output.error(chalk.dim(` Retry: stim worktree remove ${path}`));
|
|
3239
|
+
output.fail();
|
|
3240
|
+
finish();
|
|
3241
|
+
return;
|
|
3242
|
+
}
|
|
2697
3243
|
if (checkedOutAt) {
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
3244
|
+
output.error(chalk.yellow(phaseLine("branch", `kept ${branch} (it is checked out at ${checkedOutAt})`)));
|
|
3245
|
+
output.error(chalk.dim(` Switch that worktree to another branch, then retry: stim worktree remove ${path}`));
|
|
3246
|
+
output.fail();
|
|
2701
3247
|
finish();
|
|
2702
3248
|
return;
|
|
2703
3249
|
}
|
|
2704
3250
|
try {
|
|
2705
|
-
|
|
2706
|
-
|
|
3251
|
+
await deleteBranchAsync(branchDeleteCwd, branch, approvedBranchSha, options);
|
|
3252
|
+
output.error(chalk.dim(phaseLine("branch", `deleted ${branch}`)));
|
|
2707
3253
|
} catch (error) {
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
3254
|
+
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
3255
|
+
output.error(chalk.yellow(phaseLine("branch", `kept ${branch} (${String(error?.message || error)})`)));
|
|
3256
|
+
output.error(chalk.dim(` Retry with: git -C ${branchDeleteCwd} branch -D -- ${branch}`));
|
|
3257
|
+
output.fail();
|
|
2711
3258
|
finish();
|
|
2712
3259
|
return;
|
|
2713
3260
|
}
|
|
2714
|
-
} else if (branch && retainedBranchReason)
|
|
3261
|
+
} else if (branch && retainedBranchReason) output.error(chalk.dim(phaseLine("branch", `kept ${branch} (${retainedBranchReason})`)));
|
|
2715
3262
|
removeProject(path);
|
|
2716
3263
|
finish();
|
|
2717
|
-
})
|
|
3264
|
+
}, opts.operation, opts.beforeRemove), {
|
|
3265
|
+
signal: opts.operation?.signal,
|
|
3266
|
+
declareSpawnsFor: opts.operation
|
|
3267
|
+
});
|
|
2718
3268
|
}
|
|
2719
3269
|
function registerRemove(worktree) {
|
|
2720
3270
|
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) => {
|
|
@@ -2727,4 +3277,4 @@ function worktreeCommand(program) {
|
|
|
2727
3277
|
registerRemove(worktree);
|
|
2728
3278
|
}
|
|
2729
3279
|
//#endregion
|
|
2730
|
-
export {
|
|
3280
|
+
export { levelFloors as A, measureDisk as C, measureSizes as D, measurePressureAsync as E, lowestFree as M, protectWindowMs as N, sizeScanDeferred as O, cacheBlocked as S, measurePressure as T, worktreeCommand as _, matchWorktreeEntry as a, deleteSelectedArchives as b, reclaimKeys as c, removalBlockers as d, removalPath as f, warmedWorkspace as g, warmWorktreeTarget as h, excludeWatchmanCookies as i, levelStillShort as j, diskPressureLevel as k, registerRemove as l, removeWorktreeTarget as m, dependencyInstallCommand as n, nestedWorktrees as o, removalRemedy as p, excludePodChurn as r, porcelainPath as s, customIosDirs as t, registerWarm as u, describeDereferenced as v, measureDiskAsync as w, sweepArchiveStaging as x, reclaimProject as y };
|