stim 1.21.0 → 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 +60 -7
- package/dist/{acquire-artifact-CD4lbiWE.mjs → acquire-artifact-Co2k4f7O.mjs} +21 -3
- package/dist/{act-DHAw2A2S.mjs → act-DsuqEkiM.mjs} +298 -164
- package/dist/{activity-BzVfHSrY.mjs → activity-CMLmzp7o.mjs} +33 -12
- package/dist/{agent-device-usage-output-zh1vhbLp.mjs → agent-device-usage-output-DUFaTMdd.mjs} +7 -7
- package/dist/{android-BLgbNqox.mjs → android-ByRMUoiJ.mjs} +66 -52
- package/dist/{android-DUYkNF4u.mjs → android-C7sVmyI9.mjs} +160 -59
- package/dist/{android-CV_GCELZ.mjs → android-DGkNqa_c.mjs} +5 -5
- package/dist/{android-BEJ8Jpl7.mjs → android-DR5lZ2Az.mjs} +2 -2
- package/dist/android-cas-compiler.mjs +1 -1
- package/dist/{android-cas-Ct5b1xCK.mjs → android-cas-ngferVq2.mjs} +3 -3
- package/dist/{client-B7QVOlCr.d.mts → android-project-DVWyCbWU.d.mts} +145 -7
- package/dist/api.d.mts +132 -118
- package/dist/api.mjs +239 -42
- package/dist/{artifact-B8T3S5gi.mjs → artifact-8EY8SAPw.mjs} +40 -35
- package/dist/{artifact-DAuZ-FNU.mjs → artifact-DJPy518Y.mjs} +69 -40
- package/dist/artifact-platform-CXcmX_j3.mjs +27 -0
- package/dist/{browser-web-QQzVjz3O.mjs → browser-web-Bcv99PBM.mjs} +187 -59
- package/dist/{budget-Bs6SxxzO.mjs → budget-Cvwn54qV.mjs} +830 -699
- package/dist/{build-CoPzQRNw.mjs → build-CabC3G23.mjs} +34 -27
- package/dist/{build-cache-DmyR9Zzc.mjs → build-cache-Dv7RN200.mjs} +4 -4
- package/dist/{build-plan-CaFskB7_.mjs → build-plan-DUC8Ka9y.mjs} +74 -14
- package/dist/{cache-manifest-DkkqguoK.mjs → cache-manifest-DSeQjt-Q.mjs} +1 -1
- package/dist/cache-manifest.mjs +1 -1
- package/dist/{ccache-BJRZ9pG3.mjs → ccache-Btwc36-I.mjs} +6 -6
- package/dist/{cdp-CWmIUMbw.mjs → cdp-3fwICjzi.mjs} +1 -1
- package/dist/{chrome-B6tfCta1.mjs → chrome-Bbw689-6.mjs} +1 -1
- package/dist/{cli-0DYf8QhQ.mjs → cli-CHZa9Lvk.mjs} +20 -20
- package/dist/cli.mjs +1 -1
- package/dist/{client-UXD70ZbM.mjs → client-OyRoEuSr.mjs} +218 -47
- package/dist/collector-run.d.mts +1 -1
- package/dist/collector-run.mjs +6 -6
- package/dist/{command-output-B1UrjZHL.mjs → command-output-gwpm28fu.mjs} +1 -1
- package/dist/{config-CYcBF478.mjs → config-DlrsduwG.mjs} +17 -11
- package/dist/{created-devices-CqeBlrEn.mjs → created-devices-x9rN6bhs.mjs} +13 -3
- package/dist/{dependency-state-BbKQW83h.mjs → dependency-state-1I9KZT1Y.mjs} +2 -2
- package/dist/{detached-entry-DFRyG1_o.mjs → detached-entry-BwNEtIrP.mjs} +1 -1
- package/dist/{dev-client-UOeESKm6.mjs → dev-client-BSDR5h3F.mjs} +2 -2
- package/dist/{device-ExJq8Ei7.mjs → device-B-L3Fbxy.mjs} +71 -14
- package/dist/{device-DCxH7_By.mjs → device-DklWZLqb.mjs} +16 -16
- package/dist/{device-capacity-CMlqEcvw.mjs → device-capacity-DK5_EwRS.mjs} +9 -10
- package/dist/device-host-worker.mjs +25 -32
- package/dist/{device-ios-DRUeKaFV.mjs → device-ios-DoB6KFCO.mjs} +39 -16
- package/dist/{device-lease-run-C8raGFoP.mjs → device-lease-run-xJPQ_QQG.mjs} +5 -5
- package/dist/{device-pool-DMRpVX9a.mjs → device-pool-BMkdsefq.mjs} +3 -3
- package/dist/{doctor-DTvaDOda.mjs → doctor-Bo7BXLux.mjs} +24 -37
- package/dist/{doctor-Dhqebm0q.mjs → doctor-D5_EzwfA.mjs} +19 -16
- package/dist/doctor-fingerprint-DZLoYFVb.mjs +16 -0
- package/dist/doctor-fingerprint.mjs +1 -1
- package/dist/{doctor-BLHCYxzi.mjs → doctor-thjWAV0z.mjs} +1 -1
- package/dist/{doctor-watchman-Cv1F10Kj.mjs → doctor-watchman-N2wDotwQ.mjs} +5 -5
- package/dist/{eas-build-BomfRnFT.mjs → eas-build-CwknaQYj.mjs} +4 -4
- package/dist/{eas-fallback-DJwZpy0c.mjs → eas-fallback-CSgAulr5.mjs} +27 -18
- package/dist/{eas-session-ledger-op6A54yi.mjs → eas-session-ledger-DlY_serQ.mjs} +8 -7
- package/dist/{error-diagnostics-Bf0rZvG9.mjs → error-diagnostics-DBWO2rxE.mjs} +10 -10
- package/dist/{exec-_UbtCQcG.mjs → exec-DEoAhcze.mjs} +155 -47
- package/dist/{exec-H20xSg2T.d.mts → exec-DnYc4HGH.d.mts} +17 -4
- package/dist/{gc-_-dCXdyL.mjs → gc-CXxN-4SE.mjs} +587 -313
- package/dist/gc-DnNDQyaP.mjs +3 -0
- package/dist/{guide-Yqwt2bd6.mjs → guide-Br0LEfKy.mjs} +239 -43
- package/dist/{hosted-android-Bw-honsd.mjs → hosted-android-B-9f-jkd.mjs} +6 -6
- package/dist/{hosted-client-DB_gBPfY.mjs → hosted-client-DZAPrcro.mjs} +4 -4
- package/dist/{hosted-ios-CUvOf4Vr.mjs → hosted-ios-CsCS21hn.mjs} +1 -1
- package/dist/{hosted-logs-DL_P5AdN.mjs → hosted-logs-FIRx9X2f.mjs} +5 -5
- package/dist/{hosted-macos-QI141yKk.mjs → hosted-macos-_awYSpfv.mjs} +21 -12
- package/dist/{hosted-native-Ksyjgzq-.mjs → hosted-native-BdzE668-.mjs} +27 -14
- package/dist/{idle-BVeRGroT.d.mts → idle-C4cZ757K.d.mts} +3 -3
- package/dist/{idle-shutdown-DaKmZi0z.mjs → idle-shutdown-DDg3SdvY.mjs} +7 -7
- package/dist/ios-CDvlmUs7.d.mts +24 -0
- package/dist/{ios-CQ68hG9s.mjs → ios-Ds1clbIt.mjs} +8 -8
- package/dist/{ios-device-CwFMehUS.mjs → ios-device--h_-8ENa.mjs} +3 -3
- package/dist/{ios-device-CXGP1bsD.mjs → ios-device-BXZ0c7Kb.mjs} +1 -1
- package/dist/{ios-CpByZy12.mjs → ios-hcBpnEGk.mjs} +81 -34
- package/dist/{ios-ubFsBzwq.mjs → ios-pm4nL6By.mjs} +59 -50
- package/dist/{ios-project-WuwwBdvW.mjs → ios-project-CZhKODF3.mjs} +5 -5
- package/dist/{ios-signing-DTTIChGs.mjs → ios-signing-BtAffm0A.mjs} +2 -2
- package/dist/{ios-state-DvFX-K_l.mjs → ios-state-Dtw5K2aI.mjs} +1 -1
- package/dist/{ios-qNJt4dFm.mjs → ios-xtlRHaeB.mjs} +1 -1
- package/dist/{js-swap-Cc1dttps.mjs → js-swap-Bwhh4S7V.mjs} +7 -29
- package/dist/{launch-Zq2mOSDy.mjs → launch-BjxemNpK.mjs} +40 -39
- package/dist/{launch-CD1yVc7U.mjs → launch-CCzXlc8r.mjs} +16 -16
- package/dist/{logs-BS00Anxf.mjs → logs-BfwwadsZ.mjs} +151 -24
- package/dist/{logs-CMm05HxN.mjs → logs-jTS_lHSo.mjs} +10 -17
- package/dist/{macos-B65--pPd.mjs → macos-DVlSSyUg.mjs} +1 -1
- package/dist/{macos-C-0qJ6zq.mjs → macos-NfV2D7dO.mjs} +143 -46
- package/dist/macos-run.mjs +1 -1
- package/dist/maintenance-run.mjs +8 -8
- package/dist/{metro-DEXpIhHC.mjs → metro-DiXtA3Ty.mjs} +59 -37
- package/dist/{metro-gateway-BMJNlj3E.mjs → metro-gateway-CKZqKqx7.mjs} +120 -22
- package/dist/{named-ports-BWTzt6mu.mjs → named-ports-DkEzOfzO.mjs} +40 -33
- package/dist/{native-android-JACyjTZs.mjs → native-android-CqgnOgsT.mjs} +43 -242
- package/dist/native-gradle-artifact-inputs-DHFnmrVf.mjs +270 -0
- package/dist/{native-gradle-inputs-Bep9JcSF.mjs → native-gradle-inputs-Dnh84Zbd.mjs} +3 -129
- package/dist/{native-run-DY_RLx3C.mjs → native-run-NwUuxbkg.mjs} +4 -5
- package/dist/{native-runtime-Dg1Q7tfF.mjs → native-runtime-B7KIafpM.mjs} +8 -7
- package/dist/native-source-Y0yLhFyQ.mjs +133 -0
- package/dist/{native-xcode-inputs-CB2iQeWq.mjs → native-xcode-inputs-D_VOfFL7.mjs} +2 -3
- package/dist/{native-xcode-ios-CXX39F7o.mjs → native-xcode-ios-BYDOHWLN.mjs} +27 -32
- package/dist/offload-worker.d.mts +65 -2
- package/dist/offload-worker.mjs +305 -73
- package/dist/operation-exec-BW0em61G.mjs +171 -0
- package/dist/{ownership-DEmM5vhh.mjs → ownership-C5fo5Edb.mjs} +3 -3
- package/dist/{page-CPaHeYqj.mjs → page-DBL2PiUU.mjs} +8 -4
- package/dist/{placement-log-BqGbTa61.mjs → placement-log-BnGgJRRZ.mjs} +1 -1
- package/dist/{plan-placement-Bwj-IjY1.mjs → plan-placement-DIsMtN-d.mjs} +12 -8
- package/dist/{ports-D-MmCTdv.mjs → ports-Cy1RkmuQ.mjs} +3 -3
- package/dist/{prebuild-uD_ipCxF.mjs → prebuild-D4JoEAEs.mjs} +49 -14
- package/dist/{project-BZNjmW_8.mjs → project-wM_XFTvE.mjs} +16 -16
- package/dist/{pull-request-Lfetn8Kr.mjs → pull-request-W1IuLXDt.mjs} +102 -20
- package/dist/{pull-request-zjCSAryH.d.mts → pull-request-eODyIqZF.d.mts} +6 -2
- package/dist/pull-requests.d.mts +2 -2
- package/dist/pull-requests.mjs +2 -2
- package/dist/{react-native-android-DuzH0cn3.mjs → react-native-android-6OZ7MSM7.mjs} +71 -48
- package/dist/{react-native-build-rhk4smwQ.mjs → react-native-build-Dvd8hC21.mjs} +8 -8
- package/dist/{react-native-doctor-CQpuIMso.mjs → react-native-doctor-DsoSF1Y9.mjs} +7 -7
- package/dist/{react-native-ios-Bhq1rJeV.mjs → react-native-ios-4u1Rn-GR.mjs} +40 -26
- package/dist/{recordings-DYteQSu4.mjs → recordings-CQmfzAh7.mjs} +1 -1
- package/dist/registration-BcRgF6Ox.mjs +51 -0
- package/dist/{reload-CmTRFJaX.mjs → reload-BzkYabB-.mjs} +16 -15
- package/dist/{reload-DzjSl5P_.mjs → reload-D7AVlb8N.mjs} +1 -1
- package/dist/{remote-ByYkXdGD.mjs → remote-DjGDmlTZ.mjs} +18 -15
- package/dist/{result-DNiS-tTP.mjs → result-D2Ykz_iD.mjs} +24 -23
- package/dist/{run-D23HmJMs.mjs → run-DidHghD1.mjs} +3 -2
- package/dist/{selection-CSl9i5qU.mjs → selection-DSMbWEX2.mjs} +6 -5
- package/dist/{server-bare-CxTrvDg8.mjs → server-bare-CafzuyFB.mjs} +2 -2
- package/dist/{server-command-CtzloH3v.mjs → server-command-C6QjeupS.mjs} +5 -5
- package/dist/{server-expo-DT_F_uVI.mjs → server-expo-CrjB29pq.mjs} +7 -5
- package/dist/server-expo-D58c9L0M.mjs +2 -0
- package/dist/{settings-CsJDEXgK.mjs → settings-C7O8A6ZI.mjs} +223 -243
- package/dist/{settings-T1gtc3bf.mjs → settings-DpR-1Wnn.mjs} +1 -1
- package/dist/{settings-write-HyVZ8eQ8.mjs → settings-write-CzD60rOD.mjs} +7 -6
- package/dist/settings.schema.json +13 -0
- package/dist/{simslim-Cgk17SXV.mjs → simslim-BNckWpXi.mjs} +2 -2
- package/dist/{slot-launch-DF8VfT64.mjs → slot-launch-CbfmNgM6.mjs} +6 -6
- package/dist/{stage-BIS0ydsV.mjs → stage-Bps5oius.mjs} +21 -4
- package/dist/{start-RChynRDp.mjs → start-Br-oxqMD.mjs} +1 -1
- package/dist/{start-7lpH7hmN.mjs → start-EojN4WFz.mjs} +622 -393
- package/dist/{state-C4ZtUisW.mjs → state--smjFSTL.mjs} +22 -7
- package/dist/{state-XMziexv2.mjs → state-08I3hw3_.mjs} +2 -2
- package/dist/{stats-53T7OTb9.mjs → stats-D3GerlFW.mjs} +5 -5
- package/dist/{stats-report-BFfMpWWF.mjs → stats-report-BzaPrRm6.mjs} +1 -1
- package/dist/{status-DqEUSSxs.mjs → status-BRROykC8.mjs} +2 -2
- package/dist/{status-CSi6tlRq.mjs → status-EvrK72L2.mjs} +1 -1
- package/dist/{status-BL7jVyAB.mjs → status-WBDajew2.mjs} +43 -29
- package/dist/{status-watch-GDCfqpTE.mjs → status-watch-Bg_u-pwR.mjs} +48 -16
- package/dist/status-worker.mjs +1 -1
- package/dist/{stim-desktop-C12we7lz.mjs → stim-desktop-D5OGpYZM.mjs} +2 -2
- package/dist/{stop-Bk_1qiV6.mjs → stop-DTZ9uTw3.mjs} +2 -2
- package/dist/{stop-D7h6jAXQ.mjs → stop-Dky10AEJ.mjs} +31 -22
- package/dist/{stop-CPzlM1L-.mjs → stop-Mv6W9FAk.mjs} +15 -16
- package/dist/supervisor-run.d.mts +3 -3
- package/dist/supervisor-run.mjs +28 -36
- package/dist/{support-Dlu37g6H.mjs → support-CNioewsd.mjs} +10 -10
- package/dist/{support-67vraa1Q.mjs → support-yUiOjnVY.mjs} +8 -8
- 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-Hp9sCpK4.mjs → toolchain-DpJMHq0n.mjs} +9 -6
- package/dist/{trigger-BAahxUPq.mjs → trigger-Cbyc41d_.mjs} +3 -3
- package/dist/trigger-yb_nFzPF.mjs +2 -0
- package/dist/{warm-progress-BHX5r0Ei.mjs → warm-progress-CA3TgT3-.mjs} +29 -16
- package/dist/web-CZ07gd77.mjs +2 -0
- package/dist/{web-CJmIj6AI.mjs → web-DKx6yoFA.mjs} +11 -10
- package/dist/web-run.mjs +11 -9
- package/dist/windows-report.mjs +1 -1
- package/dist/{workspace-state-Cjuigeeu.mjs → workspace-state-U08dEzhG.mjs} +3 -3
- package/dist/{workspaces-BccMFmGQ.mjs → workspaces-CpANmBlq.mjs} +594 -266
- package/dist/{worktree-DQ6Zi2Mv.mjs → worktree-CHecZ4Oq.mjs} +850 -551
- package/dist/worktree-ZqZVjD9K.mjs +2 -0
- package/dist/{xcode-Zlx3qjEs.mjs → xcode-BXn9-xYx.mjs} +5 -5
- package/package.json +7 -7
- package/shim/android-audio/Main.java +24 -0
- package/shim/android-audio/README.md +17 -0
- package/shim/android-audio.jar +0 -0
- package/dist/gc-C-WwL0yi.mjs +0 -3
- package/dist/git-C3iTsJaZ.mjs +0 -73
- package/dist/server-expo-C-90EhcG.mjs +0 -2
- package/dist/settings-DkdmlelE.d.mts +0 -15
- package/dist/swiftpm-macos-p8WZbxKU.mjs +0 -130
- package/dist/toolchain-DhKwp9iI.mjs +0 -2
- package/dist/trigger-Bf3uDcpv.mjs +0 -2
- package/dist/web-DknQETPs.mjs +0 -2
- package/dist/workspace-process-lock-1_1OoVW5.mjs +0 -57
- package/dist/worktree-CK2MpJW5.mjs +0 -2
|
@@ -1,45 +1,47 @@
|
|
|
1
|
-
import { f as
|
|
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
2
|
import { d as workspaceDir, l as workspaceAgentDeviceDir } from "./paths-DQtTgsBf.mjs";
|
|
3
|
-
import { E as upsertProject,
|
|
4
|
-
import { a as formatElapsed, h as shortUdid, i as formatDuration, l as phaseLine, p as releasedLeaseFact, u as plural } from "./command-output-
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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-
|
|
9
|
-
import { t as macosAppPresent } from "./state-
|
|
10
|
-
import {
|
|
11
|
-
import { o as readWebRecord } from "./state
|
|
12
|
-
import { 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-
|
|
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
13
|
import { t as canonicalPath } from "./paths-BsJMoPP2.mjs";
|
|
14
|
-
import { p as cacheUser } from "./native-run-
|
|
15
|
-
import { D as
|
|
16
|
-
import { D as releaseWorkspaceLeases, t as agentDeviceDaemon } from "./activity-
|
|
17
|
-
import {
|
|
18
|
-
import { F as readHostMemoryPressureAsync, P as readHostMemoryPressure } from "./ios-
|
|
19
|
-
import { a as workspaceLinkLine, o as workspaceLinks } from "./stim-desktop-
|
|
20
|
-
import { t as clearNamedPorts } from "./named-ports-
|
|
21
|
-
import { r as HEARTBEAT_INTERVAL_MS, x as podInstallCommand } from "./xcode-
|
|
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
22
|
import { t as resolveMaintenanceSettings } from "./settings-Djl-mpjX.mjs";
|
|
23
|
-
import { _ as
|
|
24
|
-
import { i as stopHostedAndroid } from "./hosted-android-
|
|
25
|
-
import { r as stopHostedIos } from "./hosted-ios-
|
|
26
|
-
import { n as stopMacosApp } from "./stop-
|
|
27
|
-
import { n as verifyCollectorOwnership } from "./ownership-
|
|
28
|
-
import { a as discoverXcodeProject } from "./react-native-build-
|
|
29
|
-
import { n as hasInstalledDependencies, r as installedNpmTreeIsValid, t as dependencyState } from "./dependency-state-
|
|
30
|
-
import { a as forgetStatusMeasures,
|
|
31
|
-
import {
|
|
32
|
-
import { r as workspaceRecordingEnabled } from "./recordings-
|
|
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";
|
|
33
|
+
import { claimRemoveCommand, clearFreeClaimSet, readClaimSet, releaseClaim, tryAcquireClaim } from "@stim-cli/core/ownership-claim";
|
|
33
34
|
import { createHash, randomUUID } from "node:crypto";
|
|
34
|
-
import {
|
|
35
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
35
36
|
import { cpus, freemem, homedir, loadavg, totalmem } from "node:os";
|
|
36
37
|
import { basename, dirname, join, relative } from "node:path";
|
|
37
|
-
import { buildCacheRoot, configDir, metroCacheRoot, unitLines, unitWords,
|
|
38
|
-
import { LAST_BUILD_KEYS, archiveDir, archiveEnabled, archiveLock, archiveRoot, archivedUsage, buildWorkerRoot, countErrorEntries, endedAgentSessionOf, isJsonObject,
|
|
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";
|
|
39
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";
|
|
40
|
-
import {
|
|
41
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
41
42
|
import chalk from "chalk";
|
|
42
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";
|
|
43
45
|
import { createHash as createHash$1 } from "crypto";
|
|
44
46
|
//#region src/maintenance/pressure.ts
|
|
45
47
|
/**
|
|
@@ -81,15 +83,22 @@ function measureDisk() {
|
|
|
81
83
|
buildWorkerRoot(loadConfig())
|
|
82
84
|
]);
|
|
83
85
|
}
|
|
86
|
+
function measureDiskAsync(operation) {
|
|
87
|
+
return readVolumeSpaceAsync([
|
|
88
|
+
configDir(),
|
|
89
|
+
...Object.keys(loadConfig()?.projects ?? {}),
|
|
90
|
+
buildWorkerRoot(loadConfig())
|
|
91
|
+
], operation.signal);
|
|
92
|
+
}
|
|
84
93
|
/** The disk pressure level of the fullest volume Stim writes to, measured now. */
|
|
85
|
-
function currentDiskLevel() {
|
|
86
|
-
return diskPressureLevel(lowestFree(
|
|
94
|
+
function currentDiskLevel(disk = measureDisk()) {
|
|
95
|
+
return diskPressureLevel(lowestFree(disk), resolveBudget().budget, resolveMaintenanceSettings());
|
|
87
96
|
}
|
|
88
97
|
function measurePressure(settings, previous, now) {
|
|
89
98
|
return pressureFromObservations(settings, previous, now, readHostMemoryPressure(), measureDisk());
|
|
90
99
|
}
|
|
91
100
|
async function measurePressureAsync(settings, previous, now, operation) {
|
|
92
|
-
return pressureFromObservations(settings, previous, now, await readHostMemoryPressureAsync(), await readVolumeSpaceAsync([
|
|
101
|
+
return pressureFromObservations(settings, previous, now, await readHostMemoryPressureAsync(void 0, void 0, operation.signal, operation), await readVolumeSpaceAsync([
|
|
93
102
|
configDir(),
|
|
94
103
|
...Object.keys(loadConfig()?.projects ?? {}),
|
|
95
104
|
buildWorkerRoot(loadConfig())
|
|
@@ -387,7 +396,7 @@ const KIND_PATH = {
|
|
|
387
396
|
recordings: "recordings",
|
|
388
397
|
agentActions: "agent-device/sessions"
|
|
389
398
|
};
|
|
390
|
-
function archiveSettings() {
|
|
399
|
+
function archiveSettings(env = process.env) {
|
|
391
400
|
const config = loadConfig();
|
|
392
401
|
return Object.fromEntries([
|
|
393
402
|
"maxAgeDays",
|
|
@@ -399,57 +408,63 @@ function archiveSettings() {
|
|
|
399
408
|
"recordings.maxTotalGb",
|
|
400
409
|
"agentActions.maxAgeDays"
|
|
401
410
|
].map((key) => {
|
|
402
|
-
const result = machineNumber(`archive.${key}`, config,
|
|
411
|
+
const result = machineNumber(`archive.${key}`, config, env);
|
|
403
412
|
if (result.error) throw new Error(result.error);
|
|
404
413
|
return [key, result.value];
|
|
405
414
|
}));
|
|
406
415
|
}
|
|
407
|
-
function enabled(root) {
|
|
416
|
+
function enabled(root, env) {
|
|
408
417
|
const config = loadConfig();
|
|
409
418
|
const project = config?.projects?.[root];
|
|
410
419
|
const present = existsSync(root);
|
|
411
420
|
const common = present ? gitCommonDirOnDisk(root) : null;
|
|
412
421
|
if (!present && Object.values(config?.repos ?? {}).some((repo) => repo.settings?.archive?.enabled === false)) return false;
|
|
413
|
-
return archiveEnabled(
|
|
422
|
+
return archiveEnabled(env, [
|
|
414
423
|
project?.settings,
|
|
415
424
|
common ? config?.repos?.[common]?.settings : void 0,
|
|
416
425
|
present ? readCommittedSettings(root) : void 0,
|
|
417
426
|
config
|
|
418
427
|
]);
|
|
419
428
|
}
|
|
420
|
-
function files(dir) {
|
|
429
|
+
async function files(dir, signal) {
|
|
430
|
+
signal?.throwIfAborted();
|
|
421
431
|
if (!existsSync(dir)) return [];
|
|
422
|
-
|
|
432
|
+
const found = [];
|
|
433
|
+
for (const name of await readdir(dir)) {
|
|
434
|
+
signal?.throwIfAborted();
|
|
423
435
|
const path = join(dir, name);
|
|
424
|
-
const
|
|
425
|
-
if (
|
|
426
|
-
|
|
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({
|
|
427
440
|
path,
|
|
428
|
-
bytes:
|
|
429
|
-
at:
|
|
430
|
-
}
|
|
431
|
-
}
|
|
441
|
+
bytes: entry.size,
|
|
442
|
+
at: entry.mtimeMs
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
return found;
|
|
432
446
|
}
|
|
433
|
-
function secureTree(dir) {
|
|
434
|
-
if (
|
|
435
|
-
|
|
436
|
-
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)) {
|
|
437
451
|
const path = join(dir, name);
|
|
438
|
-
const
|
|
439
|
-
if (
|
|
440
|
-
if (
|
|
441
|
-
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);
|
|
442
456
|
}
|
|
443
457
|
}
|
|
444
|
-
function writeRecord(dir, record) {
|
|
445
|
-
|
|
458
|
+
async function writeRecord(dir, record) {
|
|
459
|
+
let small = 0;
|
|
460
|
+
for (const name of [
|
|
446
461
|
"state.json",
|
|
447
462
|
"ended-agents.json",
|
|
448
463
|
"log-error-index.json"
|
|
449
|
-
]
|
|
464
|
+
]) {
|
|
450
465
|
const path = join(dir, name);
|
|
451
|
-
|
|
452
|
-
}
|
|
466
|
+
if (existsSync(path)) small += (await stat(path)).size;
|
|
467
|
+
}
|
|
453
468
|
for (let pass = 0; pass < 8; pass++) {
|
|
454
469
|
record.bytes.total = KINDS.reduce((sum, kind) => sum + record.bytes[kind], record.bytes.record);
|
|
455
470
|
const bytes = small + Buffer.byteLength(`${JSON.stringify(record)}\n`);
|
|
@@ -459,59 +474,70 @@ function writeRecord(dir, record) {
|
|
|
459
474
|
record.bytes.total = KINDS.reduce((sum, kind) => sum + record.bytes[kind], record.bytes.record);
|
|
460
475
|
const tmp = join(dir, `.archive-${randomUUID()}.tmp`);
|
|
461
476
|
try {
|
|
462
|
-
|
|
463
|
-
|
|
477
|
+
await writeFile(tmp, `${JSON.stringify(record)}\n`, { mode: 384 });
|
|
478
|
+
await rename(tmp, join(dir, "archive.json"));
|
|
464
479
|
} finally {
|
|
465
|
-
|
|
480
|
+
await rm(tmp, { force: true });
|
|
466
481
|
}
|
|
467
482
|
const now = /* @__PURE__ */ new Date();
|
|
468
|
-
|
|
483
|
+
await utimes(archiveRoot(), now, now);
|
|
469
484
|
}
|
|
470
|
-
function removeRecord(id) {
|
|
485
|
+
async function removeRecord(id) {
|
|
471
486
|
const dir = archiveDir(id);
|
|
472
487
|
if (!existsSync(dir)) return;
|
|
473
488
|
const removing = join(archiveRoot(), `.removing-${id}`);
|
|
474
|
-
|
|
475
|
-
|
|
489
|
+
await rename(dir, removing);
|
|
490
|
+
await rm(removing, {
|
|
476
491
|
recursive: true,
|
|
477
492
|
force: true
|
|
478
493
|
});
|
|
479
494
|
}
|
|
480
|
-
function sweepArchiveStaging(remove, waitMs = 0) {
|
|
495
|
+
async function sweepArchiveStaging(remove, waitMs = 0, signal) {
|
|
496
|
+
signal?.throwIfAborted();
|
|
481
497
|
if (!existsSync(archiveRoot())) return [];
|
|
482
|
-
return
|
|
483
|
-
const
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
root
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
})
|
|
503
|
-
|
|
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
|
+
}
|
|
504
526
|
}
|
|
527
|
+
entries.push(entry);
|
|
505
528
|
}
|
|
506
|
-
return
|
|
507
|
-
}
|
|
529
|
+
return entries;
|
|
530
|
+
}, {
|
|
531
|
+
waitMs,
|
|
532
|
+
signal
|
|
533
|
+
});
|
|
508
534
|
}
|
|
509
535
|
function stripPids(value) {
|
|
510
536
|
if (Array.isArray(value)) return value.map(stripPids);
|
|
511
537
|
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).filter(([key]) => !/pids?$/i.test(key)).map(([key, child]) => [key, stripPids(child)]));
|
|
512
538
|
return value;
|
|
513
539
|
}
|
|
514
|
-
function gitFacts(root) {
|
|
540
|
+
async function gitFacts(root, operation) {
|
|
515
541
|
const empty = {
|
|
516
542
|
repository: null,
|
|
517
543
|
branch: null,
|
|
@@ -521,32 +547,55 @@ function gitFacts(root) {
|
|
|
521
547
|
pullRequest: null
|
|
522
548
|
};
|
|
523
549
|
if (!existsSync(root)) return cachedFacts(root) ?? empty;
|
|
524
|
-
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
|
+
};
|
|
525
571
|
const common = gitCommonDirOnDisk(root);
|
|
526
|
-
const summary = run([
|
|
572
|
+
const summary = (await run([
|
|
527
573
|
"show",
|
|
528
574
|
"-s",
|
|
529
575
|
"--format=%H%n%s",
|
|
530
576
|
"HEAD"
|
|
531
|
-
])?.split("\n");
|
|
532
|
-
const location = run([
|
|
577
|
+
]))?.split("\n");
|
|
578
|
+
const location = (await run([
|
|
533
579
|
"rev-parse",
|
|
534
580
|
"--show-toplevel",
|
|
535
581
|
"--abbrev-ref",
|
|
536
582
|
"HEAD"
|
|
537
|
-
])?.split("\n");
|
|
583
|
+
]))?.split("\n");
|
|
538
584
|
const branch = location?.[1] && location[1] !== "HEAD" ? location[1] : null;
|
|
539
|
-
const defaultRef = run([
|
|
585
|
+
const defaultRef = await run([
|
|
540
586
|
"symbolic-ref",
|
|
541
587
|
"--quiet",
|
|
542
588
|
"refs/remotes/origin/HEAD"
|
|
543
589
|
]);
|
|
544
590
|
let merged = null;
|
|
545
591
|
if (defaultRef?.startsWith("refs/remotes/")) {
|
|
546
|
-
const state =
|
|
592
|
+
const state = await mergeStateAsync(root, {
|
|
547
593
|
ref: defaultRef,
|
|
548
594
|
name: defaultRef.slice(13)
|
|
549
|
-
}, {
|
|
595
|
+
}, {
|
|
596
|
+
...execution,
|
|
597
|
+
timeoutMs: 2e3
|
|
598
|
+
});
|
|
550
599
|
merged = state.merged ? true : state.unknown ? null : false;
|
|
551
600
|
}
|
|
552
601
|
let prPath = root;
|
|
@@ -587,35 +636,48 @@ function cachedFacts(root) {
|
|
|
587
636
|
}
|
|
588
637
|
return null;
|
|
589
638
|
}
|
|
590
|
-
function
|
|
591
|
-
|
|
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();
|
|
592
648
|
try {
|
|
593
|
-
enforceArchiveRetention();
|
|
649
|
+
await enforceArchiveRetention(void 0, operation);
|
|
594
650
|
} catch (error) {
|
|
595
|
-
|
|
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);
|
|
596
654
|
}
|
|
597
655
|
}
|
|
598
|
-
function writeArchive(root, removedBy, state) {
|
|
656
|
+
async function writeArchive(root, removedBy, state, operation) {
|
|
657
|
+
const env = operation?.env ?? process.env;
|
|
599
658
|
let staging = null;
|
|
600
659
|
let claim;
|
|
601
660
|
try {
|
|
602
661
|
const dir = workspaceDir(root);
|
|
603
|
-
if (!existsSync(dir) ||
|
|
662
|
+
if (!existsSync(dir) || (await lstat(dir)).isSymbolicLink()) return;
|
|
604
663
|
if (existsSync(root)) root = realpathSync(root);
|
|
605
664
|
try {
|
|
606
|
-
sweepArchiveStaging(true, 5e3);
|
|
607
|
-
} catch {
|
|
608
|
-
|
|
609
|
-
|
|
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);
|
|
610
671
|
if (!limits.maxAgeDays || !limits.maxCount) return;
|
|
611
672
|
const logs = join(dir, "logs");
|
|
612
|
-
if (!existsSync(join(dir, "state.json")) && !Object.values(readBuildHistory(state)).some((entries) => entries?.length) && (!existsSync(logs) ||
|
|
613
|
-
const worktree = gitFacts(root);
|
|
614
|
-
|
|
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(), {
|
|
615
677
|
recursive: true,
|
|
616
678
|
mode: 448
|
|
617
679
|
});
|
|
618
|
-
|
|
680
|
+
await chmod(archiveRoot(), 448);
|
|
619
681
|
const token = randomUUID();
|
|
620
682
|
staging = join(archiveRoot(), `.incoming-${token}`);
|
|
621
683
|
const attempt = tryAcquireClaim({
|
|
@@ -628,49 +690,56 @@ function writeArchive(root, removedBy, state) {
|
|
|
628
690
|
releaseClaim(attempt.pending);
|
|
629
691
|
throw new Error("archive staging claim is held");
|
|
630
692
|
}
|
|
631
|
-
|
|
632
|
-
|
|
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 });
|
|
633
702
|
let at = Date.now();
|
|
634
703
|
const workspace = workspaceName(root);
|
|
635
704
|
while (existsSync(archiveDir(`${workspace}--${at}`))) at++;
|
|
636
705
|
const id = `${workspace}--${at}`;
|
|
637
706
|
if (state) {
|
|
638
707
|
const { supervisor: _supervisor, collectors: _collectors, warm: _warm, ...stable } = state;
|
|
639
|
-
|
|
708
|
+
await writeFile(join(staging, "state.json"), `${JSON.stringify(stripPids(stable))}\n`, { mode: 384 });
|
|
640
709
|
}
|
|
641
710
|
for (const name of ["ended-agents.json", "log-error-index.json"]) {
|
|
642
711
|
const data = readJsonObject(join(dir, name));
|
|
643
|
-
if (data)
|
|
712
|
+
if (data) await writeFile(join(staging, name), `${JSON.stringify(stripPids(data))}\n`, { mode: 384 });
|
|
644
713
|
}
|
|
645
|
-
const move = (kind) => {
|
|
714
|
+
const move = async (kind) => {
|
|
646
715
|
const from = join(dir, KIND_PATH[kind]);
|
|
647
716
|
if (!limits[`${kind}.maxAgeDays`] || !existsSync(from)) return;
|
|
648
717
|
const to = join(staging, KIND_PATH[kind]);
|
|
649
|
-
|
|
718
|
+
await mkdir(join(to, ".."), {
|
|
650
719
|
recursive: true,
|
|
651
720
|
mode: 448
|
|
652
721
|
});
|
|
653
|
-
|
|
654
|
-
secureTree(to);
|
|
722
|
+
await moveHistory(from, to);
|
|
723
|
+
await secureTree(to);
|
|
655
724
|
};
|
|
656
|
-
move("logs");
|
|
657
|
-
move("agentActions");
|
|
725
|
+
await move("logs");
|
|
726
|
+
await move("agentActions");
|
|
658
727
|
const config = loadConfig();
|
|
659
|
-
if (limits["recordings.maxAgeDays"] && workspaceRecordingEnabled(root, config?.projects?.[root] ?? {}, config,
|
|
728
|
+
if (limits["recordings.maxAgeDays"] && workspaceRecordingEnabled(root, config?.projects?.[root] ?? {}, config, env)) {
|
|
660
729
|
const recordingDir = join(dir, "recordings");
|
|
661
|
-
if (existsSync(recordingDir) &&
|
|
662
|
-
for (const device of
|
|
663
|
-
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}`);
|
|
664
733
|
const to = join(staging, "recordings", `${device.platform}-${device.slot}`);
|
|
665
734
|
for (const segment of device.segments.filter((entry) => !entry.open)) {
|
|
666
|
-
|
|
735
|
+
await mkdir(to, {
|
|
667
736
|
recursive: true,
|
|
668
737
|
mode: 448
|
|
669
738
|
});
|
|
670
739
|
try {
|
|
671
|
-
if (
|
|
672
|
-
|
|
673
|
-
|
|
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);
|
|
674
743
|
} catch (error) {
|
|
675
744
|
if (error.code !== "ENOENT") throw error;
|
|
676
745
|
}
|
|
@@ -699,7 +768,7 @@ function writeArchive(root, removedBy, state) {
|
|
|
699
768
|
record: new Date(at + limits.maxAgeDays * DAY).toISOString()
|
|
700
769
|
};
|
|
701
770
|
for (const kind of KINDS) {
|
|
702
|
-
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);
|
|
703
772
|
if (bytes[kind]) expires[kind] = new Date(at + limits[`${kind}.maxAgeDays`] * DAY).toISOString();
|
|
704
773
|
}
|
|
705
774
|
const record = {
|
|
@@ -721,102 +790,144 @@ function writeArchive(root, removedBy, state) {
|
|
|
721
790
|
expires,
|
|
722
791
|
version: 1
|
|
723
792
|
};
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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
|
+
});
|
|
727
800
|
releaseClaim(claim);
|
|
728
801
|
claim = void 0;
|
|
729
802
|
staging = null;
|
|
730
803
|
} catch (error) {
|
|
804
|
+
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
731
805
|
if (staging) try {
|
|
732
|
-
|
|
806
|
+
await rm(staging, {
|
|
733
807
|
recursive: true,
|
|
734
808
|
force: true
|
|
735
809
|
});
|
|
736
810
|
} catch {}
|
|
737
|
-
|
|
811
|
+
operation?.signal.throwIfAborted();
|
|
812
|
+
archiveWarning(`could not archive: ${String(error?.message ?? error).split("\n")[0]}`, operation);
|
|
738
813
|
} finally {
|
|
739
814
|
releaseClaim(claim);
|
|
740
815
|
}
|
|
741
816
|
}
|
|
742
|
-
function removeKind(record, kind) {
|
|
743
|
-
|
|
817
|
+
async function removeKind(record, kind) {
|
|
818
|
+
await rm(join(archiveDir(record.id), KIND_PATH[kind]), {
|
|
744
819
|
recursive: true,
|
|
745
820
|
force: true
|
|
746
821
|
});
|
|
747
822
|
record.bytes[kind] = 0;
|
|
748
823
|
record.expires[kind] = null;
|
|
749
|
-
writeRecord(archiveDir(record.id), record);
|
|
824
|
+
await writeRecord(archiveDir(record.id), record);
|
|
750
825
|
}
|
|
751
826
|
function logRemovalOrder(path) {
|
|
752
827
|
return /\.ndjson\.\d+$/.test(path) ? 0 : /^build-.*\.ndjson$/.test(basename(path)) ? 1 : 2;
|
|
753
828
|
}
|
|
754
|
-
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();
|
|
755
833
|
if (!existsSync(archiveRoot())) return;
|
|
756
834
|
let limits;
|
|
757
835
|
try {
|
|
758
|
-
limits = archiveSettings();
|
|
836
|
+
limits = archiveSettings(env);
|
|
759
837
|
} catch (error) {
|
|
760
|
-
|
|
838
|
+
archiveWarning(`could not enforce archive retention: ${String(error.message).split("\n")[0]}`, operation);
|
|
761
839
|
return;
|
|
762
840
|
}
|
|
763
|
-
|
|
764
|
-
const listed =
|
|
841
|
+
await withDirLockTransaction(archiveLock(), async () => {
|
|
842
|
+
const listed = (await readArchivesAsync(signal)).toReversed();
|
|
765
843
|
const records = listed.filter((record, index) => limits.maxAgeDays > 0 && now - Date.parse(record.removedAt) < limits.maxAgeDays * DAY && index >= listed.length - limits.maxCount);
|
|
766
|
-
for (const record of listed)
|
|
767
|
-
|
|
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()]);
|
|
768
849
|
for (const record of records) {
|
|
850
|
+
signal?.throwIfAborted();
|
|
769
851
|
for (const kind of [
|
|
770
852
|
"recordings",
|
|
771
853
|
"agentActions",
|
|
772
854
|
"logs"
|
|
773
855
|
]) {
|
|
856
|
+
signal?.throwIfAborted();
|
|
774
857
|
const age = limits[`${kind}.maxAgeDays`];
|
|
775
|
-
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);
|
|
776
859
|
}
|
|
777
860
|
const cap = limits["logs.maxMbPerWorkspace"] * 1024 ** 2;
|
|
778
861
|
if (record.bytes.logs > cap) {
|
|
779
|
-
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);
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
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
|
+
}
|
|
784
877
|
}
|
|
785
|
-
if (!record.bytes.logs) record.expires.logs = null;
|
|
786
|
-
writeRecord(archiveDir(record.id), record);
|
|
787
878
|
}
|
|
788
879
|
}
|
|
789
880
|
const totalCap = limits.maxTotalGb * 1024 ** 3;
|
|
790
881
|
const recordingCap = limits["recordings.maxTotalGb"] * 1024 ** 3;
|
|
791
|
-
for (const record of records)
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
+
}
|
|
799
894
|
}
|
|
800
895
|
for (const record of records) {
|
|
896
|
+
signal?.throwIfAborted();
|
|
801
897
|
const before = JSON.stringify(record.expires);
|
|
802
898
|
for (const kind of KINDS) record.expires[kind] = record.bytes[kind] ? new Date(Date.parse(record.removedAt) + limits[`${kind}.maxAgeDays`] * DAY).toISOString() : null;
|
|
803
899
|
record.expires.record = new Date(Date.parse(record.removedAt) + limits.maxAgeDays * DAY).toISOString();
|
|
804
|
-
if (JSON.stringify(record.expires) !== before) writeRecord(archiveDir(record.id), record);
|
|
900
|
+
if (JSON.stringify(record.expires) !== before) await writeRecord(archiveDir(record.id), record);
|
|
805
901
|
}
|
|
806
|
-
}, {
|
|
902
|
+
}, {
|
|
903
|
+
waitMs: 3e4,
|
|
904
|
+
signal
|
|
905
|
+
});
|
|
807
906
|
}
|
|
808
|
-
function deleteSelectedArchives(ids, kind) {
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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
|
+
});
|
|
813
919
|
}
|
|
814
920
|
//#endregion
|
|
815
921
|
//#region src/devices/reclaim.ts
|
|
816
|
-
|
|
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 }) {
|
|
817
927
|
const skippedDevices = [];
|
|
818
928
|
const failedDevices = [];
|
|
819
929
|
for (const [platform, record] of Object.entries(collectors)) {
|
|
930
|
+
operation?.signal.throwIfAborted();
|
|
820
931
|
const rec = record;
|
|
821
932
|
const pid = rec?.pid;
|
|
822
933
|
if (typeof pid !== "number" || pid <= 0 || pid === process.pid || !pidExists(pid)) continue;
|
|
@@ -840,7 +951,7 @@ async function reapCollectors(root, { verify = verifyCollectorOwnership, collect
|
|
|
840
951
|
}
|
|
841
952
|
try {
|
|
842
953
|
process.kill(pid, "SIGTERM");
|
|
843
|
-
if (await (0, process_identity_exports.waitForProcessExit)(rec, 5e3)) continue;
|
|
954
|
+
if (await (0, process_identity_exports.waitForProcessExit)(rec, 5e3, operation?.signal)) continue;
|
|
844
955
|
} catch (error) {
|
|
845
956
|
if (error.code === "ESRCH") continue;
|
|
846
957
|
}
|
|
@@ -857,7 +968,7 @@ async function reapCollectors(root, { verify = verifyCollectorOwnership, collect
|
|
|
857
968
|
failedDevices
|
|
858
969
|
};
|
|
859
970
|
}
|
|
860
|
-
async function reclaimRemoteSession(root, { stopSession = defaultStopSession } = {}) {
|
|
971
|
+
async function reclaimRemoteSession(root, { stopSession = defaultStopSession, operation } = {}) {
|
|
861
972
|
const sessionId = readRemoteSessionId(root);
|
|
862
973
|
if (!sessionId) return {
|
|
863
974
|
stopped: null,
|
|
@@ -865,8 +976,10 @@ async function reclaimRemoteSession(root, { stopSession = defaultStopSession } =
|
|
|
865
976
|
};
|
|
866
977
|
let result;
|
|
867
978
|
try {
|
|
868
|
-
result = await stopSession(root, sessionId);
|
|
979
|
+
result = await stopSession(root, sessionId, operation);
|
|
980
|
+
operation?.signal.throwIfAborted();
|
|
869
981
|
} catch (err) {
|
|
982
|
+
throwIfCleanupInterrupted(err, operation);
|
|
870
983
|
result = {
|
|
871
984
|
status: "failed",
|
|
872
985
|
reason: String(err?.message ?? err)
|
|
@@ -896,17 +1009,18 @@ async function reclaimRemoteSession(root, { stopSession = defaultStopSession } =
|
|
|
896
1009
|
}
|
|
897
1010
|
};
|
|
898
1011
|
}
|
|
899
|
-
function defaultStopSession(root, sessionId) {
|
|
1012
|
+
function defaultStopSession(root, sessionId, operation) {
|
|
900
1013
|
return endRecordedSession({
|
|
901
1014
|
root,
|
|
902
1015
|
sessionId,
|
|
903
|
-
easBin: resolveEasCliBin(root)?.file ?? null
|
|
1016
|
+
easBin: resolveEasCliBin(root)?.file ?? null,
|
|
1017
|
+
operation
|
|
904
1018
|
});
|
|
905
1019
|
}
|
|
906
|
-
function defaultStopMetroTunnel(record) {
|
|
907
|
-
return stopTunnel(record);
|
|
1020
|
+
function defaultStopMetroTunnel(record, operation) {
|
|
1021
|
+
return stopTunnel(record, { operation });
|
|
908
1022
|
}
|
|
909
|
-
async function reclaimMetroTunnel(root, { stopMetroTunnel = defaultStopMetroTunnel } = {}) {
|
|
1023
|
+
async function reclaimMetroTunnel(root, { stopMetroTunnel = defaultStopMetroTunnel, operation } = {}) {
|
|
910
1024
|
const record = readMetroTunnel(root);
|
|
911
1025
|
if (record?.kind !== "managed") return {
|
|
912
1026
|
stopped: null,
|
|
@@ -914,8 +1028,10 @@ async function reclaimMetroTunnel(root, { stopMetroTunnel = defaultStopMetroTunn
|
|
|
914
1028
|
};
|
|
915
1029
|
let result;
|
|
916
1030
|
try {
|
|
917
|
-
result = await stopMetroTunnel(record);
|
|
1031
|
+
result = await stopMetroTunnel(record, operation);
|
|
1032
|
+
operation?.signal.throwIfAborted();
|
|
918
1033
|
} catch (err) {
|
|
1034
|
+
throwIfCleanupInterrupted(err, operation);
|
|
919
1035
|
result = {
|
|
920
1036
|
status: "failed",
|
|
921
1037
|
reason: String(err?.message ?? err)
|
|
@@ -971,7 +1087,7 @@ function parkRequest(project, projectPath, slot, simslimManaged) {
|
|
|
971
1087
|
cacheKey: lastBuildCacheKey(projectPath, "ios")
|
|
972
1088
|
};
|
|
973
1089
|
}
|
|
974
|
-
async function reclaimOwnedDevices(project, projectPath, { park = false } = {}) {
|
|
1090
|
+
async function reclaimOwnedDevices(project, projectPath, { park = false, operation } = {}) {
|
|
975
1091
|
const deletedDevices = [];
|
|
976
1092
|
const parkedDevices = [];
|
|
977
1093
|
const evictedDevices = [];
|
|
@@ -979,11 +1095,13 @@ async function reclaimOwnedDevices(project, projectPath, { park = false } = {})
|
|
|
979
1095
|
const skippedDevices = [];
|
|
980
1096
|
const failedDevices = [];
|
|
981
1097
|
for (const { slot, platforms } of projectDeviceSlots(project)) {
|
|
1098
|
+
operation?.signal.throwIfAborted();
|
|
982
1099
|
const ios = platforms.ios;
|
|
983
1100
|
if (ios?.owned && ios.deviceUdid) {
|
|
984
1101
|
const udid = ios.deviceUdid;
|
|
985
1102
|
const label = ios.deviceName || udid;
|
|
986
1103
|
const r = await teardownOwnedIosSim(udid, {
|
|
1104
|
+
operation,
|
|
987
1105
|
del: true,
|
|
988
1106
|
label,
|
|
989
1107
|
...park ? { park: parkRequest(project, projectPath, slot, Boolean(ios.simslimManaged)) } : {}
|
|
@@ -1013,9 +1131,11 @@ async function reclaimOwnedDevices(project, projectPath, { park = false } = {})
|
|
|
1013
1131
|
}
|
|
1014
1132
|
}
|
|
1015
1133
|
const android = platforms.android;
|
|
1134
|
+
operation?.signal.throwIfAborted();
|
|
1016
1135
|
if (android?.owned && android.avdName) {
|
|
1017
1136
|
const bound = parkedMaxSetting("android");
|
|
1018
1137
|
const r = await teardownOwnedAvd(android.avdName, {
|
|
1138
|
+
operation,
|
|
1019
1139
|
del: true,
|
|
1020
1140
|
owner: {
|
|
1021
1141
|
projectPath,
|
|
@@ -1066,6 +1186,7 @@ async function reclaimProject(path, options = {}) {
|
|
|
1066
1186
|
const dir = workspaceDir(path);
|
|
1067
1187
|
const existed = existsSync(dir);
|
|
1068
1188
|
const run = await withIdleWorkspace(path, () => reclaimIdleProject(path, existed, options), {
|
|
1189
|
+
operation: options.operation,
|
|
1069
1190
|
purpose: "workspace removal",
|
|
1070
1191
|
hosted: false,
|
|
1071
1192
|
supervisor: false,
|
|
@@ -1096,10 +1217,52 @@ async function reclaimProject(path, options = {}) {
|
|
|
1096
1217
|
failedWorkspaceDirs: []
|
|
1097
1218
|
};
|
|
1098
1219
|
}
|
|
1099
|
-
|
|
1100
|
-
|
|
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();
|
|
1101
1264
|
const project = getProject(path);
|
|
1102
|
-
|
|
1265
|
+
verifyAvdClaims(project, operation);
|
|
1103
1266
|
const dereferenced = describeDereferenced(project);
|
|
1104
1267
|
const initialState = readWorkspaceState$1(path);
|
|
1105
1268
|
let killedPid = null;
|
|
@@ -1117,9 +1280,9 @@ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwned
|
|
|
1117
1280
|
const orphanIdentity = supervisor.status === "stale" && typeof orphanServer.processToken === "string" ? (0, process_identity_exports.inspectProcessIdentity)(orphanServer) : null;
|
|
1118
1281
|
if (supervisor.status === "ours") {
|
|
1119
1282
|
requestDevServerStop(path, supervisor.processToken, stopRequester);
|
|
1120
|
-
const signalled =
|
|
1283
|
+
const signalled = await killMetroTreeAsync(supervisor.pid, supervisor.processToken, operation);
|
|
1121
1284
|
if (!signalled) withdrawDevServerStopRequest(path, supervisor.processToken);
|
|
1122
|
-
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;
|
|
1123
1286
|
else {
|
|
1124
1287
|
skippedMetro = `could not confirm supervisor pid ${supervisor.pid} exited`;
|
|
1125
1288
|
supervisorHeld = true;
|
|
@@ -1130,9 +1293,14 @@ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwned
|
|
|
1130
1293
|
} else if (orphanIdentity === "same") {
|
|
1131
1294
|
let signalled = false;
|
|
1132
1295
|
try {
|
|
1133
|
-
signalled =
|
|
1134
|
-
|
|
1135
|
-
|
|
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;
|
|
1136
1304
|
else {
|
|
1137
1305
|
skippedMetro = `could not confirm dev server pid ${orphanServer.pid} left by the supervisor exited`;
|
|
1138
1306
|
supervisorHeld = true;
|
|
@@ -1145,9 +1313,15 @@ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwned
|
|
|
1145
1313
|
}
|
|
1146
1314
|
const { skippedDevices: skippedCollectors, failedDevices: failedCollectors } = await reapCollectors(path, {
|
|
1147
1315
|
verify: verifyCollector,
|
|
1148
|
-
collectors: initialState?.collectors ?? {}
|
|
1316
|
+
collectors: initialState?.collectors ?? {},
|
|
1317
|
+
operation
|
|
1149
1318
|
});
|
|
1150
|
-
|
|
1319
|
+
signal?.throwIfAborted();
|
|
1320
|
+
const browser = await teardownBrowser(path, {
|
|
1321
|
+
deleteProfile: true,
|
|
1322
|
+
operation
|
|
1323
|
+
});
|
|
1324
|
+
signal?.throwIfAborted();
|
|
1151
1325
|
if (browser.status === "failed" || browser.status === "skipped") {
|
|
1152
1326
|
const entry = {
|
|
1153
1327
|
name: "owned Chrome",
|
|
@@ -1165,7 +1339,10 @@ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwned
|
|
|
1165
1339
|
}
|
|
1166
1340
|
}
|
|
1167
1341
|
retainReplacementProcesses();
|
|
1168
|
-
const { deletedDevices, parkedDevices, evictedDevices, poolNotes, skippedDevices, failedDevices } = deleteOwnedDevices && !supervisorHeld && failedCollectors.length === 0 ? await 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
|
+
}) : {
|
|
1169
1346
|
deletedDevices: [],
|
|
1170
1347
|
parkedDevices: [],
|
|
1171
1348
|
evictedDevices: [],
|
|
@@ -1175,27 +1352,37 @@ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwned
|
|
|
1175
1352
|
};
|
|
1176
1353
|
skippedDevices.push(...skippedCollectors);
|
|
1177
1354
|
failedDevices.push(...failedCollectors);
|
|
1178
|
-
for (const platform of ["ios", "android"])
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
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
|
+
}
|
|
1188
1369
|
}
|
|
1189
|
-
|
|
1370
|
+
signal?.throwIfAborted();
|
|
1371
|
+
const remote = await reclaimRemoteSession(path, {
|
|
1372
|
+
stopSession,
|
|
1373
|
+
operation
|
|
1374
|
+
});
|
|
1190
1375
|
if (remote.failed) {
|
|
1191
1376
|
skippedDevices.push(remote.failed);
|
|
1192
1377
|
failedDevices.push(remote.failed);
|
|
1193
1378
|
}
|
|
1194
1379
|
if (macosAppPresent(path)) {
|
|
1380
|
+
signal?.throwIfAborted();
|
|
1195
1381
|
const hosted = readMacosRecord(path)?.host;
|
|
1196
1382
|
try {
|
|
1197
|
-
await stopMacosApp(path);
|
|
1383
|
+
await stopMacosApp(path, operation);
|
|
1198
1384
|
} catch (error) {
|
|
1385
|
+
throwIfCleanupInterrupted(error, operation);
|
|
1199
1386
|
const failed = {
|
|
1200
1387
|
name: hosted ? `hosted macOS session ${hosted.session} on ${hosted.machine}` : "macOS app",
|
|
1201
1388
|
reason: String(error?.message ?? error)
|
|
@@ -1204,52 +1391,39 @@ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwned
|
|
|
1204
1391
|
failedDevices.push(failed);
|
|
1205
1392
|
}
|
|
1206
1393
|
}
|
|
1207
|
-
|
|
1394
|
+
signal?.throwIfAborted();
|
|
1395
|
+
const tunnel = await reclaimMetroTunnel(path, {
|
|
1396
|
+
stopMetroTunnel,
|
|
1397
|
+
operation
|
|
1398
|
+
});
|
|
1208
1399
|
if (tunnel.failed) {
|
|
1209
1400
|
skippedDevices.push(tunnel.failed);
|
|
1210
1401
|
failedDevices.push(tunnel.failed);
|
|
1211
1402
|
}
|
|
1212
1403
|
let releasedLeases = [];
|
|
1404
|
+
signal?.throwIfAborted();
|
|
1213
1405
|
try {
|
|
1214
1406
|
releasedLeases = releaseLeases(path);
|
|
1215
|
-
} catch {
|
|
1407
|
+
} catch (error) {
|
|
1408
|
+
throwIfCleanupInterrupted(error, operation);
|
|
1216
1409
|
releasedLeases = [];
|
|
1217
1410
|
}
|
|
1218
1411
|
retainReplacementProcesses();
|
|
1219
1412
|
const removedWorkspaceDirs = [];
|
|
1220
1413
|
const failedWorkspaceDirs = [];
|
|
1221
1414
|
if (failedDevices.length === 0 && !supervisorHeld) {
|
|
1415
|
+
signal?.throwIfAborted();
|
|
1222
1416
|
const dir = workspaceDir(path);
|
|
1223
1417
|
if (existsSync(dir)) try {
|
|
1224
|
-
|
|
1225
|
-
if (!["none", "dead"].includes(agentDeviceDaemon(stateDir))) {
|
|
1226
|
-
const exec = getExecutor();
|
|
1227
|
-
try {
|
|
1228
|
-
if (!exec.findExecutable("agent-device")) throw new Error("agent-device is not on PATH");
|
|
1229
|
-
exec.runFile("agent-device", [
|
|
1230
|
-
"daemon",
|
|
1231
|
-
"stop",
|
|
1232
|
-
"--state-dir",
|
|
1233
|
-
stateDir,
|
|
1234
|
-
"--clean"
|
|
1235
|
-
], {
|
|
1236
|
-
timeoutMs: 2e4,
|
|
1237
|
-
killSignal: "SIGKILL",
|
|
1238
|
-
env: { AGENT_DEVICE_NO_UPDATE_NOTIFIER: "1" }
|
|
1239
|
-
});
|
|
1240
|
-
} catch (error) {
|
|
1241
|
-
process.stderr.write(`${phaseLine("agent-device", `daemon could not be stopped: ${error.message}; run agent-device daemon stop --state-dir "${stateDir}" --clean`)}\n`);
|
|
1242
|
-
throw error;
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
if (archive) archiveWorkspace(path, archive.removedBy, initialState);
|
|
1246
|
-
emptyWorkspaceDir(dir);
|
|
1418
|
+
await removeWorkspaceOutput(path, dir, archive, initialState, operation);
|
|
1247
1419
|
if (workspaceExisted) removedWorkspaceDirs.push(dir);
|
|
1248
|
-
} catch {
|
|
1420
|
+
} catch (error) {
|
|
1421
|
+
throwIfCleanupInterrupted(error, operation);
|
|
1249
1422
|
failedWorkspaceDirs.push(dir);
|
|
1250
1423
|
}
|
|
1251
1424
|
}
|
|
1252
1425
|
const keptEntry = supervisorHeld || failedDevices.length > 0 || failedWorkspaceDirs.length > 0;
|
|
1426
|
+
signal?.throwIfAborted();
|
|
1253
1427
|
if (project && !keptEntry && !preserveProjectRecord) removeProject(path);
|
|
1254
1428
|
return {
|
|
1255
1429
|
path,
|
|
@@ -1418,7 +1592,7 @@ function warmCommand(phase) {
|
|
|
1418
1592
|
function asHolder(holder) {
|
|
1419
1593
|
return {
|
|
1420
1594
|
pid: holder.owner.pid,
|
|
1421
|
-
phase: holder.mode === "exclusive" ? "refresh" : "copy"
|
|
1595
|
+
phase: holder.details.phase === "copy" ? "copy" : holder.mode === "exclusive" ? "refresh" : "copy"
|
|
1422
1596
|
};
|
|
1423
1597
|
}
|
|
1424
1598
|
function waitingLine(holder, elapsedMs) {
|
|
@@ -1504,7 +1678,7 @@ function timeoutError(holder, elapsedMs, root) {
|
|
|
1504
1678
|
error.code = "STIM_LOCK_TIMEOUT";
|
|
1505
1679
|
return error;
|
|
1506
1680
|
}
|
|
1507
|
-
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 = () => {} }) {
|
|
1508
1682
|
const root = warmClaimPath(repositoryRoot);
|
|
1509
1683
|
const started = now();
|
|
1510
1684
|
let lastProgress = started;
|
|
@@ -1512,26 +1686,31 @@ async function acquireWarmClaim({ repositoryRoot, phase, mode = phase === "refre
|
|
|
1512
1686
|
let pending = null;
|
|
1513
1687
|
try {
|
|
1514
1688
|
for (;;) {
|
|
1689
|
+
signal?.throwIfAborted();
|
|
1515
1690
|
const attempt = pending ? (0, ownership_claim_exports.settleClaim)(pending) : (0, ownership_claim_exports.tryAcquireClaim)({
|
|
1516
1691
|
root,
|
|
1517
1692
|
mode,
|
|
1518
|
-
label: WARM_CLAIM_LABEL
|
|
1693
|
+
label: WARM_CLAIM_LABEL,
|
|
1694
|
+
details: { phase }
|
|
1519
1695
|
});
|
|
1520
1696
|
const acquired = attempt.acquired;
|
|
1521
1697
|
if (acquired) {
|
|
1698
|
+
if (declareSpawnsFor) declareSpawnsOn(declareSpawnsFor, acquired);
|
|
1522
1699
|
const installer = installerClaim(acquired, {
|
|
1523
1700
|
now,
|
|
1524
1701
|
progressMs,
|
|
1525
1702
|
out
|
|
1526
1703
|
});
|
|
1527
1704
|
return {
|
|
1705
|
+
claim: acquired,
|
|
1528
1706
|
wait: {
|
|
1529
1707
|
waitedMs: now() - started,
|
|
1530
1708
|
holder: waitedOn
|
|
1531
1709
|
},
|
|
1532
1710
|
installer,
|
|
1533
|
-
release: () => {
|
|
1534
|
-
if (
|
|
1711
|
+
release: (retained = false) => {
|
|
1712
|
+
if (declareSpawnsFor) stopDeclaringSpawnsOn(declareSpawnsFor, acquired);
|
|
1713
|
+
if (retained || installer.holding()) return;
|
|
1535
1714
|
(0, ownership_claim_exports.releaseClaim)(acquired);
|
|
1536
1715
|
}
|
|
1537
1716
|
};
|
|
@@ -1560,10 +1739,14 @@ async function acquireWarmClaim({ repositoryRoot, phase, mode = phase === "refre
|
|
|
1560
1739
|
}
|
|
1561
1740
|
async function withWarmClaim(options, fn) {
|
|
1562
1741
|
const { release, ...hold } = await acquireWarmClaim(options);
|
|
1742
|
+
let retained = false;
|
|
1563
1743
|
try {
|
|
1564
1744
|
return await fn(hold);
|
|
1745
|
+
} catch (cause) {
|
|
1746
|
+
retained = cause instanceof WorkspaceProcessCleanupError && cause.claims.includes(hold.claim);
|
|
1747
|
+
throw cause;
|
|
1565
1748
|
} finally {
|
|
1566
|
-
release();
|
|
1749
|
+
release(retained);
|
|
1567
1750
|
}
|
|
1568
1751
|
}
|
|
1569
1752
|
//#endregion
|
|
@@ -2092,7 +2275,7 @@ function dependencyInstallCommand(target, dir = ".") {
|
|
|
2092
2275
|
else if (existsSync$1(resolve$1(installRoot, "package-lock.json"))) command = "npm ci";
|
|
2093
2276
|
return `cd '${installRoot.replaceAll("'", "'\\''")}' && ${command}`;
|
|
2094
2277
|
}
|
|
2095
|
-
function relocateCarriedPods(root, target, copied) {
|
|
2278
|
+
function relocateCarriedPods(root, target, copied, out) {
|
|
2096
2279
|
const canRelocate = depsOutOfSync(root, target, copied).length === 0;
|
|
2097
2280
|
for (const rel of copied) {
|
|
2098
2281
|
if (rel !== "Pods" && !rel.endsWith("/Pods")) continue;
|
|
@@ -2105,41 +2288,40 @@ function relocateCarriedPods(root, target, copied) {
|
|
|
2105
2288
|
targetRoot: target,
|
|
2106
2289
|
canRelocate
|
|
2107
2290
|
});
|
|
2108
|
-
if (result.ok)
|
|
2109
|
-
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`)));
|
|
2110
2293
|
} catch (error) {
|
|
2111
|
-
|
|
2294
|
+
out(chalk.yellow(phaseLine("carry", `could not move ${rel} to this checkout's path: ${error.message}. pod install will run`)));
|
|
2112
2295
|
}
|
|
2113
2296
|
}
|
|
2114
2297
|
}
|
|
2115
|
-
function reportCarriedStateHealth(root, target, copied) {
|
|
2298
|
+
function reportCarriedStateHealth(root, target, copied, out) {
|
|
2116
2299
|
const outOfSync = depsOutOfSync(root, target, copied);
|
|
2117
2300
|
for (const d of outOfSync.filter((dependency) => dependency.reason === "installed")) {
|
|
2118
2301
|
const where = d.dir === "." ? d.lockfile : `${d.dir}/${d.lockfile}`;
|
|
2119
2302
|
const modules = d.dir === "." ? "node_modules" : `${d.dir}/node_modules`;
|
|
2120
|
-
|
|
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.`)));
|
|
2121
2304
|
}
|
|
2122
2305
|
const staleDeps = outOfSync.filter((dependency) => dependency.reason === "lockfile");
|
|
2123
2306
|
if (staleDeps.length) {
|
|
2124
2307
|
const manifests = staleDeps.map((d) => d.dir === "." ? d.lockfile : `${d.dir}/${d.lockfile}`).join(", ");
|
|
2125
2308
|
const remedies = [...new Set(staleDeps.map((dependency) => dependencyInstallCommand(target, dependency.dir)))];
|
|
2126
|
-
|
|
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.`)));
|
|
2127
2310
|
}
|
|
2128
2311
|
for (const p of podsOutOfSync(target, copied)) {
|
|
2129
2312
|
const where = p.dir === "." ? "Podfile.lock" : `${p.dir}/Podfile.lock`;
|
|
2130
2313
|
const pod = podInstallCommand(p.dir === "." ? target : resolve$1(target, p.dir, ".."));
|
|
2131
|
-
|
|
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.`)));
|
|
2132
2315
|
}
|
|
2133
2316
|
}
|
|
2134
|
-
function reportRefreshFailure(failure) {
|
|
2135
|
-
|
|
2136
|
-
for (const line of failure.lines)
|
|
2137
|
-
if (failure.remedy)
|
|
2138
|
-
|
|
2139
|
-
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}`));
|
|
2140
2322
|
}
|
|
2141
|
-
function mainCheckoutAppDir(root, target) {
|
|
2142
|
-
const app = findProjectRoot$1(
|
|
2323
|
+
function mainCheckoutAppDir(root, target, cwd) {
|
|
2324
|
+
const app = findProjectRoot$1(cwd);
|
|
2143
2325
|
if (!app) return root;
|
|
2144
2326
|
const rel = relative$1(target, app);
|
|
2145
2327
|
if (!rel || rel.startsWith("..") || isAbsolute$1(rel)) return root;
|
|
@@ -2163,150 +2345,204 @@ function warmedWorkspace(root, target, cwd) {
|
|
|
2163
2345
|
}
|
|
2164
2346
|
return apps.size === 1 ? [...apps][0] : app;
|
|
2165
2347
|
}
|
|
2166
|
-
function registerWarmedWorkspace(workspace) {
|
|
2348
|
+
function registerWarmedWorkspace(workspace, out) {
|
|
2167
2349
|
try {
|
|
2168
2350
|
if (!getProject(workspace)) upsertProject(workspace, {});
|
|
2169
2351
|
} catch (error) {
|
|
2170
|
-
|
|
2352
|
+
out(chalk.yellow(`Could not register ${workspace} for status: ${error?.message || String(error)}`));
|
|
2171
2353
|
}
|
|
2172
2354
|
}
|
|
2173
2355
|
function registerWarm(worktree) {
|
|
2174
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) => {
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
workspace = warmedWorkspace(root, target, process.cwd());
|
|
2182
|
-
if (workspace && workspace !== app) recordWorkspaceUse(workspace);
|
|
2183
|
-
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")));
|
|
2184
|
-
else {
|
|
2185
|
-
registerWarmedWorkspace(workspace);
|
|
2186
|
-
progress = startWarmProgress(workspace, opts.refresh ? "refresh" : "copy", { note: (line) => console.error(chalk.yellow(line)) });
|
|
2187
|
-
}
|
|
2188
|
-
const readSettings = () => {
|
|
2189
|
-
const settings = resolveSettings({
|
|
2190
|
-
gitCommonDir: common,
|
|
2191
|
-
repoRoot: root
|
|
2192
|
-
});
|
|
2193
|
-
const shapeErrors = settingShapeErrors(settings);
|
|
2194
|
-
if (shapeErrors.length) {
|
|
2195
|
-
for (const message of shapeErrors) console.error(chalk.red(message));
|
|
2196
|
-
console.error(chalk.dim(SETTING_SHAPE_REMEDY));
|
|
2197
|
-
return null;
|
|
2198
|
-
}
|
|
2199
|
-
for (const key of unknownSettingKeys(settings)) console.error(chalk.yellow(`Warning: setting "${key}" is not read by Stim and will be ignored.`));
|
|
2200
|
-
return settings;
|
|
2201
|
-
};
|
|
2202
|
-
const copy = (wait) => {
|
|
2203
|
-
progress?.step("copy");
|
|
2204
|
-
if (wait?.holder) console.error(warmClaimAcquiredLine(wait));
|
|
2205
|
-
const incomplete = incompleteInstallRefusal(root, mainCheckoutAppDir(root, target));
|
|
2206
|
-
if (incomplete) {
|
|
2207
|
-
reportRefreshFailure(incomplete);
|
|
2208
|
-
return;
|
|
2209
|
-
}
|
|
2210
|
-
const settings = readSettings();
|
|
2211
|
-
if (!settings) {
|
|
2212
|
-
process.exitCode = 1;
|
|
2213
|
-
return;
|
|
2214
|
-
}
|
|
2215
|
-
const excluded = readWorktreeExclude(root);
|
|
2216
|
-
const patterns = excluded?.length ? excluded : settings.worktree?.exclude || [];
|
|
2217
|
-
const result = cloneIgnoredEntries({
|
|
2218
|
-
root,
|
|
2219
|
-
target,
|
|
2220
|
-
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
|
|
2221
2363
|
});
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
console.error(chalk.
|
|
2226
|
-
|
|
2227
|
-
|
|
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}`));
|
|
2228
2371
|
}
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
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
|
+
}
|
|
2241
2440
|
const settings = readSettings();
|
|
2242
2441
|
if (!settings) {
|
|
2243
|
-
|
|
2442
|
+
exitCode = 1;
|
|
2244
2443
|
return;
|
|
2245
2444
|
}
|
|
2246
|
-
const
|
|
2247
|
-
const
|
|
2445
|
+
const excluded = readWorktreeExclude(root);
|
|
2446
|
+
const patterns = excluded?.length ? excluded : settings.worktree?.exclude || [];
|
|
2447
|
+
const result = await cloneIgnoredEntries({
|
|
2248
2448
|
root,
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
emit: (line) => lines.push(line),
|
|
2253
|
-
inspectOnly: true
|
|
2449
|
+
target,
|
|
2450
|
+
patterns,
|
|
2451
|
+
operation
|
|
2254
2452
|
});
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
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);
|
|
2261
2459
|
}
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
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;
|
|
2265
2480
|
}
|
|
2266
|
-
const
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
process.exitCode = 1;
|
|
2275
|
-
return null;
|
|
2276
|
-
}
|
|
2277
|
-
return refreshMainCheckout({
|
|
2278
|
-
root,
|
|
2279
|
-
appDir: mainCheckoutAppDir(root, target),
|
|
2280
|
-
settings,
|
|
2281
|
-
iosProject: appIosProjectDir(mainCheckoutAppDir(root, target)),
|
|
2282
|
-
emit: (line) => console.error(line),
|
|
2283
|
-
installer: hold.installer
|
|
2284
|
-
});
|
|
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
|
|
2285
2489
|
});
|
|
2286
|
-
if (
|
|
2287
|
-
|
|
2288
|
-
|
|
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;
|
|
2289
2498
|
}
|
|
2290
|
-
|
|
2291
|
-
}
|
|
2292
|
-
|
|
2499
|
+
return result.mutation;
|
|
2500
|
+
});
|
|
2501
|
+
if (mutation === null) return { exitCode };
|
|
2502
|
+
const failure = await withWarmClaim({
|
|
2293
2503
|
repositoryRoot: root,
|
|
2294
|
-
phase: "
|
|
2295
|
-
out
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
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 };
|
|
2308
2529
|
}
|
|
2309
|
-
|
|
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 };
|
|
2310
2546
|
}
|
|
2311
2547
|
function carriedFileList(files) {
|
|
2312
2548
|
const shown = files.slice(0, 3).join(", ");
|
|
@@ -2456,12 +2692,16 @@ function reclaimKeys(rootPath) {
|
|
|
2456
2692
|
}
|
|
2457
2693
|
return [...keys].toSorted();
|
|
2458
2694
|
}
|
|
2459
|
-
async function withReclaimLocks(rootPath, fn) {
|
|
2695
|
+
async function withReclaimLocks(rootPath, fn, operation, beforeRemove) {
|
|
2460
2696
|
const keys = reclaimKeys(rootPath);
|
|
2461
|
-
|
|
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
|
+
});
|
|
2462
2702
|
return acquire(0);
|
|
2463
2703
|
}
|
|
2464
|
-
async function reclaimAll(rootPath, keys = reclaimKeys(rootPath), { preserveRootProject = false, removedBy } = {}) {
|
|
2704
|
+
async function reclaimAll(rootPath, keys = reclaimKeys(rootPath), { preserveRootProject = false, removedBy, operation } = {}) {
|
|
2465
2705
|
const dereferenced = [];
|
|
2466
2706
|
const killedPids = [];
|
|
2467
2707
|
const deletedDevices = [];
|
|
@@ -2476,8 +2716,10 @@ async function reclaimAll(rootPath, keys = reclaimKeys(rootPath), { preserveRoot
|
|
|
2476
2716
|
const releasedLeases = [];
|
|
2477
2717
|
const removedWorkspaceDirs = [];
|
|
2478
2718
|
const failedWorkspaceDirs = [];
|
|
2479
|
-
|
|
2719
|
+
operation?.signal.throwIfAborted();
|
|
2720
|
+
const pressure = currentDiskLevel(operation ? await measureDiskAsync(operation) : void 0);
|
|
2480
2721
|
for (const key of keys) {
|
|
2722
|
+
operation?.signal.throwIfAborted();
|
|
2481
2723
|
let r;
|
|
2482
2724
|
try {
|
|
2483
2725
|
r = await reclaimProject(key, {
|
|
@@ -2485,9 +2727,12 @@ async function reclaimAll(rootPath, keys = reclaimKeys(rootPath), { preserveRoot
|
|
|
2485
2727
|
deleteOwnedDevices: true,
|
|
2486
2728
|
parkOwnedDevices: pressure < 2,
|
|
2487
2729
|
preserveProjectRecord: preserveRootProject && key === rootPath,
|
|
2488
|
-
stopRequester: { by: "stim worktree remove" }
|
|
2730
|
+
stopRequester: { by: "stim worktree remove" },
|
|
2731
|
+
operation
|
|
2489
2732
|
});
|
|
2490
2733
|
} catch (error) {
|
|
2734
|
+
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
2735
|
+
operation?.signal.throwIfAborted();
|
|
2491
2736
|
keptEntries.push(key);
|
|
2492
2737
|
retainedResources.push({
|
|
2493
2738
|
name: "ownership state",
|
|
@@ -2569,18 +2814,18 @@ function poolLines(result) {
|
|
|
2569
2814
|
for (const note of result.poolNotes) lines.push(chalk.yellow(phaseLine("device", note)));
|
|
2570
2815
|
return lines;
|
|
2571
2816
|
}
|
|
2572
|
-
function reportRetainedResources(root, result) {
|
|
2573
|
-
|
|
2817
|
+
function reportRetainedResources(root, result, output) {
|
|
2818
|
+
output.error(chalk.red(`Refusing to remove ${root}: Stim could not release owned resources.`));
|
|
2574
2819
|
for (const resource of result.retainedResources) {
|
|
2575
|
-
|
|
2576
|
-
|
|
2820
|
+
output.error(chalk.yellow(` - Stim still tracks ${describeKeptDevice(resource)} for ${resource.project}`));
|
|
2821
|
+
output.error(chalk.dim(` ${resource.reason}`));
|
|
2577
2822
|
}
|
|
2578
2823
|
for (const kept of result.keptEntries) {
|
|
2579
2824
|
if (result.retainedResources.some((resource) => resource.project === kept)) continue;
|
|
2580
|
-
|
|
2825
|
+
output.error(chalk.yellow(` - retained Stim ownership state for ${kept}`));
|
|
2581
2826
|
}
|
|
2582
|
-
|
|
2583
|
-
|
|
2827
|
+
output.error(chalk.dim("Fix the reported cause, then run `stim worktree remove` again."));
|
|
2828
|
+
output.fail();
|
|
2584
2829
|
}
|
|
2585
2830
|
function hasRegisteredProjectUnder(rootPath) {
|
|
2586
2831
|
const cfg = loadConfig$1();
|
|
@@ -2590,28 +2835,40 @@ function projectAtOrAbove(root, path) {
|
|
|
2590
2835
|
const here = nativeCanonicalPath(path);
|
|
2591
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;
|
|
2592
2837
|
}
|
|
2593
|
-
async function reclaimEnvironment(root, why) {
|
|
2838
|
+
async function reclaimEnvironment(root, why, output, operation, beforeRemove) {
|
|
2594
2839
|
await withManagedRemoteWorktreeRemovalLock(root, () => withReclaimLocks(root, async (lockedKeys) => {
|
|
2595
|
-
const result = await reclaimAll(root, lockedKeys, {
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
for (const
|
|
2600
|
-
for (const
|
|
2601
|
-
for (const
|
|
2602
|
-
for (const
|
|
2603
|
-
for (const
|
|
2604
|
-
|
|
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(", ")}`)));
|
|
2605
2853
|
if (result.keptEntries.length) {
|
|
2606
|
-
reportRetainedResources(root, result);
|
|
2854
|
+
reportRetainedResources(root, result, output);
|
|
2607
2855
|
return;
|
|
2608
2856
|
}
|
|
2609
|
-
for (const s of result.skippedDevices)
|
|
2610
|
-
|
|
2611
|
-
})
|
|
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
|
+
});
|
|
2612
2863
|
}
|
|
2613
|
-
|
|
2614
|
-
|
|
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);
|
|
2615
2872
|
}
|
|
2616
2873
|
function canonicalExistingPath(target) {
|
|
2617
2874
|
const resolved = resolve$1(target);
|
|
@@ -2625,9 +2882,9 @@ function canonicalExistingPath(target) {
|
|
|
2625
2882
|
}
|
|
2626
2883
|
return resolve$1(realpathSync$1(existing), ...missing);
|
|
2627
2884
|
}
|
|
2628
|
-
function inspectRemoval(path, mergedHead) {
|
|
2885
|
+
async function inspectRemoval(path, mergedHead, operation) {
|
|
2629
2886
|
try {
|
|
2630
|
-
return inspectGit(path, mergedHead);
|
|
2887
|
+
return await inspectGit(path, mergedHead, operation);
|
|
2631
2888
|
} catch (error) {
|
|
2632
2889
|
if (!isTimeoutError(error)) throw error;
|
|
2633
2890
|
return timedOutInspection(error);
|
|
@@ -2643,18 +2900,24 @@ function timedOutInspection(error) {
|
|
|
2643
2900
|
timedOut: true
|
|
2644
2901
|
};
|
|
2645
2902
|
}
|
|
2646
|
-
function inspectGit(path, mergedHead) {
|
|
2647
|
-
const
|
|
2648
|
-
|
|
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) : [];
|
|
2649
2912
|
const { lines: withoutCookies, cookies } = excludeWatchmanCookies(allDirty ?? []);
|
|
2650
2913
|
const { lines: dirtyLines, restore: podChurn } = excludePodChurn(withoutCookies, customIosDirs(path));
|
|
2651
2914
|
const dirty = gitAnswered === null || allDirty === null ? null : dirtyLines.length > 0;
|
|
2652
|
-
const unpushed =
|
|
2915
|
+
const unpushed = await unpushedCommitsAsync(path, options);
|
|
2653
2916
|
const blockers = removalBlockers({
|
|
2654
2917
|
dirty,
|
|
2655
|
-
unpushed: Boolean(mergedHead) &&
|
|
2918
|
+
unpushed: Boolean(mergedHead) && await resolveFullRefAsync(path, "HEAD", options) === mergedHead && unpushed ? [] : unpushed
|
|
2656
2919
|
});
|
|
2657
|
-
const submodules =
|
|
2920
|
+
const submodules = await hasPopulatedSubmodulesAsync(path, options);
|
|
2658
2921
|
if (submodules === null) blockers.push("could not check for initialized submodules; re-run with --force to override");
|
|
2659
2922
|
else if (submodules) blockers.push("initialized submodules, which git removes only with --force");
|
|
2660
2923
|
return {
|
|
@@ -2665,43 +2928,50 @@ function inspectGit(path, mergedHead) {
|
|
|
2665
2928
|
blockers
|
|
2666
2929
|
};
|
|
2667
2930
|
}
|
|
2668
|
-
function printRemovalRefusal(path, inspection) {
|
|
2931
|
+
async function printRemovalRefusal(path, inspection, output, operation) {
|
|
2669
2932
|
const { blockers, dirtyLines, unpushed } = inspection;
|
|
2670
|
-
|
|
2671
|
-
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}`));
|
|
2672
2935
|
if (inspection.timedOut) {
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
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();
|
|
2676
2939
|
return;
|
|
2677
2940
|
}
|
|
2678
|
-
if (unpushed && unpushed.length && !
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
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();
|
|
2687
2953
|
}
|
|
2688
2954
|
function deleteWatchmanCookies(path, cookies) {
|
|
2689
2955
|
for (const cookie of cookies) rmSync$1(join$1(path, cookie), { force: true });
|
|
2690
2956
|
}
|
|
2691
|
-
function restorePodChurn(path, files) {
|
|
2692
|
-
for (const file of files) if (
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
for (const
|
|
2701
|
-
for (const
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
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})`)));
|
|
2705
2975
|
}
|
|
2706
2976
|
async function removeWorktreeTarget(target, opts = {}) {
|
|
2707
2977
|
let removed = false;
|
|
@@ -2711,222 +2981,245 @@ async function removeWorktreeTarget(target, opts = {}) {
|
|
|
2711
2981
|
return removed;
|
|
2712
2982
|
}
|
|
2713
2983
|
async function runRemove(target, opts, onRemoved) {
|
|
2984
|
+
const output = opts.output ?? REMOVAL_OUTPUT;
|
|
2714
2985
|
try {
|
|
2715
|
-
await removeOrRefuse(target, opts, onRemoved);
|
|
2986
|
+
await removeOrRefuse(target, opts, onRemoved, output);
|
|
2716
2987
|
} catch (error) {
|
|
2988
|
+
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
2989
|
+
opts.operation?.signal.throwIfAborted();
|
|
2717
2990
|
if (!isTimeoutError(error)) throw error;
|
|
2718
|
-
printRemovalRefusal(removalPath(target), timedOutInspection(error));
|
|
2991
|
+
await printRemovalRefusal(removalPath(target, opts.operation?.cwd), timedOutInspection(error), output, opts.operation);
|
|
2719
2992
|
}
|
|
2720
2993
|
}
|
|
2721
|
-
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();
|
|
2722
3002
|
const poolError = parkedMaxSetting("ios").error || parkedMaxSetting("android").error;
|
|
2723
3003
|
if (poolError) {
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
3004
|
+
output.error(chalk.red(poolError));
|
|
3005
|
+
output.error(chalk.dim(POOL_SETTING_REMEDY));
|
|
3006
|
+
output.fail();
|
|
2727
3007
|
return;
|
|
2728
3008
|
}
|
|
2729
|
-
let path = removalPath(target);
|
|
3009
|
+
let path = removalPath(target, opts.operation?.cwd);
|
|
2730
3010
|
if (!existsSync$1(path)) {
|
|
3011
|
+
opts.beforeRemove?.(path);
|
|
2731
3012
|
const pending = getProject(path);
|
|
2732
3013
|
if (pending?.worktreeRemovalComplete === true && pending.worktreeBranchOwned === true && pending.worktreeBranch && pending.worktreeMainRoot && pending.worktreePendingBranchSha) {
|
|
2733
3014
|
const branch = pending.worktreeBranch;
|
|
2734
3015
|
const mainRoot = pending.worktreeMainRoot;
|
|
2735
3016
|
if (!existsSync$1(mainRoot)) {
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
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();
|
|
2739
3020
|
return;
|
|
2740
3021
|
}
|
|
2741
|
-
|
|
3022
|
+
opts.beforeRemove?.(removalPath(mainRoot));
|
|
3023
|
+
if (!await branchExistsAsync(mainRoot, branch, options)) {
|
|
2742
3024
|
removeProject(path);
|
|
2743
|
-
|
|
3025
|
+
output.error(chalk.dim(phaseLine("branch", `${branch} is already absent; cleared its pending cleanup record`)));
|
|
2744
3026
|
return;
|
|
2745
3027
|
}
|
|
2746
|
-
const checkedOutAt =
|
|
3028
|
+
const checkedOutAt = (await listWorktreesAsync(mainRoot, options)).find((candidate) => candidate.branch === branch && resolve$1(candidate.path) !== resolve$1(path))?.path;
|
|
2747
3029
|
if (checkedOutAt) {
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
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();
|
|
2751
3033
|
return;
|
|
2752
3034
|
}
|
|
2753
|
-
const currentSha =
|
|
3035
|
+
const currentSha = await resolveFullRefAsync(mainRoot, branch, options);
|
|
2754
3036
|
if (currentSha !== pending.worktreePendingBranchSha) {
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
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();
|
|
2758
3040
|
return;
|
|
2759
3041
|
}
|
|
2760
3042
|
try {
|
|
2761
|
-
|
|
3043
|
+
await deleteBranchAsync(mainRoot, branch, pending.worktreePendingBranchSha, options);
|
|
2762
3044
|
removeProject(path);
|
|
2763
|
-
|
|
3045
|
+
output.error(chalk.dim(phaseLine("branch", `deleted ${branch}; cleared its pending cleanup record`)));
|
|
2764
3046
|
} catch (error) {
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
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();
|
|
2768
3051
|
}
|
|
2769
3052
|
return;
|
|
2770
3053
|
}
|
|
2771
|
-
|
|
2772
|
-
|
|
3054
|
+
output.error(chalk.red(`No such worktree: ${path}`));
|
|
3055
|
+
output.fail();
|
|
2773
3056
|
return;
|
|
2774
3057
|
}
|
|
2775
|
-
const worktrees =
|
|
3058
|
+
const worktrees = await listWorktreesAsync(path, options);
|
|
2776
3059
|
const entry = matchWorktreeEntry(worktrees, path);
|
|
2777
3060
|
if (!entry) {
|
|
2778
|
-
if (!opts.linkedOnly &&
|
|
2779
|
-
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);
|
|
2780
3063
|
return;
|
|
2781
3064
|
}
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
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();
|
|
2785
3068
|
return;
|
|
2786
3069
|
}
|
|
2787
3070
|
if (entry.bare) {
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
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();
|
|
2791
3074
|
return;
|
|
2792
3075
|
}
|
|
2793
3076
|
const source = sourceCheckoutOf(worktrees);
|
|
2794
3077
|
if ("refusal" in source) {
|
|
2795
|
-
|
|
2796
|
-
|
|
3078
|
+
output.error(chalk.red(`Refusing to remove ${path}: ${source.refusal}`));
|
|
3079
|
+
output.fail();
|
|
2797
3080
|
return;
|
|
2798
3081
|
}
|
|
2799
3082
|
if (entry.path === source.path && opts.linkedOnly) {
|
|
2800
|
-
|
|
2801
|
-
|
|
3083
|
+
output.error(chalk.red(`Refusing to remove ${path}: ${entry.path} is the source checkout.`));
|
|
3084
|
+
output.fail();
|
|
2802
3085
|
return;
|
|
2803
3086
|
}
|
|
2804
3087
|
if (entry.path === source.path) {
|
|
2805
3088
|
if (nativeCanonicalPath(entry.path) !== nativeCanonicalPath(path)) {
|
|
2806
3089
|
const project = projectAtOrAbove(entry.path, path);
|
|
2807
3090
|
if (!project) {
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
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();
|
|
2811
3094
|
return;
|
|
2812
3095
|
}
|
|
2813
|
-
|
|
2814
|
-
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);
|
|
2815
3098
|
return;
|
|
2816
3099
|
}
|
|
2817
|
-
await reclaimEnvironment(entry.path, "it is the source checkout");
|
|
3100
|
+
await reclaimEnvironment(entry.path, "it is the source checkout", output, opts.operation, opts.beforeRemove);
|
|
2818
3101
|
return;
|
|
2819
3102
|
}
|
|
2820
3103
|
if (entry.path !== path) {
|
|
2821
|
-
|
|
3104
|
+
output.error(chalk.dim(`${path} is inside the worktree ${entry.path}; removing that.`));
|
|
2822
3105
|
path = entry.path;
|
|
2823
3106
|
}
|
|
2824
3107
|
if (entry.locked) {
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
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();
|
|
2828
3111
|
return;
|
|
2829
3112
|
}
|
|
2830
3113
|
const nested = nestedWorktrees(worktrees, entry.path);
|
|
2831
3114
|
if (nested.length) {
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
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();
|
|
2835
3118
|
return;
|
|
2836
3119
|
}
|
|
2837
3120
|
const service = serviceRunningFrom(path, nativeCanonicalPath);
|
|
2838
3121
|
if (service) {
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
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();
|
|
2842
3125
|
return;
|
|
2843
3126
|
}
|
|
2844
3127
|
const project = getProject(path);
|
|
2845
3128
|
const branch = entry.branch;
|
|
2846
3129
|
const ownsBranch = Boolean(branch && project?.worktreeBranchOwned === true && project.worktreeBranch === branch);
|
|
2847
|
-
const approvedBranchSha = ownsBranch ?
|
|
3130
|
+
const approvedBranchSha = ownsBranch ? await resolveFullRefAsync(path, "HEAD", options) : null;
|
|
2848
3131
|
let mergedHead = opts.mergedHead;
|
|
2849
|
-
let inspection = inspectRemoval(path, mergedHead);
|
|
3132
|
+
let inspection = await inspectRemoval(path, mergedHead, opts.operation);
|
|
2850
3133
|
if (inspection.blockers.length && !opts.force && !mergedHead && branch && inspection.unpushed?.length && !inspection.dirtyLines.length) {
|
|
2851
|
-
const head =
|
|
2852
|
-
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;
|
|
2853
3136
|
if (head && pr?.state === "merged") {
|
|
2854
3137
|
mergedHead = head;
|
|
2855
|
-
inspection = inspectRemoval(path, mergedHead);
|
|
2856
|
-
|
|
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.`));
|
|
2857
3140
|
}
|
|
2858
3141
|
}
|
|
2859
3142
|
if (inspection.blockers.length && !opts.force) {
|
|
2860
|
-
printRemovalRefusal(path, inspection);
|
|
3143
|
+
await printRemovalRefusal(path, inspection, output, opts.operation);
|
|
2861
3144
|
return;
|
|
2862
3145
|
}
|
|
2863
3146
|
const deleteOwnedBranch = Boolean(ownsBranch && approvedBranchSha && inspection.unpushed?.length === 0);
|
|
2864
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;
|
|
2865
3148
|
const branchDeleteCwd = source.path;
|
|
2866
3149
|
await withManagedRemoteWorktreeRemovalLock(path, () => withReclaimLocks(path, async (lockedKeys) => {
|
|
2867
|
-
|
|
2868
|
-
|
|
3150
|
+
options.signal?.throwIfAborted();
|
|
3151
|
+
if ((await opts.guard?.(lockedKeys))?.length) return;
|
|
3152
|
+
options.signal?.throwIfAborted();
|
|
3153
|
+
const inspectedHead = await resolveFullRefAsync(path, "HEAD", options);
|
|
2869
3154
|
if (opts.expectedHead && inspectedHead !== opts.expectedHead) {
|
|
2870
|
-
|
|
2871
|
-
|
|
3155
|
+
output.error(chalk.red(`Refusing to remove ${path}: its HEAD moved since it was checked.`));
|
|
3156
|
+
output.fail();
|
|
2872
3157
|
return;
|
|
2873
3158
|
}
|
|
2874
|
-
const current = inspectRemoval(path, mergedHead);
|
|
3159
|
+
const current = await inspectRemoval(path, mergedHead, opts.operation);
|
|
2875
3160
|
if (current.blockers.length && !opts.force) {
|
|
2876
|
-
printRemovalRefusal(path, current);
|
|
3161
|
+
await printRemovalRefusal(path, current, output, opts.operation);
|
|
2877
3162
|
return;
|
|
2878
3163
|
}
|
|
2879
3164
|
const result = await reclaimAll(path, lockedKeys, {
|
|
2880
3165
|
preserveRootProject: true,
|
|
2881
|
-
removedBy: opts.removedBy ?? "worktree-remove"
|
|
3166
|
+
removedBy: opts.removedBy ?? "worktree-remove",
|
|
3167
|
+
operation: opts.operation
|
|
2882
3168
|
});
|
|
2883
3169
|
if (result.keptEntries.length) {
|
|
2884
|
-
reportRetainedResources(path, result);
|
|
3170
|
+
reportRetainedResources(path, result, output);
|
|
2885
3171
|
return;
|
|
2886
3172
|
}
|
|
2887
3173
|
let headMoved = false;
|
|
2888
3174
|
if (!opts.force) try {
|
|
2889
|
-
headMoved =
|
|
3175
|
+
headMoved = await resolveFullRefAsync(path, "HEAD", options) !== inspectedHead;
|
|
2890
3176
|
} catch (error) {
|
|
3177
|
+
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
3178
|
+
opts.operation?.signal.throwIfAborted();
|
|
2891
3179
|
if (!isTimeoutError(error)) throw error;
|
|
2892
|
-
printRemovalRefusal(path, timedOutInspection(error));
|
|
2893
|
-
|
|
2894
|
-
printRemovalCleanup(result, true);
|
|
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);
|
|
2895
3183
|
return;
|
|
2896
3184
|
}
|
|
2897
3185
|
if (headMoved) {
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
printRemovalCleanup(result, true);
|
|
2901
|
-
|
|
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();
|
|
2902
3190
|
return;
|
|
2903
3191
|
}
|
|
2904
|
-
restorePodChurn(path, current.podChurn);
|
|
3192
|
+
await restorePodChurn(path, current.podChurn, output, opts.operation);
|
|
3193
|
+
options.signal?.throwIfAborted();
|
|
2905
3194
|
deleteWatchmanCookies(path, current.cookies);
|
|
2906
3195
|
try {
|
|
2907
|
-
|
|
3196
|
+
await removeWorktreeAsync(path, {
|
|
2908
3197
|
from: source.path,
|
|
2909
|
-
force: opts.force
|
|
3198
|
+
force: opts.force,
|
|
3199
|
+
env: options.env,
|
|
3200
|
+
signal: options.signal,
|
|
3201
|
+
declareFor: opts.operation
|
|
2910
3202
|
});
|
|
2911
3203
|
} catch (error) {
|
|
3204
|
+
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
2912
3205
|
const message = String(error?.message || error);
|
|
2913
|
-
|
|
2914
|
-
if (process.platform === "win32" && /Permission denied/i.test(message))
|
|
2915
|
-
|
|
2916
|
-
printRemovalCleanup(result, true);
|
|
2917
|
-
|
|
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();
|
|
2918
3211
|
return;
|
|
2919
3212
|
}
|
|
2920
3213
|
onRemoved();
|
|
2921
3214
|
forgetStatusMeasures(path, result.reclaimedKeys);
|
|
2922
3215
|
const finish = () => {
|
|
2923
|
-
printRemovalCleanup(result, false);
|
|
2924
|
-
|
|
3216
|
+
printRemovalCleanup(result, false, output);
|
|
3217
|
+
output.error(chalk.dim(phaseLine("removed", path)));
|
|
2925
3218
|
};
|
|
2926
3219
|
if (deleteOwnedBranch && branch) {
|
|
2927
3220
|
if (!approvedBranchSha) {
|
|
2928
|
-
|
|
2929
|
-
|
|
3221
|
+
output.error(chalk.yellow(phaseLine("branch", `kept ${branch} (Stim could not record its commit before removal)`)));
|
|
3222
|
+
output.fail();
|
|
2930
3223
|
finish();
|
|
2931
3224
|
return;
|
|
2932
3225
|
}
|
|
@@ -2936,36 +3229,42 @@ async function removeOrRefuse(target, opts, onRemoved) {
|
|
|
2936
3229
|
});
|
|
2937
3230
|
let checkedOutAt;
|
|
2938
3231
|
try {
|
|
2939
|
-
checkedOutAt =
|
|
3232
|
+
checkedOutAt = (await listWorktreesAsync(branchDeleteCwd, options)).find((candidate) => candidate.branch === branch && resolve$1(candidate.path) !== resolve$1(path))?.path;
|
|
2940
3233
|
} catch (error) {
|
|
3234
|
+
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
3235
|
+
opts.operation?.signal.throwIfAborted();
|
|
2941
3236
|
if (!isTimeoutError(error)) throw error;
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
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();
|
|
2945
3240
|
finish();
|
|
2946
3241
|
return;
|
|
2947
3242
|
}
|
|
2948
3243
|
if (checkedOutAt) {
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
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();
|
|
2952
3247
|
finish();
|
|
2953
3248
|
return;
|
|
2954
3249
|
}
|
|
2955
3250
|
try {
|
|
2956
|
-
|
|
2957
|
-
|
|
3251
|
+
await deleteBranchAsync(branchDeleteCwd, branch, approvedBranchSha, options);
|
|
3252
|
+
output.error(chalk.dim(phaseLine("branch", `deleted ${branch}`)));
|
|
2958
3253
|
} catch (error) {
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
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();
|
|
2962
3258
|
finish();
|
|
2963
3259
|
return;
|
|
2964
3260
|
}
|
|
2965
|
-
} else if (branch && retainedBranchReason)
|
|
3261
|
+
} else if (branch && retainedBranchReason) output.error(chalk.dim(phaseLine("branch", `kept ${branch} (${retainedBranchReason})`)));
|
|
2966
3262
|
removeProject(path);
|
|
2967
3263
|
finish();
|
|
2968
|
-
})
|
|
3264
|
+
}, opts.operation, opts.beforeRemove), {
|
|
3265
|
+
signal: opts.operation?.signal,
|
|
3266
|
+
declareSpawnsFor: opts.operation
|
|
3267
|
+
});
|
|
2969
3268
|
}
|
|
2970
3269
|
function registerRemove(worktree) {
|
|
2971
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) => {
|
|
@@ -2978,4 +3277,4 @@ function worktreeCommand(program) {
|
|
|
2978
3277
|
registerRemove(worktree);
|
|
2979
3278
|
}
|
|
2980
3279
|
//#endregion
|
|
2981
|
-
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 };
|