stim 1.26.0 → 1.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{acquire-artifact-DVsvSwf7.mjs → acquire-artifact-BKN2Yuhg.mjs} +2 -2
- package/dist/{act-C-BqJEpu.mjs → act-ROviTF_t.mjs} +297 -122
- package/dist/{activity-Lnb_EoRY.mjs → activity-Cd-ysFes.mjs} +6 -6
- package/dist/{agent-device-usage-output-DbXy5tg8.mjs → agent-device-usage-output-C5nCKWp9.mjs} +5 -5
- package/dist/{android-CSbLRD53.mjs → android--HBgpvpW.mjs} +5 -5
- package/dist/{android-CiNQw3kq.mjs → android-Cd6w7_qw.mjs} +96 -43
- package/dist/{android-B5P81yI-.mjs → android-Deb9X3_Y.mjs} +37 -15
- package/dist/{android-DTAcRwBC.mjs → android-DriNoAVy.mjs} +23 -18
- package/dist/{android-cas-BTVhZ0Qs.mjs → android-cas-D5e6ecUg.mjs} +4 -4
- package/dist/android-cas-compiler.mjs +1 -1
- package/dist/api.d.mts +13 -3
- package/dist/api.mjs +39 -29
- package/dist/{apk-signed-tail-B46U9hXc.mjs → apk-signed-tail-BHQHx3Rm.mjs} +2 -2
- package/dist/{artifact-BxREhAaV.mjs → artifact-0AGJYB9O.mjs} +18 -23
- package/dist/{artifact-CsAP6AGK.mjs → artifact-faemIZv5.mjs} +19 -24
- package/dist/{artifact-platform-CMRc0M_r.mjs → artifact-platform-BO2Uvk_A.mjs} +1 -1
- package/dist/auto-placement-z-H5pJza.mjs +350 -0
- package/dist/{browser-web-rYPyqhTf.mjs → browser-web-slD8AvLp.mjs} +16 -16
- package/dist/{build-cache-DrXNCy3T.mjs → build-cache-gBJ-WKzA.mjs} +43 -21
- package/dist/{build-DPhApsjV.mjs → build-hS0QyFlQ.mjs} +35 -31
- package/dist/{build-plan-DrMMdayj.mjs → build-plan-CBqYbzYM.mjs} +7 -6
- package/dist/{cache-manifest-BEZ9Bpl1.mjs → cache-manifest-DEjqGGat.mjs} +1 -1
- package/dist/cache-manifest.mjs +1 -1
- package/dist/{budget-k2mZGvvk.mjs → ccache-BWTze1Ls.mjs} +560 -1603
- package/dist/{cdp-BG3FJCCx.mjs → cdp-C3svvXiE.mjs} +1 -1
- package/dist/{chrome-BvV8ssI3.mjs → chrome-DyDWurWG.mjs} +1 -1
- package/dist/{cli-BBKFGOFl.mjs → cli-ftXDLto1.mjs} +24 -23
- package/dist/cli.mjs +1 -1
- package/dist/{client-D9CaRks-.mjs → client-xsdxrhWA.mjs} +48 -62
- package/dist/collector-run.d.mts +10 -9
- package/dist/collector-run.mjs +42 -27
- package/dist/{command-output-BF5AR2dL.mjs → command-output-Dj-WT4BP.mjs} +1 -1
- package/dist/{config-BktkY_zC.mjs → config-Cn94Jc0g.mjs} +23 -4
- package/dist/{dependencies-Dw6wfQdv.mjs → dependencies-BGokZ-Ng.mjs} +108 -60
- package/dist/{dependency-state-DFB41vfr.mjs → dependency-state-C3OtgG94.mjs} +14 -7
- package/dist/{detached-entry-KmieOKgN.mjs → detached-entry-BPlSaCwC.mjs} +1 -1
- package/dist/{dev-client-DXR_tPVY.mjs → dev-client-BMca82xo.mjs} +2 -2
- package/dist/{device-BcYeHSFu.mjs → device-BJm0irrq.mjs} +16 -16
- package/dist/{device-capacity-DCSvUF0l.mjs → device-capacity-6LP7QsaA.mjs} +101 -16
- package/dist/device-host-worker.mjs +19 -19
- package/dist/{device-ios-BLGfXQeK.mjs → device-ios-534c14fz.mjs} +251 -23
- package/dist/{device-lease-run-BmvIl7bI.mjs → device-lease-run-DwsGfjUN.mjs} +5 -5
- package/dist/{device-pool-Dluix4H3.mjs → device-pool-BnQqvs6o.mjs} +3 -3
- package/dist/{device-BK-zwyd_.mjs → device-tM41-TUq.mjs} +25 -24
- package/dist/{doctor-Cp-BhzTS.mjs → doctor-Dbx1ZpD2.mjs} +1 -1
- package/dist/{doctor-Bhg1ii70.mjs → doctor-DyqE9n1J.mjs} +28 -16
- package/dist/{doctor-OnwP94ne.mjs → doctor-F-3VUUyN.mjs} +76 -32
- package/dist/{doctor-fingerprint-D3a04Mhu.mjs → doctor-fingerprint-o7ZI9YLn.mjs} +1 -1
- package/dist/doctor-fingerprint.mjs +1 -1
- package/dist/{doctor-watchman-BStK05nt.mjs → doctor-watchman-CxDbyrrt.mjs} +5 -5
- package/dist/{eas-build-Dsj93iM9.mjs → eas-build-CjNSWmjo.mjs} +4 -4
- package/dist/eas-fallback-9fqb46kW.mjs +87 -0
- package/dist/{eas-session-ledger-B5vygTiD.mjs → eas-session-ledger-DrP4OuuM.mjs} +6 -6
- package/dist/{error-diagnostics-BOOm9Db7.mjs → error-diagnostics-snej9k4j.mjs} +51 -25
- package/dist/{exec-VFRpu1iA.mjs → exec-aULwtNly.mjs} +57 -6
- package/dist/{gc-C_79zvz4.mjs → gc-BRwAY3qT.mjs} +2 -2
- package/dist/{gc-ldgr-hXU.mjs → gc-CGZ1Yg88.mjs} +160 -45
- package/dist/{guide-D7K0X6KG.mjs → guide-_rfEh_zM.mjs} +580 -183
- package/dist/{hosted-android-Bk54ej6n.mjs → hosted-android-BMKzUNnY.mjs} +4 -4
- package/dist/{hosted-client-fHO8yqzc.mjs → hosted-client-D7Pk_Oo1.mjs} +12 -8
- package/dist/{hosted-logs-BfbmDM4M.mjs → hosted-logs-B1gqQuva.mjs} +8 -8
- package/dist/{hosted-logs-sync-B0pvmOk-.mjs → hosted-logs-sync-Dg3qrZmV.mjs} +5 -5
- package/dist/{hosted-macos-CPl0OZ9h.mjs → hosted-macos-B9w4H8hc.mjs} +6 -6
- package/dist/{hosted-native-CpDB3-PO.mjs → hosted-native-CdgaiuyP.mjs} +17 -16
- package/dist/{idle-shutdown-FfxL1KdG.mjs → idle-shutdown-Cbvo5KEh.mjs} +7 -8
- package/dist/{ios-ftxdQbJS.mjs → ios-B5HlVMhQ.mjs} +92 -37
- package/dist/{ios-B7-lToDu.mjs → ios-Bk4mWbtS.mjs} +8 -8
- package/dist/{ios-ENC4sRPA.mjs → ios-CsRVik64.mjs} +7 -1
- package/dist/{ios-WxpDJ5T1.mjs → ios-E26qSpkb.mjs} +27 -11
- package/dist/{ios-device-CsyryvpT.mjs → ios-device-C_QkIf1Z.mjs} +3 -3
- package/dist/{ios-device-B6HK951C.mjs → ios-device-CiBA-er9.mjs} +1 -1
- package/dist/{ios-project-Ty5pHl9O.mjs → ios-project-D8Jcy_br.mjs} +22 -9
- package/dist/{ios-signing-4c_qXvQm.mjs → ios-signing-BJzvDPfU.mjs} +2 -2
- package/dist/{ios-state-Cw7-WQf2.mjs → ios-state-BZ03NFgF.mjs} +1 -1
- package/dist/{js-swap-BMhXYVDS.mjs → js-swap-Dg2a945i.mjs} +8 -7
- package/dist/{launch-DOp8RjDz.mjs → launch-CRyBeNsA.mjs} +42 -38
- package/dist/lightened-BCsdAGj_.mjs +171 -0
- package/dist/{logs-dQDki6Tt.mjs → logs-CC_-ZzEE.mjs} +7 -7
- package/dist/{logs-s1dRIG1E.mjs → logs-aJQb-RDg.mjs} +46 -21
- package/dist/{macos-B0VEV3Gf.mjs → macos-BGmTpnv9.mjs} +1 -1
- package/dist/{macos-DBBPNnIA.mjs → macos-DlCBhMw5.mjs} +73 -38
- package/dist/macos-run.mjs +1 -1
- package/dist/maintenance-run.mjs +7 -7
- package/dist/{metro-CjC10t6E.mjs → metro-CvyD-xwe.mjs} +4 -4
- package/dist/{metro-gateway-B6_PgD0L.mjs → metro-gateway-BnHdqMO_.mjs} +3 -3
- package/dist/{named-ports-BG0j1X4g.mjs → named-ports-BCyMWA6b.mjs} +3 -3
- package/dist/{native-android-CL2NGiik.mjs → native-android-DlpxCyOP.mjs} +17 -18
- package/dist/{native-gradle-artifact-inputs-btSUulme.mjs → native-gradle-artifact-inputs-C13qT7R_.mjs} +5 -5
- package/dist/{native-runtime-Dq3RBcON.mjs → native-runtime-DqaIBO5w.mjs} +9 -8
- package/dist/{native-source-D-4Dh4JU.mjs → native-source-D1AzWiuP.mjs} +1 -1
- package/dist/{native-xcode-inputs-BW7H8JGu.mjs → native-xcode-inputs-B1W8SrxB.mjs} +3 -3
- package/dist/{native-xcode-ios-CBXJAxfC.mjs → native-xcode-ios-QPP0Rm68.mjs} +17 -17
- package/dist/{ndjson-CgE_o3Dl.mjs → ndjson-Dy2W4vRa.mjs} +2 -2
- package/dist/offload-worker.mjs +23 -24
- package/dist/{operation-exec-CUZifp6Z.mjs → operation-exec-Dp4FuIGR.mjs} +1 -1
- package/dist/{ownership-BjYvMNX_.mjs → ownership-Bx-0cku-.mjs} +3 -3
- package/dist/{page-CzEA_-5_.mjs → page-CQJlcj3x.mjs} +2 -2
- package/dist/{phone-reverses-lmj6kcPm.mjs → phone-reverses-C9yAaKEv.mjs} +18 -4
- package/dist/{placement-log-DIFPQAXp.mjs → placement-log-BXUbiZM6.mjs} +3 -3
- package/dist/{plan-placement-IiiSQhtz.mjs → plan-placement-UxnQjv5Z.mjs} +36 -10
- package/dist/{ports-C2_w1Z0Z.mjs → ports-uUrghqWN.mjs} +3 -3
- package/dist/{prebuild-gmouvmJZ.mjs → prebuild-BDpToDgR.mjs} +16 -17
- package/dist/{project-BUu6Is4k.mjs → project-DYj-HVx6.mjs} +17 -17
- package/dist/{pull-request-CbNlkFok.mjs → pull-request-BzLmzAIQ.mjs} +2 -2
- package/dist/pull-requests.mjs +1 -1
- package/dist/{react-native-android-Cgg99REN.mjs → react-native-android-DDBccgJw.mjs} +32 -26
- package/dist/{react-native-build-oNwqF5Wb.mjs → react-native-build-DaK2IFyP.mjs} +9 -9
- package/dist/{react-native-doctor-BhA3KUte.mjs → react-native-doctor-B-Mw6DcR.mjs} +6 -6
- package/dist/{react-native-ios-B-nf4rjN.mjs → react-native-ios-DjYwUF9U.mjs} +23 -17
- package/dist/{recordings-BldzVKq0.mjs → recordings-C20iUZfp.mjs} +1 -1
- package/dist/{registration-CN6nlhcD.mjs → registration-BwCe64PH.mjs} +1 -1
- package/dist/{reload-C9OduiNc.mjs → reload-DJBeF_rC.mjs} +1 -1
- package/dist/{reload-CCR7KC_l.mjs → reload-fUdkH2_p.mjs} +16 -16
- package/dist/{remote-C8S7FlPF.mjs → remote-y6n99WMW.mjs} +22 -9
- package/dist/{result-7F5hEwQJ.mjs → result-CTqBw2Te.mjs} +32 -21
- package/dist/{run-BqB0v3pt.mjs → run-DaCeNKRm.mjs} +3 -3
- package/dist/{selection-BNPBZ6T5.mjs → selection-Bs8phwhN.mjs} +37 -29
- package/dist/{server-bare-A75rDIdD.mjs → server-bare-BOh-iZEj.mjs} +2 -2
- package/dist/{server-command-CssKFOD7.mjs → server-command-8CuA51Cv.mjs} +6 -6
- package/dist/{server-expo-eQfOdydb.mjs → server-expo-ABQs2XFA.mjs} +4 -4
- package/dist/server-expo-Ba_Y21vB.mjs +2 -0
- package/dist/{settings-CosW5X-S.mjs → settings-BDVI8Ivc.mjs} +331 -62
- package/dist/{settings-OaGQF-ri.mjs → settings-CMHhQNwI.mjs} +24 -3
- package/dist/settings-payload-cm60Zhib.mjs +257 -0
- package/dist/settings-write-BdNRLlBn.mjs +185 -0
- package/dist/settings.schema.json +51 -33
- package/dist/{simslim-Bax1qcxu.mjs → simslim-DCQwzoPy.mjs} +2 -2
- package/dist/{slot-launch-CYCEhfBY.mjs → slot-launch-0l7RqSpr.mjs} +6 -6
- package/dist/{stage-jKQ7CP_B.mjs → stage-F4cAfJGi.mjs} +2 -2
- package/dist/{start-C_wefNKB.mjs → start-DcqnF9p8.mjs} +1 -1
- package/dist/{start-_BdJ32cZ.mjs → start-DrPwER_2.mjs} +27 -22
- package/dist/{state-C1i2nMOP.mjs → state-BX1y5syV.mjs} +4 -4
- package/dist/{state-C4OdpOTF.mjs → state-CZxLyetD.mjs} +2 -2
- package/dist/{stats-tEpEEH-u.mjs → stats-GxuCbles.mjs} +5 -5
- package/dist/{stats-report-n2U5plhk.mjs → stats-report-B86Cv7Uc.mjs} +1 -1
- package/dist/{status-CRXz-Gtj.mjs → status-7ULDEJ1e.mjs} +1 -1
- package/dist/{status-DQrqodLj.mjs → status-BfnK9pUd.mjs} +25 -25
- package/dist/{status-B9M7x4CZ.mjs → status-Bj7ZYvvc.mjs} +1 -1
- package/dist/{status-watch-B5wDIH_o.mjs → status-watch-BfabQn03.mjs} +5 -5
- package/dist/status-worker.mjs +1 -1
- package/dist/{stim-desktop-6Dqp-gjX.mjs → stim-desktop-CbWo21kb.mjs} +2 -2
- package/dist/{stop-ySNQHXMG.mjs → stop-CH79S3IZ.mjs} +5 -5
- package/dist/{stop-DRqQVqDJ.mjs → stop-D-EAr25B.mjs} +21 -20
- package/dist/{stop-CmdNhGlC.mjs → stop-x1Yu3He4.mjs} +2 -2
- package/dist/supervisor-run.mjs +17 -17
- package/dist/{support-CVOCp0Ft.mjs → support-Be0UeVZU.mjs} +10 -10
- package/dist/{support-BGSCBoib.mjs → support-CNg_YsOk.mjs} +6 -6
- package/dist/{swiftpm-artifact-inputs-CfUzt_0Z.mjs → swiftpm-artifact-inputs-uPqYvGBv.mjs} +4 -4
- package/dist/{swiftpm-macos-x6HPpl_2.mjs → swiftpm-macos-Ch4YjY_A.mjs} +11 -12
- package/dist/{toolchain-J4J8MrAr.mjs → toolchain-CbCG6C5Z.mjs} +321 -15
- package/dist/{toolchain-DoavrvIA.mjs → toolchain-JYAOquPg.mjs} +1 -1
- package/dist/{trigger-KVaWWNyn.mjs → trigger-DMou-X7j.mjs} +2 -2
- package/dist/trigger-qp2ji5wE.mjs +2 -0
- package/dist/{warm-progress-DvhTX_eu.mjs → warm-progress-C0RsbngN.mjs} +731 -727
- package/dist/{web-Bihbf7-u.mjs → web-Cq_AQ4DK.mjs} +9 -9
- package/dist/web-Je6abcpz.mjs +2 -0
- package/dist/web-run.mjs +9 -9
- package/dist/windows-report.mjs +1 -1
- package/dist/{workspace-state-pUg3_RRA.mjs → workspace-state-BjqdjnJR.mjs} +3 -3
- package/dist/{workspaces-g2lhdBXF.mjs → workspaces-DFbj07jp.mjs} +1795 -70
- package/dist/{worktree-BKuklFh_.mjs → worktree-COAJ3-OD.mjs} +934 -514
- package/dist/{worktree-Bm4cb-nU.mjs → worktree-JGUd_53L.mjs} +1 -1
- package/package.json +7 -7
- package/dist/ccache-Bx5TbxwG.mjs +0 -143
- package/dist/eas-fallback-FwkeqqGi.mjs +0 -381
- package/dist/hosted-ios-D55QhYaD.mjs +0 -15
- package/dist/native-run-BfIDFKLH.mjs +0 -275
- package/dist/server-expo-wvcj5tiZ.mjs +0 -2
- package/dist/settings-write-QjYfSrjF.mjs +0 -360
- package/dist/trigger-DzJu208Q.mjs +0 -2
- package/dist/web-BFLueSh-.mjs +0 -2
|
@@ -1,209 +1,45 @@
|
|
|
1
|
-
import { D as ownership_claim_exports, S as process_identity_exports, b as operationState, l as WorkspaceProcessCleanupError, m as declareSpawnsOn, n as isTimeoutError, t as getExecutor, v as stopDeclaringSpawnsOn } from "./exec-
|
|
2
|
-
import { E as workspaceDir, a as recordWorkspaceUseAsync, l as withWorkspaceStateLockAsync, r as readWorkspaceState$1, w as workspaceAgentDeviceDir } from "./workspace-state-
|
|
3
|
-
import { E as upsertProject, G as projectDeviceSlots, L as measuredDirectorySizeAsync, O as withConfigLockAsync$1, S as removeProject, _ as loadConfig$1, a as clearDevice, d as getConfigDir, g as isPathPrefix, j as acquireAvdClaim, p as getProject } from "./config-
|
|
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 { c as gitQuiet, o as git, r as childEnvironment, s as gitAsync, t as runOperationFile } from "./operation-exec-
|
|
6
|
-
import { At as
|
|
7
|
-
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-
|
|
8
|
-
import { t as macosAppPresent } from "./state-
|
|
9
|
-
import { a as workspaceLinkLine, o as workspaceLinks } from "./stim-desktop-
|
|
10
|
-
import { ft as POOL_SETTING_REMEDY, vt as parkedMaxSetting } from "./android-
|
|
11
|
-
import { c as pidExists, d as resolveProjectMetro, p as signalProcessTreeAsync, r as killMetroTreeAsync } from "./metro-
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
1
|
+
import { D as ownership_claim_exports, S as process_identity_exports, b as operationState, l as WorkspaceProcessCleanupError, m as declareSpawnsOn, n as isTimeoutError, t as getExecutor, v as stopDeclaringSpawnsOn } from "./exec-aULwtNly.mjs";
|
|
2
|
+
import { E as workspaceDir, a as recordWorkspaceUseAsync, l as withWorkspaceStateLockAsync, r as readWorkspaceState$1, w as workspaceAgentDeviceDir } from "./workspace-state-BjqdjnJR.mjs";
|
|
3
|
+
import { E as upsertProject, G as projectDeviceSlots, L as measuredDirectorySizeAsync, O as withConfigLockAsync$1, S as removeProject, _ as loadConfig$1, a as clearDevice, d as getConfigDir, g as isPathPrefix$1, j as acquireAvdClaim, p as getProject } from "./config-Cn94Jc0g.mjs";
|
|
4
|
+
import { a as formatElapsed, h as shortUdid, i as formatDuration, l as phaseLine, p as releasedLeaseFact, u as plural } from "./command-output-Dj-WT4BP.mjs";
|
|
5
|
+
import { c as gitQuiet, l as gitQuietAsync, o as git, r as childEnvironment, s as gitAsync, t as runOperationFile } from "./operation-exec-Dp4FuIGR.mjs";
|
|
6
|
+
import { At as listWorktreesAsync, Ct as hasPopulatedSubmodulesAsync, Et as isPodInstallChurn, Ft as removeWorktreeAsync, Ht as sourceCheckoutOf, Nt as podsOutOfSync, Pt as readWorktreeExclude, Q as resolveSettings, Rt as resolveFullRef, St as gitCommonDirOnDisk, Tt as hasUncommittedWorkAsync, Ut as unpushedCommitsAsync, V as readCommittedSettings, Vt as restoreFileAsync, Wt as warmWorktreePaths, X as resolveProjectSettings, Xt as moveIgnoredTreesToTrash, Y as resolveIosProjectDir, Yt as disposeTrash, Zt as restoreTrashedTrees, _ as inertSettingWarning, _t as depsOutOfSync, a as SETTING_SHAPE_REMEDY, gt as deleteBranchAsync, ht as cloneIgnoredEntries, jt as locallyKnownUpstream, mt as branchExistsAsync, rt as settingShapeErrors, st as unknownSettingKeys, wt as hasRemoteAsync, xt as gitCommonDirAsync, yt as dirtyPathsAsync, zt as resolveFullRefAsync } from "./settings-BDVI8Ivc.mjs";
|
|
7
|
+
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-DYj-HVx6.mjs";
|
|
8
|
+
import { t as macosAppPresent } from "./state-CZxLyetD.mjs";
|
|
9
|
+
import { a as workspaceLinkLine, o as workspaceLinks } from "./stim-desktop-CbWo21kb.mjs";
|
|
10
|
+
import { ft as POOL_SETTING_REMEDY, vt as parkedMaxSetting } from "./android-DriNoAVy.mjs";
|
|
11
|
+
import { c as pidExists, d as resolveProjectMetro, p as signalProcessTreeAsync, r as killMetroTreeAsync } from "./metro-CvyD-xwe.mjs";
|
|
12
|
+
import { A as diskPressureLevel, D as requestDevServerStop, En as runProcessOperation, G as planCacheEmptying, Ln as resolveEasCliBin, N as lowestFree, Ot as endRecordedSession, _ as resolveBudget, g as readVolumeSpaceAsync, h as readVolumeSpace, k as withdrawDevServerStopRequest, p as machineNumber, ur as discoverCaches } from "./ccache-BWTze1Ls.mjs";
|
|
13
|
+
import { o as readWebRecord } from "./state-BX1y5syV.mjs";
|
|
14
|
+
import { D as withManagedTunnelRemovalLock, S as stopTunnel, T as withManagedRemoteWorktreeRemovalLock, _ as withIdleWorkspace, b as resolveSupervisorTarget, dt as teardownOwnedBrowser, ft as teardownOwnedIosSim, g as emptyWorkspaceDir, o as collectWorkspaceOutputs, ut as teardownOwnedAvd } from "./workspaces-DFbj07jp.mjs";
|
|
14
15
|
import { t as canonicalPath } from "./paths-BsJMoPP2.mjs";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import { t as clearNamedPorts } from "./named-ports-BG0j1X4g.mjs";
|
|
16
|
+
import { D as runCaptured, O as runPodInstall, S as DEPS_ERROR, T as readPodState, Y as podInstallCommand, ct as cacheUser, j as HEARTBEAT_INTERVAL_MS, w as podsAreStale } from "./toolchain-CbCG6C5Z.mjs";
|
|
17
|
+
import { D as releaseWorkspaceLeases, t as agentDeviceDaemon } from "./activity-Cd-ysFes.mjs";
|
|
18
|
+
import { R as readHostMemoryPressure, z as readHostMemoryPressureAsync } from "./ios-E26qSpkb.mjs";
|
|
19
|
+
import { t as clearNamedPorts } from "./named-ports-BCyMWA6b.mjs";
|
|
20
20
|
import { t as resolveMaintenanceSettings } from "./settings-Djl-mpjX.mjs";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import { n as
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import { a as
|
|
30
|
-
import { o as pullRequestLookupAsync, r as endedPullRequest } from "./pull-request-CbNlkFok.mjs";
|
|
31
|
-
import { r as workspaceRecordingEnabled } from "./recordings-BldzVKq0.mjs";
|
|
21
|
+
import { i as stopHostedAndroid } from "./hosted-android-BMKzUNnY.mjs";
|
|
22
|
+
import { a as stopHostedIos, n as removePhoneReverses } from "./phone-reverses-C9yAaKEv.mjs";
|
|
23
|
+
import { n as stopMacosApp } from "./stop-CH79S3IZ.mjs";
|
|
24
|
+
import { n as verifyCollectorOwnership } from "./ownership-Bx-0cku-.mjs";
|
|
25
|
+
import { o as pullRequestLookupAsync, r as endedPullRequest } from "./pull-request-BzLmzAIQ.mjs";
|
|
26
|
+
import { c as mergeStateAsync, d as forgetStatusMeasures, t as startWarmProgress } from "./warm-progress-C0RsbngN.mjs";
|
|
27
|
+
import { r as workspaceRecordingEnabled } from "./recordings-C20iUZfp.mjs";
|
|
28
|
+
import { i as installedNpmTreeProblems, n as hasInstalledDependencies, t as dependencyState } from "./dependency-state-C3OtgG94.mjs";
|
|
29
|
+
import { a as discoverXcodeProject } from "./react-native-build-DaK2IFyP.mjs";
|
|
32
30
|
import { claimRemoveCommand, clearFreeClaimSet, readClaimSet, releaseClaim, tryAcquireClaim } from "@stim-cli/core/ownership-claim";
|
|
33
31
|
import { createHash, randomUUID } from "node:crypto";
|
|
34
32
|
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
35
33
|
import { cpus, freemem, homedir, loadavg, totalmem } from "node:os";
|
|
36
34
|
import { basename, dirname, join, relative } from "node:path";
|
|
37
|
-
import { buildCacheRoot, configDir, metroCacheRoot, unitLines, unitWords, withDirLockTransaction, workspaceName } from "@stim-cli/core";
|
|
38
|
-
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,
|
|
35
|
+
import { buildCacheRoot, configDir, metroCacheRoot, quotedPath, unitLines, unitWords, withDirLockTransaction, workspaceName } from "@stim-cli/core";
|
|
36
|
+
import { LAST_BUILD_KEYS, archiveDir, archiveEnabled, archiveLock, archiveRoot, archivedUsage, buildWorkerRoot, countErrorEntries, endedAgentSessionOf, isJsonObject, isPathPrefix, listRecordedDevicesAsync, loadConfig, queryLogs, readArchivesAsync, readBuildHistory, readJsonObject, readLastBuilds, readMacosRecord, readPullRequestCache, readWorkspaceState, recordingEnabled, sharedCcache, sharedCompilationCache, sortByTs, trace, worktreeArchiveRoot } from "@stim-cli/core/state";
|
|
39
37
|
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
38
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
41
|
-
import { captureProcessIdentity } from "@stim-cli/core/process-identity";
|
|
42
39
|
import chalk from "chalk";
|
|
43
40
|
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,
|
|
41
|
+
import { chmod, lstat, mkdir, readdir, rename, rm, rmdir, stat, utimes, writeFile } from "node:fs/promises";
|
|
45
42
|
import { createHash as createHash$1 } from "crypto";
|
|
46
|
-
//#region src/maintenance/pressure.ts
|
|
47
|
-
/**
|
|
48
|
-
* The free-disk floor of each level in MiB. Level 2 has no floor of its own: it runs when level 1 left free disk
|
|
49
|
-
* under the budget floor, and stops once free disk is back above it. Its urgent threshold only sets the reported level.
|
|
50
|
-
*/
|
|
51
|
-
function levelFloors(budget, settings) {
|
|
52
|
-
const floor = Math.max(budget.minFreeDiskMb, budget.hardFloorDiskMb);
|
|
53
|
-
return {
|
|
54
|
-
1: floor,
|
|
55
|
-
2: floor,
|
|
56
|
-
3: Math.max(settings.criticalFreeDiskGb * 1024, budget.hardFloorDiskMb),
|
|
57
|
-
urgent: settings.urgentFreeDiskGb * 1024
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
function diskPressureLevel(freeMb, budget, settings) {
|
|
61
|
-
const floors = levelFloors(budget, settings);
|
|
62
|
-
if (freeMb < floors[3]) return 3;
|
|
63
|
-
if (freeMb < floors.urgent) return 2;
|
|
64
|
-
return freeMb < floors[1] ? 1 : 0;
|
|
65
|
-
}
|
|
66
|
-
function lowestFree(disk) {
|
|
67
|
-
return disk.length ? Math.min(...disk.map((volume) => volume.freeMb)) : Infinity;
|
|
68
|
-
}
|
|
69
|
-
/** Whether an action planned at a pressure level may still run with this much free disk. */
|
|
70
|
-
function levelStillShort(action, freeMb, budget, settings) {
|
|
71
|
-
return action.level === void 0 || freeMb < levelFloors(budget, settings)[action.level];
|
|
72
|
-
}
|
|
73
|
-
/** The protection window in milliseconds: protectRecentHours, or maintenance.pressureProtectMinutes for actions of level 2 and 3. */
|
|
74
|
-
function protectWindowMs(action, settings) {
|
|
75
|
-
return action.level === void 0 ? settings.protectRecentHours * 36e5 : settings.pressureProtectMinutes * 6e4;
|
|
76
|
-
}
|
|
77
|
-
//#endregion
|
|
78
|
-
//#region src/maintenance/measure.ts
|
|
79
|
-
function measureDisk() {
|
|
80
|
-
return readVolumeSpace([
|
|
81
|
-
configDir(),
|
|
82
|
-
...Object.keys(loadConfig()?.projects ?? {}),
|
|
83
|
-
buildWorkerRoot(loadConfig())
|
|
84
|
-
]);
|
|
85
|
-
}
|
|
86
|
-
function measureDiskAsync(operation) {
|
|
87
|
-
return readVolumeSpaceAsync([
|
|
88
|
-
configDir(),
|
|
89
|
-
...Object.keys(loadConfig()?.projects ?? {}),
|
|
90
|
-
buildWorkerRoot(loadConfig())
|
|
91
|
-
], operation.signal);
|
|
92
|
-
}
|
|
93
|
-
/** The disk pressure level of the fullest volume Stim writes to, measured now. */
|
|
94
|
-
function currentDiskLevel(disk = measureDisk()) {
|
|
95
|
-
return diskPressureLevel(lowestFree(disk), resolveBudget().budget, resolveMaintenanceSettings());
|
|
96
|
-
}
|
|
97
|
-
function measurePressure(settings, previous, now) {
|
|
98
|
-
return pressureFromObservations(settings, previous, now, readHostMemoryPressure(), measureDisk());
|
|
99
|
-
}
|
|
100
|
-
async function measurePressureAsync(settings, previous, now, operation) {
|
|
101
|
-
return pressureFromObservations(settings, previous, now, await readHostMemoryPressureAsync(void 0, void 0, operation.signal, operation), await readVolumeSpaceAsync([
|
|
102
|
-
configDir(),
|
|
103
|
-
...Object.keys(loadConfig()?.projects ?? {}),
|
|
104
|
-
buildWorkerRoot(loadConfig())
|
|
105
|
-
], operation.signal));
|
|
106
|
-
}
|
|
107
|
-
function pressureFromObservations(settings, previous, now, level, disk) {
|
|
108
|
-
const availableBytes = process.platform === "darwin" ? null : freemem();
|
|
109
|
-
const warningSince = level === "warning" ? previous?.warningSince ?? now : null;
|
|
110
|
-
return {
|
|
111
|
-
disk,
|
|
112
|
-
memory: {
|
|
113
|
-
level,
|
|
114
|
-
availableBytes,
|
|
115
|
-
pressured: settings.memoryPressureLevel !== "off" && (level === "critical" || settings.memoryPressureLevel === "warning" && warningSince !== null && now - warningSince >= settings.memoryWarningMinutes * 6e4 || availableBytes !== null && availableBytes < (settings.minAvailableMemoryGb === void 0 ? totalmem() * .1 : settings.minAvailableMemoryGb * 1024 ** 3))
|
|
116
|
-
},
|
|
117
|
-
warningSince
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
function sizeScanDeferred(settings) {
|
|
121
|
-
return loadavg()[0] / Math.max(1, cpus().length) > settings.maxLoadPerCore;
|
|
122
|
-
}
|
|
123
|
-
function cacheBlocked(entry, discovered = discoverCaches(), env = process.env) {
|
|
124
|
-
return planCacheEmptying([{
|
|
125
|
-
...entry,
|
|
126
|
-
note: "",
|
|
127
|
-
prune: "entries",
|
|
128
|
-
...discovered.some((cache) => canonicalPath(cache.dir) === canonicalPath(entry.dir) && cache.source === "registered") ? { source: "registered" } : {}
|
|
129
|
-
}], false, env)[0]?.machineGlobal ?? (entry.category === "compilation-cache" ? cacheUser(entry.dir) : void 0);
|
|
130
|
-
}
|
|
131
|
-
async function measureSizes(now, failure) {
|
|
132
|
-
const sizes = [];
|
|
133
|
-
const outputs = await collectWorkspaceOutputs({
|
|
134
|
-
olderThan: null,
|
|
135
|
-
now,
|
|
136
|
-
sizeTimeoutMs: 12e4
|
|
137
|
-
});
|
|
138
|
-
for (const entry of outputs.workspaces) {
|
|
139
|
-
if (entry.bytes === null) {
|
|
140
|
-
failure(entry.dir, entry.projectRoot ?? void 0);
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
sizes.push({
|
|
144
|
-
name: "workspace build outputs",
|
|
145
|
-
category: "workspace-outputs",
|
|
146
|
-
dir: entry.dir,
|
|
147
|
-
bytes: entry.bytes,
|
|
148
|
-
measuredAt: now,
|
|
149
|
-
...entry.projectRoot ? { workspace: canonicalPath(entry.projectRoot) } : {},
|
|
150
|
-
idleDays: entry.idleDays,
|
|
151
|
-
...!entry.willClear ? { blocked: entry.keptReason ?? "workspace ownership is unresolved" } : {}
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
const known = [
|
|
155
|
-
{
|
|
156
|
-
name: "shared build cache",
|
|
157
|
-
dir: buildCacheRoot(),
|
|
158
|
-
category: "build-cache"
|
|
159
|
-
},
|
|
160
|
-
{
|
|
161
|
-
name: "Metro transform caches",
|
|
162
|
-
dir: metroCacheRoot(),
|
|
163
|
-
category: "metro-cache"
|
|
164
|
-
},
|
|
165
|
-
{
|
|
166
|
-
name: "ccache",
|
|
167
|
-
dir: sharedCcache(),
|
|
168
|
-
category: "ccache"
|
|
169
|
-
},
|
|
170
|
-
{
|
|
171
|
-
name: "Swift compilation cache",
|
|
172
|
-
dir: sharedCompilationCache(),
|
|
173
|
-
category: "compilation-cache"
|
|
174
|
-
}
|
|
175
|
-
];
|
|
176
|
-
const discovered = discoverCaches();
|
|
177
|
-
for (const cache of discovered) {
|
|
178
|
-
if (cache.source !== "registered" && !canonicalPath(cache.dir).startsWith(`${canonicalPath(configDir())}/`)) continue;
|
|
179
|
-
if (cache.prune === "report-only") continue;
|
|
180
|
-
const dir = canonicalPath(cache.dir);
|
|
181
|
-
if (known.some((item) => dir === canonicalPath(item.dir) || dir.startsWith(`${canonicalPath(item.dir)}/`))) continue;
|
|
182
|
-
const category = cache.name.startsWith("Metro transform cache") ? "metro-cache" : /^(Expo build cache|native build cache|Build cache)$/.test(cache.name) ? "build-cache" : "other";
|
|
183
|
-
known.push({
|
|
184
|
-
name: cache.name,
|
|
185
|
-
dir: cache.dir,
|
|
186
|
-
category
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
for (const entry of known) {
|
|
190
|
-
if (!existsSync(entry.dir)) continue;
|
|
191
|
-
const bytes = await measuredDirectorySizeAsync(entry.dir, { timeoutMs: 12e4 });
|
|
192
|
-
if (bytes === null) {
|
|
193
|
-
failure(entry.dir);
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
const blocked = cacheBlocked(entry, discovered);
|
|
197
|
-
sizes.push({
|
|
198
|
-
...entry,
|
|
199
|
-
bytes,
|
|
200
|
-
measuredAt: now,
|
|
201
|
-
...blocked ? { blocked } : {}
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
return sizes;
|
|
205
|
-
}
|
|
206
|
-
//#endregion
|
|
207
43
|
//#region src/workspace/pods-relocate.ts
|
|
208
44
|
const CHECKSUMS_HEADER = "SPEC CHECKSUMS:\n";
|
|
209
45
|
const GENERATED_DIRS = [
|
|
@@ -828,6 +664,95 @@ async function removeKind(record, kind) {
|
|
|
828
664
|
function logRemovalOrder(path) {
|
|
829
665
|
return /\.ndjson\.\d+$/.test(path) ? 0 : /^build-.*\.ndjson$/.test(basename(path)) ? 1 : 2;
|
|
830
666
|
}
|
|
667
|
+
/**
|
|
668
|
+
* How long and how large worktree archives of `root` are kept, from `archive.maxAgeDays` and `archive.maxTotalGb`, or
|
|
669
|
+
* why its uncommitted changes cannot be archived: archives are disabled for it or for a project registered inside it,
|
|
670
|
+
* a limit is 0, or a setting is invalid.
|
|
671
|
+
*/
|
|
672
|
+
function worktreeArchiveLimits(root, env = process.env) {
|
|
673
|
+
if (!enabled(root, env)) return { refusal: "archive.enabled is false for it" };
|
|
674
|
+
const nested = Object.keys(loadConfig()?.projects ?? {}).find((key) => key !== root && isPathPrefix(root, key) && !enabled(key, env));
|
|
675
|
+
if (nested) return { refusal: `archive.enabled is false for ${nested}, inside it` };
|
|
676
|
+
let limits;
|
|
677
|
+
try {
|
|
678
|
+
limits = archiveSettings(env);
|
|
679
|
+
} catch (error) {
|
|
680
|
+
return { refusal: (error instanceof Error ? error.message : String(error)).split("\n")[0] ?? "" };
|
|
681
|
+
}
|
|
682
|
+
if (!limits.maxAgeDays) return { refusal: "archive.maxAgeDays is 0, so no archive would be kept" };
|
|
683
|
+
if (!limits.maxTotalGb) return { refusal: "archive.maxTotalGb is 0, so no archive would be kept" };
|
|
684
|
+
return {
|
|
685
|
+
maxAgeDays: limits.maxAgeDays,
|
|
686
|
+
maxBytes: limits.maxTotalGb * 1024 ** 3
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
async function listWorktreeArchives(signal) {
|
|
690
|
+
const root = worktreeArchiveRoot();
|
|
691
|
+
let repositories;
|
|
692
|
+
try {
|
|
693
|
+
repositories = await readdir(root);
|
|
694
|
+
} catch {
|
|
695
|
+
return {
|
|
696
|
+
repositories: [],
|
|
697
|
+
entries: []
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
const entries = [];
|
|
701
|
+
for (const repository of repositories) {
|
|
702
|
+
signal?.throwIfAborted();
|
|
703
|
+
const dir = join(root, repository);
|
|
704
|
+
if (!(await lstat(dir)).isDirectory()) continue;
|
|
705
|
+
for (const name of await readdir(dir)) {
|
|
706
|
+
if (name.endsWith(".claims")) continue;
|
|
707
|
+
const path = join(dir, name);
|
|
708
|
+
const entry = await lstat(path);
|
|
709
|
+
if (!entry.isDirectory()) continue;
|
|
710
|
+
const created = Date.parse(String(readJsonObject(join(path, "manifest.json"))?.createdAt ?? ""));
|
|
711
|
+
const bytes = (await files(path, signal)).reduce((sum, file) => sum + file.bytes, 0);
|
|
712
|
+
entries.push({
|
|
713
|
+
path,
|
|
714
|
+
at: Number.isFinite(created) ? created : entry.mtimeMs,
|
|
715
|
+
bytes
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return {
|
|
720
|
+
repositories,
|
|
721
|
+
entries
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
/** Bytes every worktree archive holds, the one being written included. */
|
|
725
|
+
async function worktreeArchiveBytes(signal) {
|
|
726
|
+
return (await listWorktreeArchives(signal)).entries.reduce((sum, entry) => sum + entry.bytes, 0);
|
|
727
|
+
}
|
|
728
|
+
async function pruneWorktreeArchives(now, limits, signal) {
|
|
729
|
+
const listed = await listWorktreeArchives(signal);
|
|
730
|
+
const { repositories } = listed;
|
|
731
|
+
const entries = listed.entries.filter((entry) => {
|
|
732
|
+
const claims = readClaimSet(`${entry.path}.claims`);
|
|
733
|
+
return !claims.live.length && !claims.unresolved.length;
|
|
734
|
+
});
|
|
735
|
+
entries.sort((a, b) => a.at - b.at);
|
|
736
|
+
let total = entries.reduce((sum, entry) => sum + entry.bytes, 0);
|
|
737
|
+
const cap = (limits.maxTotalGb ?? 0) * 1024 ** 3;
|
|
738
|
+
const maxAgeMs = (limits.maxAgeDays ?? 0) * DAY;
|
|
739
|
+
for (const entry of entries) {
|
|
740
|
+
signal?.throwIfAborted();
|
|
741
|
+
if (now - entry.at < maxAgeMs && total <= cap) continue;
|
|
742
|
+
if (clearFreeClaimSet({
|
|
743
|
+
root: `${entry.path}.claims`,
|
|
744
|
+
label: "worktree archive"
|
|
745
|
+
}).status !== "cleared") continue;
|
|
746
|
+
await rm(entry.path, {
|
|
747
|
+
recursive: true,
|
|
748
|
+
force: true
|
|
749
|
+
});
|
|
750
|
+
total -= entry.bytes;
|
|
751
|
+
}
|
|
752
|
+
for (const repository of repositories) try {
|
|
753
|
+
await rmdir(join(worktreeArchiveRoot(), repository));
|
|
754
|
+
} catch {}
|
|
755
|
+
}
|
|
831
756
|
async function enforceArchiveRetention(now = Date.now(), operation) {
|
|
832
757
|
const signal = operation?.signal;
|
|
833
758
|
const env = operation?.env ?? process.env;
|
|
@@ -901,6 +826,7 @@ async function enforceArchiveRetention(now = Date.now(), operation) {
|
|
|
901
826
|
record.expires.record = new Date(Date.parse(record.removedAt) + limits.maxAgeDays * DAY).toISOString();
|
|
902
827
|
if (JSON.stringify(record.expires) !== before) await writeRecord(archiveDir(record.id), record);
|
|
903
828
|
}
|
|
829
|
+
await pruneWorktreeArchives(now, limits, signal);
|
|
904
830
|
}, {
|
|
905
831
|
waitMs: 3e4,
|
|
906
832
|
signal
|
|
@@ -1456,7 +1382,7 @@ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwned
|
|
|
1456
1382
|
//#region src/workspace/service-guard.ts
|
|
1457
1383
|
const SERVICE_IN_USE = { code: "STIM_WORKTREE_SERVICE" };
|
|
1458
1384
|
function inside(folder, canonical) {
|
|
1459
|
-
return (value) => typeof value === "string" && value.startsWith("/") && isPathPrefix(folder, canonical(value));
|
|
1385
|
+
return (value) => typeof value === "string" && value.startsWith("/") && isPathPrefix$1(folder, canonical(value));
|
|
1460
1386
|
}
|
|
1461
1387
|
function serviceUseOf(plist, file, folder, canonical) {
|
|
1462
1388
|
if (typeof plist !== "object" || plist === null) return null;
|
|
@@ -1535,37 +1461,561 @@ function serviceRunningFrom(folder, canonical, agentsDir = join(homedir(), "Libr
|
|
|
1535
1461
|
argument: null
|
|
1536
1462
|
};
|
|
1537
1463
|
}
|
|
1538
|
-
for (const name of names) {
|
|
1539
|
-
const file = join(agentsDir, name);
|
|
1540
|
-
const label = name.replace(/\.plist$/, "");
|
|
1541
|
-
try {
|
|
1542
|
-
if (!readFileSync(file).includes("StimService")) continue;
|
|
1543
|
-
} catch {
|
|
1544
|
-
return {
|
|
1545
|
-
label,
|
|
1546
|
-
plist: file,
|
|
1547
|
-
argument: null
|
|
1548
|
-
};
|
|
1464
|
+
for (const name of names) {
|
|
1465
|
+
const file = join(agentsDir, name);
|
|
1466
|
+
const label = name.replace(/\.plist$/, "");
|
|
1467
|
+
try {
|
|
1468
|
+
if (!readFileSync(file).includes("StimService")) continue;
|
|
1469
|
+
} catch {
|
|
1470
|
+
return {
|
|
1471
|
+
label,
|
|
1472
|
+
plist: file,
|
|
1473
|
+
argument: null
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
try {
|
|
1477
|
+
const json = getExecutor().runFile("plutil", [
|
|
1478
|
+
"-convert",
|
|
1479
|
+
"json",
|
|
1480
|
+
"-o",
|
|
1481
|
+
"-",
|
|
1482
|
+
file
|
|
1483
|
+
], { timeoutMs: 5e3 });
|
|
1484
|
+
const use = serviceUseOf(JSON.parse(json), file, folder, canonical);
|
|
1485
|
+
if (use) return use;
|
|
1486
|
+
} catch {
|
|
1487
|
+
return {
|
|
1488
|
+
label,
|
|
1489
|
+
plist: file,
|
|
1490
|
+
argument: null
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
return null;
|
|
1495
|
+
}
|
|
1496
|
+
/** The Stim CLI entry points removal must not delete: the running one and the `stim` on PATH. */
|
|
1497
|
+
function stimCliEntries(env) {
|
|
1498
|
+
const entries = [];
|
|
1499
|
+
if (process.argv[1]) entries.push({
|
|
1500
|
+
label: "the running stim",
|
|
1501
|
+
path: process.argv[1]
|
|
1502
|
+
});
|
|
1503
|
+
const onPath = getExecutor().findExecutable("stim", env ? { env } : void 0);
|
|
1504
|
+
if (onPath) entries.push({
|
|
1505
|
+
label: "the stim on PATH",
|
|
1506
|
+
path: onPath
|
|
1507
|
+
});
|
|
1508
|
+
return entries;
|
|
1509
|
+
}
|
|
1510
|
+
/** Why removing `folder` would delete a Stim CLI in `entries`, resolved through symlinks; empty when it would not. */
|
|
1511
|
+
function stimCliUseOf(folder, canonical, entries) {
|
|
1512
|
+
return entries.flatMap(({ label, path }) => {
|
|
1513
|
+
const resolved = canonical(path);
|
|
1514
|
+
return isPathPrefix$1(folder, resolved) ? [`it holds ${label} (${resolved})`] : [];
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
//#endregion
|
|
1518
|
+
//#region src/workspace/worktree-archive.ts
|
|
1519
|
+
const WATCHMAN_COOKIE = /(?:^|\/)\.watchman-cookie-[^/]*$/;
|
|
1520
|
+
const ARCHIVE_TIMEOUT_MS = 6e5;
|
|
1521
|
+
function execution(operation) {
|
|
1522
|
+
return {
|
|
1523
|
+
...operation ? childEnvironment(operation.env) : {},
|
|
1524
|
+
signal: operation?.signal,
|
|
1525
|
+
declareFor: operation,
|
|
1526
|
+
killSignal: "SIGKILL"
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
function nulList(text) {
|
|
1530
|
+
return text.split("\0").filter(Boolean);
|
|
1531
|
+
}
|
|
1532
|
+
async function changesOf(path, operation) {
|
|
1533
|
+
const read = {
|
|
1534
|
+
...execution(operation),
|
|
1535
|
+
untrimmed: true,
|
|
1536
|
+
timeoutMs: 6e4
|
|
1537
|
+
};
|
|
1538
|
+
const tracked = nulList(await gitAsync(path, [
|
|
1539
|
+
"diff",
|
|
1540
|
+
"--name-only",
|
|
1541
|
+
"--no-renames",
|
|
1542
|
+
"-z",
|
|
1543
|
+
"HEAD",
|
|
1544
|
+
"--"
|
|
1545
|
+
], read));
|
|
1546
|
+
const untracked = nulList(await gitAsync(path, [
|
|
1547
|
+
"ls-files",
|
|
1548
|
+
"-z",
|
|
1549
|
+
"--others",
|
|
1550
|
+
"--exclude-standard",
|
|
1551
|
+
"--"
|
|
1552
|
+
], read)).filter((file) => !WATCHMAN_COOKIE.test(file));
|
|
1553
|
+
return {
|
|
1554
|
+
tracked: tracked.toSorted(),
|
|
1555
|
+
untracked: untracked.toSorted()
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* Paths whose staged content differs from the working tree. `git diff HEAD` saves only the working tree, so a staged
|
|
1560
|
+
* version that the working tree no longer holds would be lost.
|
|
1561
|
+
*/
|
|
1562
|
+
async function stagedOnly(path, operation) {
|
|
1563
|
+
const read = {
|
|
1564
|
+
...execution(operation),
|
|
1565
|
+
untrimmed: true,
|
|
1566
|
+
timeoutMs: 6e4
|
|
1567
|
+
};
|
|
1568
|
+
const staged = new Set(nulList(await gitAsync(path, [
|
|
1569
|
+
"diff",
|
|
1570
|
+
"--cached",
|
|
1571
|
+
"--name-only",
|
|
1572
|
+
"--no-renames",
|
|
1573
|
+
"-z",
|
|
1574
|
+
"--"
|
|
1575
|
+
], read)));
|
|
1576
|
+
return nulList(await gitAsync(path, [
|
|
1577
|
+
"diff",
|
|
1578
|
+
"--name-only",
|
|
1579
|
+
"--no-renames",
|
|
1580
|
+
"-z",
|
|
1581
|
+
"--"
|
|
1582
|
+
], read)).filter((file) => staged.has(file));
|
|
1583
|
+
}
|
|
1584
|
+
async function headSizes(path, files, operation) {
|
|
1585
|
+
if (!files.length) return 0;
|
|
1586
|
+
if (files.some((file) => file.includes("\n"))) throw new Error("a changed path contains a newline");
|
|
1587
|
+
return (await gitAsync(path, ["cat-file", "--batch-check=%(objectsize)"], {
|
|
1588
|
+
...execution(operation),
|
|
1589
|
+
timeoutMs: 6e4,
|
|
1590
|
+
input: `${files.map((file) => `HEAD:${file}`).join("\n")}\n`
|
|
1591
|
+
})).split("\n").reduce((sum, line) => sum + (/^\d+$/.test(line.trim()) ? Number(line.trim()) : 0), 0);
|
|
1592
|
+
}
|
|
1593
|
+
async function changedAt(file, deleted) {
|
|
1594
|
+
for (let current = file;; current = dirname(current)) try {
|
|
1595
|
+
const { mtimeMs, ctimeMs } = await lstat(current);
|
|
1596
|
+
return Math.max(mtimeMs, ctimeMs);
|
|
1597
|
+
} catch (error) {
|
|
1598
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
|
|
1599
|
+
if (deleted === "skip" || dirname(current) === current) return null;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
/** With `parent`, a deleted file counts its nearest existing parent directory's change, since unlinking changes it. */
|
|
1603
|
+
async function newestChange(path, changes, deleted = "skip") {
|
|
1604
|
+
let newest = null;
|
|
1605
|
+
for (const file of [...changes.tracked, ...changes.untracked]) {
|
|
1606
|
+
const changed = await changedAt(join(path, file), deleted);
|
|
1607
|
+
if (changed !== null && (newest === null || changed > newest)) newest = changed;
|
|
1608
|
+
}
|
|
1609
|
+
return newest;
|
|
1610
|
+
}
|
|
1611
|
+
/**
|
|
1612
|
+
* Plans a bundle of HEAD's commits that the default branch lacks, all of HEAD's history without `origin/HEAD`, so the
|
|
1613
|
+
* restore can recreate HEAD after its branch is deleted and git collects the commits; null when the default branch
|
|
1614
|
+
* already contains HEAD. `bytes` is their size on disk.
|
|
1615
|
+
*/
|
|
1616
|
+
async function headBundlePlan(path, head, operation) {
|
|
1617
|
+
const run = {
|
|
1618
|
+
...execution(operation),
|
|
1619
|
+
timeoutMs: ARCHIVE_TIMEOUT_MS
|
|
1620
|
+
};
|
|
1621
|
+
const base = (await gitQuietAsync(path, [
|
|
1622
|
+
"symbolic-ref",
|
|
1623
|
+
"--quiet",
|
|
1624
|
+
"refs/remotes/origin/HEAD"
|
|
1625
|
+
], run))?.trim();
|
|
1626
|
+
if (base && await gitQuietAsync(path, [
|
|
1627
|
+
"merge-base",
|
|
1628
|
+
"--is-ancestor",
|
|
1629
|
+
head,
|
|
1630
|
+
base
|
|
1631
|
+
], run) !== null) return null;
|
|
1632
|
+
const exclude = base ? [`^${base}`] : [];
|
|
1633
|
+
const bytes = Number(await gitAsync(path, [
|
|
1634
|
+
"rev-list",
|
|
1635
|
+
"--disk-usage",
|
|
1636
|
+
"--objects",
|
|
1637
|
+
"HEAD",
|
|
1638
|
+
...exclude
|
|
1639
|
+
], run));
|
|
1640
|
+
if (!Number.isFinite(bytes)) throw new Error("git did not report the size of the commits to bundle");
|
|
1641
|
+
return {
|
|
1642
|
+
exclude,
|
|
1643
|
+
bytes
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
async function bundleHead(path, plan, dir, operation) {
|
|
1647
|
+
const run = {
|
|
1648
|
+
...execution(operation),
|
|
1649
|
+
timeoutMs: ARCHIVE_TIMEOUT_MS
|
|
1650
|
+
};
|
|
1651
|
+
const bundle = join(dir, "head.bundle");
|
|
1652
|
+
await gitAsync(path, [
|
|
1653
|
+
"bundle",
|
|
1654
|
+
"create",
|
|
1655
|
+
bundle,
|
|
1656
|
+
"HEAD",
|
|
1657
|
+
...plan.exclude
|
|
1658
|
+
], run);
|
|
1659
|
+
await chmod(bundle, 384);
|
|
1660
|
+
await gitAsync(path, [
|
|
1661
|
+
"bundle",
|
|
1662
|
+
"verify",
|
|
1663
|
+
bundle
|
|
1664
|
+
], run);
|
|
1665
|
+
return bundle;
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* When a worktree's uncommitted or untracked, non-ignored files last changed, a deleted file by its nearest existing
|
|
1669
|
+
* parent directory: null when it has none, and `unreadable` when git or the files cannot be read. git records no time
|
|
1670
|
+
* for these edits, so the idle checks read the files themselves.
|
|
1671
|
+
*/
|
|
1672
|
+
async function uncommittedActivityOf(path, operation) {
|
|
1673
|
+
try {
|
|
1674
|
+
const at = await newestChange(path, await changesOf(path, operation), "parent");
|
|
1675
|
+
return at === null ? null : {
|
|
1676
|
+
at,
|
|
1677
|
+
basis: "an uncommitted file change"
|
|
1678
|
+
};
|
|
1679
|
+
} catch (error) {
|
|
1680
|
+
operation?.signal.throwIfAborted();
|
|
1681
|
+
if (error instanceof Error && "reportCleanup" in error) throw error;
|
|
1682
|
+
return "unreadable";
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
function sameList(a, b) {
|
|
1686
|
+
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
1687
|
+
}
|
|
1688
|
+
function safeName(value) {
|
|
1689
|
+
return value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[.-]+/, "") || "worktree";
|
|
1690
|
+
}
|
|
1691
|
+
function stamp(at) {
|
|
1692
|
+
return new Date(at).toISOString().replace(/[-:]/g, "").replace(".", "-");
|
|
1693
|
+
}
|
|
1694
|
+
/**
|
|
1695
|
+
* Why the archive no longer describes the worktree, or null when it still does: the same changed and untracked files,
|
|
1696
|
+
* none modified since the archive started, and the patch still reverses cleanly against the working tree.
|
|
1697
|
+
*/
|
|
1698
|
+
async function worktreeArchiveMismatch(path, archive, operation) {
|
|
1699
|
+
if (archive.tracked.length) try {
|
|
1700
|
+
await gitAsync(path, [
|
|
1701
|
+
"apply",
|
|
1702
|
+
"--check",
|
|
1703
|
+
"-R",
|
|
1704
|
+
join(archive.dir, "tracked.patch")
|
|
1705
|
+
], {
|
|
1706
|
+
...execution(operation),
|
|
1707
|
+
timeoutMs: ARCHIVE_TIMEOUT_MS
|
|
1708
|
+
});
|
|
1709
|
+
} catch (error) {
|
|
1710
|
+
if (error instanceof Error && "reportCleanup" in error) throw error;
|
|
1711
|
+
operation?.signal.throwIfAborted();
|
|
1712
|
+
return `the archived patch does not reverse against the worktree: ${(error instanceof Error ? error.message : String(error)).split("\n")[0]}`;
|
|
1713
|
+
}
|
|
1714
|
+
const staged = await stagedOnly(path, operation);
|
|
1715
|
+
if (staged.length) return `the staged content of ${staged.join(", ")} differs from its working tree`;
|
|
1716
|
+
const now = await changesOf(path, operation);
|
|
1717
|
+
if (!sameList(now.tracked, archive.tracked) || !sameList(now.untracked, archive.untracked)) return "its uncommitted or untracked files changed while they were archived";
|
|
1718
|
+
const newest = await newestChange(path, now);
|
|
1719
|
+
if (newest !== null && newest >= archive.at) return "an uncommitted file changed while it was archived";
|
|
1720
|
+
for (const file of [
|
|
1721
|
+
...archive.bundled ? ["head.bundle"] : [],
|
|
1722
|
+
...archive.tracked.length ? ["tracked.patch"] : [],
|
|
1723
|
+
...archive.untracked.length ? ["untracked.tar"] : []
|
|
1724
|
+
]) if (!await lstat(join(archive.dir, file)).catch(() => null)) return `its archive lost ${file}`;
|
|
1725
|
+
return null;
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* Saves a worktree's uncommitted work under `$STIM_HOME/archive/worktrees/<repository>/<branch-or-dir>-<timestamp>/`:
|
|
1729
|
+
* `tracked.patch` (`git diff --binary HEAD`, staged and unstaged changes together), `untracked.tar` (untracked files
|
|
1730
|
+
* git does not ignore) and `manifest.json` naming the branch, HEAD, pull request, files and restore commands. Ignored
|
|
1731
|
+
* files are not saved. Throws, leaving no archive, when archives are disabled or limited to 0, the files exceed
|
|
1732
|
+
* `archive.maxTotalGb` or the disk floor, an untracked nested repository is present, a staged version differs from
|
|
1733
|
+
* the working tree, or the written archive does not verify.
|
|
1734
|
+
*/
|
|
1735
|
+
async function archiveWorktreeChanges({ path, repository, branch, head, pullRequest, operation }) {
|
|
1736
|
+
const limits = worktreeArchiveLimits(path, operation?.env);
|
|
1737
|
+
if ("refusal" in limits) throw new Error(limits.refusal);
|
|
1738
|
+
const at = Date.now();
|
|
1739
|
+
const changes = await changesOf(path, operation);
|
|
1740
|
+
const nested = changes.untracked.find((file) => file.endsWith("/"));
|
|
1741
|
+
if (nested) throw new Error(`${nested} is an untracked nested repository`);
|
|
1742
|
+
const staged = await stagedOnly(path, operation);
|
|
1743
|
+
if (staged.length) throw new Error(`the staged content of ${staged.join(", ")} differs from its working tree`);
|
|
1744
|
+
if (!changes.tracked.length && !changes.untracked.length) throw new Error("git reported no change it could save");
|
|
1745
|
+
const retention = async () => {
|
|
1746
|
+
try {
|
|
1747
|
+
await enforceArchiveRetention(void 0, operation);
|
|
1748
|
+
} catch (error) {
|
|
1749
|
+
if (error instanceof Error && "reportCleanup" in error) throw error;
|
|
1750
|
+
operation?.signal.throwIfAborted();
|
|
1751
|
+
}
|
|
1752
|
+
};
|
|
1753
|
+
await retention();
|
|
1754
|
+
const existing = await worktreeArchiveBytes(operation?.signal);
|
|
1755
|
+
let bytes = 0;
|
|
1756
|
+
for (const file of changes.untracked) bytes += (await lstat(join(path, file))).size;
|
|
1757
|
+
let tracked = await headSizes(path, changes.tracked, operation);
|
|
1758
|
+
for (const file of changes.tracked) tracked += (await lstat(join(path, file)).catch(() => null))?.size ?? 0;
|
|
1759
|
+
const plan = await headBundlePlan(path, head, operation);
|
|
1760
|
+
if (existing + bytes + (plan?.bytes ?? 0) > limits.maxBytes) throw new Error(`its untracked files and unmerged commits (${bytes + (plan?.bytes ?? 0)} bytes) do not fit in archive.maxTotalGb beside the other archives`);
|
|
1761
|
+
const [volume] = await readVolumeSpaceAsync([worktreeArchiveRoot()], operation?.signal);
|
|
1762
|
+
const floorMb = resolveBudget({ env: operation?.env }).budget.hardFloorDiskMb;
|
|
1763
|
+
const neededMb = (bytes + 2 * tracked + 2 * (plan?.bytes ?? 0)) / 1024 ** 2;
|
|
1764
|
+
if (!volume || volume.freeMb - neededMb < floorMb) throw new Error(volume ? `writing it would leave less free disk than budget.hardFloorDiskGb on ${volume.volume}` : "free disk for it could not be read");
|
|
1765
|
+
const dir = join(worktreeArchiveRoot(), safeName(basename(repository)), `${safeName(branch?.replace(/^refs\/heads\//, "") ?? basename(path))}-${stamp(at)}-${randomUUID().slice(0, 8)}`);
|
|
1766
|
+
await mkdir(dirname(dir), {
|
|
1767
|
+
recursive: true,
|
|
1768
|
+
mode: 448
|
|
1769
|
+
});
|
|
1770
|
+
const attempt = tryAcquireClaim({
|
|
1771
|
+
root: `${dir}.claims`,
|
|
1772
|
+
mode: "exclusive",
|
|
1773
|
+
label: "worktree archive"
|
|
1774
|
+
});
|
|
1775
|
+
const claim = attempt.acquired;
|
|
1776
|
+
if (!claim) {
|
|
1777
|
+
releaseClaim(attempt.pending);
|
|
1778
|
+
throw new Error("its archive claim is held");
|
|
1779
|
+
}
|
|
1780
|
+
let created = false;
|
|
1781
|
+
const restore = [`git -C ${quotedPath(repository)} worktree add --detach ${quotedPath(path)} ${head}`];
|
|
1782
|
+
try {
|
|
1783
|
+
await mkdir(dir, { mode: 448 });
|
|
1784
|
+
created = true;
|
|
1785
|
+
const bundle = plan ? await bundleHead(path, plan, dir, operation) : null;
|
|
1786
|
+
if (bundle) restore.unshift(`git -C ${quotedPath(repository)} bundle unbundle ${quotedPath(bundle)}`);
|
|
1787
|
+
if (changes.tracked.length) {
|
|
1788
|
+
const patch = join(dir, "tracked.patch");
|
|
1789
|
+
await gitAsync(path, [
|
|
1790
|
+
"diff",
|
|
1791
|
+
"--binary",
|
|
1792
|
+
"--no-color",
|
|
1793
|
+
"--no-ext-diff",
|
|
1794
|
+
"--no-textconv",
|
|
1795
|
+
"--no-relative",
|
|
1796
|
+
"--src-prefix=a/",
|
|
1797
|
+
"--dst-prefix=b/",
|
|
1798
|
+
`--output=${patch}`,
|
|
1799
|
+
"HEAD",
|
|
1800
|
+
"--"
|
|
1801
|
+
], {
|
|
1802
|
+
...execution(operation),
|
|
1803
|
+
timeoutMs: ARCHIVE_TIMEOUT_MS
|
|
1804
|
+
});
|
|
1805
|
+
await chmod(patch, 384);
|
|
1806
|
+
restore.push(`git -C ${quotedPath(path)} apply ${quotedPath(patch)}`);
|
|
1807
|
+
}
|
|
1808
|
+
if (changes.untracked.length) {
|
|
1809
|
+
const tarball = join(dir, "untracked.tar");
|
|
1810
|
+
const exec = {
|
|
1811
|
+
...execution(operation),
|
|
1812
|
+
cwd: path,
|
|
1813
|
+
timeoutMs: ARCHIVE_TIMEOUT_MS
|
|
1814
|
+
};
|
|
1815
|
+
await getExecutor().runFileAsync("tar", [
|
|
1816
|
+
"-cf",
|
|
1817
|
+
tarball,
|
|
1818
|
+
"--null",
|
|
1819
|
+
"-T",
|
|
1820
|
+
"-"
|
|
1821
|
+
], {
|
|
1822
|
+
...exec,
|
|
1823
|
+
input: `${changes.untracked.join("\0")}\0`
|
|
1824
|
+
});
|
|
1825
|
+
await chmod(tarball, 384);
|
|
1826
|
+
if (!sameList((await getExecutor().runFileAsync("tar", ["-tf", tarball], {
|
|
1827
|
+
...exec,
|
|
1828
|
+
untrimmed: true
|
|
1829
|
+
})).split(/\r?\n/).filter(Boolean).toSorted(), changes.untracked)) throw new Error("the untracked tarball does not list every file");
|
|
1830
|
+
restore.push(`tar -xf ${quotedPath(tarball)} -C ${quotedPath(path)}`);
|
|
1831
|
+
}
|
|
1832
|
+
const archive = {
|
|
1833
|
+
dir,
|
|
1834
|
+
at,
|
|
1835
|
+
...changes,
|
|
1836
|
+
restore,
|
|
1837
|
+
bundled: bundle !== null,
|
|
1838
|
+
claim
|
|
1839
|
+
};
|
|
1840
|
+
const mismatch = await worktreeArchiveMismatch(path, archive, operation);
|
|
1841
|
+
if (mismatch) throw new Error(mismatch);
|
|
1842
|
+
const manifest = {
|
|
1843
|
+
version: 1,
|
|
1844
|
+
createdAt: new Date(at).toISOString(),
|
|
1845
|
+
worktree: path,
|
|
1846
|
+
repository,
|
|
1847
|
+
branch,
|
|
1848
|
+
head,
|
|
1849
|
+
pullRequest,
|
|
1850
|
+
headBundle: bundle ? "head.bundle" : null,
|
|
1851
|
+
trackedPatch: changes.tracked.length ? "tracked.patch" : null,
|
|
1852
|
+
untrackedTarball: changes.untracked.length ? "untracked.tar" : null,
|
|
1853
|
+
tracked: changes.tracked,
|
|
1854
|
+
untracked: changes.untracked,
|
|
1855
|
+
restore
|
|
1856
|
+
};
|
|
1857
|
+
const tmp = join(dir, ".manifest.json.tmp");
|
|
1858
|
+
await writeFile(tmp, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 384 });
|
|
1859
|
+
await rename(tmp, join(dir, "manifest.json"));
|
|
1860
|
+
const total = await worktreeArchiveBytes(operation?.signal);
|
|
1861
|
+
if (total > limits.maxBytes) throw new Error(`the archive (${total - existing} bytes) does not fit in archive.maxTotalGb beside the other archives`);
|
|
1862
|
+
const [after] = await readVolumeSpaceAsync([dir], operation?.signal);
|
|
1863
|
+
if (!after || after.freeMb < floorMb) throw new Error(`it left less free disk than budget.hardFloorDiskGb on ${after?.volume ?? "its volume"}`);
|
|
1864
|
+
await retention();
|
|
1865
|
+
if (!await lstat(join(dir, "manifest.json")).catch(() => null)) throw new Error("archive retention removed it");
|
|
1866
|
+
return archive;
|
|
1867
|
+
} catch (error) {
|
|
1868
|
+
try {
|
|
1869
|
+
if (created) await rm(dir, {
|
|
1870
|
+
recursive: true,
|
|
1871
|
+
force: true
|
|
1872
|
+
});
|
|
1873
|
+
} finally {
|
|
1874
|
+
releaseClaim(claim);
|
|
1875
|
+
}
|
|
1876
|
+
throw error;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
/** Deletes an archive whose worktree was kept intact, so a retried removal does not pile up copies. */
|
|
1880
|
+
async function discardWorktreeArchive(archive) {
|
|
1881
|
+
await rm(archive.dir, {
|
|
1882
|
+
recursive: true,
|
|
1883
|
+
force: true
|
|
1884
|
+
});
|
|
1885
|
+
releaseWorktreeArchive(archive);
|
|
1886
|
+
}
|
|
1887
|
+
/** Lets retention manage the archive once the removal that needed it has ended. */
|
|
1888
|
+
function releaseWorktreeArchive(archive) {
|
|
1889
|
+
releaseClaim(archive.claim);
|
|
1890
|
+
}
|
|
1891
|
+
//#endregion
|
|
1892
|
+
//#region src/maintenance/measure.ts
|
|
1893
|
+
function measureDisk() {
|
|
1894
|
+
return readVolumeSpace([
|
|
1895
|
+
configDir(),
|
|
1896
|
+
...Object.keys(loadConfig()?.projects ?? {}),
|
|
1897
|
+
buildWorkerRoot(loadConfig())
|
|
1898
|
+
]);
|
|
1899
|
+
}
|
|
1900
|
+
function measureDiskAsync(operation) {
|
|
1901
|
+
return readVolumeSpaceAsync([
|
|
1902
|
+
configDir(),
|
|
1903
|
+
...Object.keys(loadConfig()?.projects ?? {}),
|
|
1904
|
+
buildWorkerRoot(loadConfig())
|
|
1905
|
+
], operation.signal);
|
|
1906
|
+
}
|
|
1907
|
+
/** The disk pressure level of the fullest volume Stim writes to, measured now. */
|
|
1908
|
+
function currentDiskLevel(disk = measureDisk()) {
|
|
1909
|
+
return diskPressureLevel(lowestFree(disk), resolveBudget().budget, resolveMaintenanceSettings());
|
|
1910
|
+
}
|
|
1911
|
+
function measurePressure(settings, previous, now) {
|
|
1912
|
+
return pressureFromObservations(settings, previous, now, readHostMemoryPressure(), measureDisk());
|
|
1913
|
+
}
|
|
1914
|
+
async function measurePressureAsync(settings, previous, now, operation) {
|
|
1915
|
+
return pressureFromObservations(settings, previous, now, await readHostMemoryPressureAsync(void 0, void 0, operation.signal, operation), await readVolumeSpaceAsync([
|
|
1916
|
+
configDir(),
|
|
1917
|
+
...Object.keys(loadConfig()?.projects ?? {}),
|
|
1918
|
+
buildWorkerRoot(loadConfig())
|
|
1919
|
+
], operation.signal));
|
|
1920
|
+
}
|
|
1921
|
+
function pressureFromObservations(settings, previous, now, level, disk) {
|
|
1922
|
+
const availableBytes = process.platform === "darwin" ? null : freemem();
|
|
1923
|
+
const warningSince = level === "warning" ? previous?.warningSince ?? now : null;
|
|
1924
|
+
return {
|
|
1925
|
+
disk,
|
|
1926
|
+
memory: {
|
|
1927
|
+
level,
|
|
1928
|
+
availableBytes,
|
|
1929
|
+
pressured: settings.memoryPressureLevel !== "off" && (level === "critical" || settings.memoryPressureLevel === "warning" && warningSince !== null && now - warningSince >= settings.memoryWarningMinutes * 6e4 || availableBytes !== null && availableBytes < (settings.minAvailableMemoryGb === void 0 ? totalmem() * .1 : settings.minAvailableMemoryGb * 1024 ** 3))
|
|
1930
|
+
},
|
|
1931
|
+
warningSince
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
1934
|
+
function sizeScanDeferred(settings) {
|
|
1935
|
+
return loadavg()[0] / Math.max(1, cpus().length) > settings.maxLoadPerCore;
|
|
1936
|
+
}
|
|
1937
|
+
function cacheBlocked(entry, discovered = discoverCaches(), env = process.env) {
|
|
1938
|
+
return planCacheEmptying([{
|
|
1939
|
+
...entry,
|
|
1940
|
+
note: "",
|
|
1941
|
+
prune: "entries",
|
|
1942
|
+
...discovered.some((cache) => canonicalPath(cache.dir) === canonicalPath(entry.dir) && cache.source === "registered") ? { source: "registered" } : {}
|
|
1943
|
+
}], false, env)[0]?.machineGlobal ?? (entry.category === "compilation-cache" ? cacheUser(entry.dir) : void 0);
|
|
1944
|
+
}
|
|
1945
|
+
async function measureSizes(now, failure) {
|
|
1946
|
+
const sizes = [];
|
|
1947
|
+
const outputs = await collectWorkspaceOutputs({
|
|
1948
|
+
olderThan: null,
|
|
1949
|
+
now,
|
|
1950
|
+
sizeTimeoutMs: 12e4
|
|
1951
|
+
});
|
|
1952
|
+
for (const entry of outputs.workspaces) {
|
|
1953
|
+
if (entry.bytes === null) {
|
|
1954
|
+
failure(entry.dir, entry.projectRoot ?? void 0);
|
|
1955
|
+
continue;
|
|
1956
|
+
}
|
|
1957
|
+
sizes.push({
|
|
1958
|
+
name: "workspace build outputs",
|
|
1959
|
+
category: "workspace-outputs",
|
|
1960
|
+
dir: entry.dir,
|
|
1961
|
+
bytes: entry.bytes,
|
|
1962
|
+
measuredAt: now,
|
|
1963
|
+
...entry.projectRoot ? { workspace: canonicalPath(entry.projectRoot) } : {},
|
|
1964
|
+
idleDays: entry.idleDays,
|
|
1965
|
+
...!entry.willClear ? { blocked: entry.keptReason ?? "workspace ownership is unresolved" } : {}
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
const known = [
|
|
1969
|
+
{
|
|
1970
|
+
name: "shared build cache",
|
|
1971
|
+
dir: buildCacheRoot(),
|
|
1972
|
+
category: "build-cache"
|
|
1973
|
+
},
|
|
1974
|
+
{
|
|
1975
|
+
name: "Metro transform caches",
|
|
1976
|
+
dir: metroCacheRoot(),
|
|
1977
|
+
category: "metro-cache"
|
|
1978
|
+
},
|
|
1979
|
+
{
|
|
1980
|
+
name: "ccache",
|
|
1981
|
+
dir: sharedCcache(),
|
|
1982
|
+
category: "ccache"
|
|
1983
|
+
},
|
|
1984
|
+
{
|
|
1985
|
+
name: "Swift compilation cache",
|
|
1986
|
+
dir: sharedCompilationCache(),
|
|
1987
|
+
category: "compilation-cache"
|
|
1549
1988
|
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1989
|
+
];
|
|
1990
|
+
const discovered = discoverCaches();
|
|
1991
|
+
for (const cache of discovered) {
|
|
1992
|
+
if (cache.source !== "registered" && !canonicalPath(cache.dir).startsWith(`${canonicalPath(configDir())}/`)) continue;
|
|
1993
|
+
if (cache.prune === "report-only") continue;
|
|
1994
|
+
const dir = canonicalPath(cache.dir);
|
|
1995
|
+
if (known.some((item) => dir === canonicalPath(item.dir) || dir.startsWith(`${canonicalPath(item.dir)}/`))) continue;
|
|
1996
|
+
const category = cache.name.startsWith("Metro transform cache") ? "metro-cache" : /^(Expo build cache|native build cache|Build cache)$/.test(cache.name) ? "build-cache" : "other";
|
|
1997
|
+
known.push({
|
|
1998
|
+
name: cache.name,
|
|
1999
|
+
dir: cache.dir,
|
|
2000
|
+
category
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
for (const entry of known) {
|
|
2004
|
+
if (!existsSync(entry.dir)) continue;
|
|
2005
|
+
const bytes = await measuredDirectorySizeAsync(entry.dir, { timeoutMs: 12e4 });
|
|
2006
|
+
if (bytes === null) {
|
|
2007
|
+
failure(entry.dir);
|
|
2008
|
+
continue;
|
|
1566
2009
|
}
|
|
2010
|
+
const blocked = cacheBlocked(entry, discovered);
|
|
2011
|
+
sizes.push({
|
|
2012
|
+
...entry,
|
|
2013
|
+
bytes,
|
|
2014
|
+
measuredAt: now,
|
|
2015
|
+
...blocked ? { blocked } : {}
|
|
2016
|
+
});
|
|
1567
2017
|
}
|
|
1568
|
-
return
|
|
2018
|
+
return sizes;
|
|
1569
2019
|
}
|
|
1570
2020
|
//#endregion
|
|
1571
2021
|
//#region src/engine/warm-claim.ts
|
|
@@ -1825,9 +2275,11 @@ function checkoutFactLine(branch, plan, head) {
|
|
|
1825
2275
|
/**
|
|
1826
2276
|
* Whether to install, decided against the lockfile as it stands now rather than against what HEAD did.
|
|
1827
2277
|
* A lockfile that did not move is not evidence that its dependencies are installed: an install that was
|
|
1828
|
-
* interrupted leaves a present node_modules behind and moves HEAD no further.
|
|
2278
|
+
* interrupted leaves a present node_modules behind and moves HEAD no further. A `patches/` change counts
|
|
2279
|
+
* as a dependency change because patch-package applies it only on a fresh install. A tree `npm ls` already
|
|
2280
|
+
* rejected right after the last completed install, with the same problems, is not one another install would fix.
|
|
1829
2281
|
*/
|
|
1830
|
-
function depsPlan({ lockfile, lockfileChanged, installed,
|
|
2282
|
+
function depsPlan({ lockfile, lockfileChanged, patchesChanged, installed, treeProblems, lastInstall, lockHash, patchesHash }) {
|
|
1831
2283
|
if (!lockfile) return {
|
|
1832
2284
|
run: false,
|
|
1833
2285
|
reason: "no lockfile in this repository"
|
|
@@ -1836,7 +2288,7 @@ function depsPlan({ lockfile, lockfileChanged, installed, treeValid, lastInstall
|
|
|
1836
2288
|
run: true,
|
|
1837
2289
|
reason: "no installed dependencies"
|
|
1838
2290
|
};
|
|
1839
|
-
if (
|
|
2291
|
+
if (treeProblems && lastInstall?.treeProblems !== treeProblems) return {
|
|
1840
2292
|
run: true,
|
|
1841
2293
|
reason: `the installed tree does not match ${lockfile}`
|
|
1842
2294
|
};
|
|
@@ -1853,6 +2305,10 @@ function depsPlan({ lockfile, lockfileChanged, installed, treeValid, lastInstall
|
|
|
1853
2305
|
run: true,
|
|
1854
2306
|
reason: `${lockfile} does not match the last completed install`
|
|
1855
2307
|
};
|
|
2308
|
+
if (lastInstall.patches !== patchesHash) return {
|
|
2309
|
+
run: true,
|
|
2310
|
+
reason: "patches/ does not match the last completed install"
|
|
2311
|
+
};
|
|
1856
2312
|
return {
|
|
1857
2313
|
run: false,
|
|
1858
2314
|
reason: `${lockfile} matches the last completed install`
|
|
@@ -1862,6 +2318,10 @@ function depsPlan({ lockfile, lockfileChanged, installed, treeValid, lastInstall
|
|
|
1862
2318
|
run: true,
|
|
1863
2319
|
reason: `${lockfile} changed`
|
|
1864
2320
|
};
|
|
2321
|
+
if (patchesChanged) return {
|
|
2322
|
+
run: true,
|
|
2323
|
+
reason: "patches/ changed"
|
|
2324
|
+
};
|
|
1865
2325
|
return {
|
|
1866
2326
|
run: false,
|
|
1867
2327
|
reason: `${lockfile} unchanged`
|
|
@@ -1873,18 +2333,51 @@ function depsPlan({ lockfile, lockfileChanged, installed, treeValid, lastInstall
|
|
|
1873
2333
|
* nothing about this one, and no record at all is the state of every repository before its first refresh.
|
|
1874
2334
|
*/
|
|
1875
2335
|
function incompleteInstallRefusal(root, appDir) {
|
|
1876
|
-
const dependencies
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
}
|
|
2336
|
+
for (const dependencies of dependencyStates(root, appDir)) {
|
|
2337
|
+
const last = readInstallEvidence(dependencies.root);
|
|
2338
|
+
if (!last || last.completed || last.lock !== dependencies.lock) continue;
|
|
2339
|
+
const hash = lockfileHash(dependencies.root, dependencies.lock);
|
|
2340
|
+
if (hash === null || last.hash !== hash) continue;
|
|
2341
|
+
return {
|
|
2342
|
+
code: "STIM_DEPS_INCOMPLETE",
|
|
2343
|
+
message: `Refusing to copy from ${dependencies.root}: the last install of ${dependencies.lock} there did not finish, so its dependencies are partial.`,
|
|
2344
|
+
lines: [],
|
|
2345
|
+
remedy: `Run \`stim worktree warm --refresh\` to install them in the source checkout -- it reinstalls rather than skipping, because no completed install of this ${dependencies.lock} is recorded -- then warm again.`
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2348
|
+
return null;
|
|
2349
|
+
}
|
|
2350
|
+
function dependencyStates(root, appDir) {
|
|
2351
|
+
const app = dependencyState(existsSync(appDir) ? appDir : root);
|
|
2352
|
+
const top = dependencyState(root, root);
|
|
2353
|
+
return [app, top && top.root !== app?.root && (hasInstalledDependencies(top.root, top.installed) || readInstallEvidence(top.root)) ? top : null].filter((state) => state !== null);
|
|
2354
|
+
}
|
|
2355
|
+
/**
|
|
2356
|
+
* An npm project whose own `.npmrc` sets `ignore-scripts` gets no lifecycle scripts from `npm ci`, its own
|
|
2357
|
+
* postinstall included. `npm run` still runs a named script under that setting, so the project's postinstall
|
|
2358
|
+
* (usually patch-package) runs explicitly; dependencies' install scripts stay blocked.
|
|
2359
|
+
*/
|
|
2360
|
+
function skippedPostinstall(dependencies) {
|
|
2361
|
+
if (dependencies.lock !== "package-lock.json") return false;
|
|
2362
|
+
let postinstall;
|
|
2363
|
+
try {
|
|
2364
|
+
postinstall = JSON.parse(readFileSync(join(dependencies.root, "package.json"), "utf-8"))?.scripts?.postinstall;
|
|
2365
|
+
} catch {
|
|
2366
|
+
return false;
|
|
2367
|
+
}
|
|
2368
|
+
if (typeof postinstall !== "string" || !postinstall.trim()) return false;
|
|
2369
|
+
try {
|
|
2370
|
+
return getExecutor().runFile("npm", [
|
|
2371
|
+
"config",
|
|
2372
|
+
"get",
|
|
2373
|
+
"ignore-scripts"
|
|
2374
|
+
], {
|
|
2375
|
+
cwd: dependencies.root,
|
|
2376
|
+
timeoutMs: 3e4
|
|
2377
|
+
}) === "true";
|
|
2378
|
+
} catch {
|
|
2379
|
+
return false;
|
|
2380
|
+
}
|
|
1888
2381
|
}
|
|
1889
2382
|
function podsPlan({ hasIos, hasPodfile, podfileLockChanged, stale, dir = "ios/" }) {
|
|
1890
2383
|
if (!hasIos) return {
|
|
@@ -1936,6 +2429,22 @@ function lockfileHash(depsRoot, lockfile) {
|
|
|
1936
2429
|
return null;
|
|
1937
2430
|
}
|
|
1938
2431
|
}
|
|
2432
|
+
function hashPatches(depsRoot) {
|
|
2433
|
+
const dir = join(depsRoot, "patches");
|
|
2434
|
+
let names;
|
|
2435
|
+
try {
|
|
2436
|
+
names = readdirSync(dir, {
|
|
2437
|
+
recursive: true,
|
|
2438
|
+
withFileTypes: true
|
|
2439
|
+
}).filter((entry) => entry.isFile() && entry.name.endsWith(".patch")).map((entry) => relative(dir, join(entry.parentPath, entry.name))).toSorted();
|
|
2440
|
+
} catch {
|
|
2441
|
+
return "";
|
|
2442
|
+
}
|
|
2443
|
+
if (!names.length) return "";
|
|
2444
|
+
const hash = createHash("sha256");
|
|
2445
|
+
for (const name of names) hash.update(name).update("\0").update(readFileSync(join(dir, name))).update("\0");
|
|
2446
|
+
return hash.digest("hex");
|
|
2447
|
+
}
|
|
1939
2448
|
function readInstallEvidence(depsRoot) {
|
|
1940
2449
|
try {
|
|
1941
2450
|
const parsed = JSON.parse(readFileSync(installRecordFile(depsRoot), "utf-8"));
|
|
@@ -1944,21 +2453,19 @@ function readInstallEvidence(depsRoot) {
|
|
|
1944
2453
|
return {
|
|
1945
2454
|
lock: parsed.lock,
|
|
1946
2455
|
hash: parsed.hash,
|
|
1947
|
-
|
|
2456
|
+
patches: typeof parsed.patches === "string" ? parsed.patches : "",
|
|
2457
|
+
completed: parsed.completed === true,
|
|
2458
|
+
treeProblems: typeof parsed.treeProblems === "string" ? parsed.treeProblems : null
|
|
1948
2459
|
};
|
|
1949
2460
|
} catch {
|
|
1950
2461
|
return null;
|
|
1951
2462
|
}
|
|
1952
2463
|
}
|
|
1953
|
-
function writeInstallEvidence(depsRoot,
|
|
2464
|
+
function writeInstallEvidence(depsRoot, evidence) {
|
|
1954
2465
|
const file = installRecordFile(depsRoot);
|
|
1955
2466
|
const tmp = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
1956
2467
|
mkdirSync(join(getConfigDir(), "warm-installs"), { recursive: true });
|
|
1957
|
-
writeFileSync(tmp, JSON.stringify(
|
|
1958
|
-
lock: lockfile,
|
|
1959
|
-
hash: hash ?? "",
|
|
1960
|
-
completed
|
|
1961
|
-
}));
|
|
2468
|
+
writeFileSync(tmp, JSON.stringify(evidence));
|
|
1962
2469
|
try {
|
|
1963
2470
|
renameSync(tmp, file);
|
|
1964
2471
|
} catch (error) {
|
|
@@ -1966,8 +2473,17 @@ function writeInstallEvidence(depsRoot, lockfile, hash, completed) {
|
|
|
1966
2473
|
throw error;
|
|
1967
2474
|
}
|
|
1968
2475
|
}
|
|
2476
|
+
function pendingInstallEvidence(dependencies) {
|
|
2477
|
+
return {
|
|
2478
|
+
lock: dependencies.lock,
|
|
2479
|
+
hash: lockfileHash(dependencies.root, dependencies.lock) ?? "",
|
|
2480
|
+
patches: dependencies.lock === "package-lock.json" ? hashPatches(dependencies.root) : "",
|
|
2481
|
+
completed: false,
|
|
2482
|
+
treeProblems: null
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
1969
2485
|
function restoreInstallEvidence(depsRoot, prior) {
|
|
1970
|
-
if (prior) writeInstallEvidence(depsRoot, prior
|
|
2486
|
+
if (prior) writeInstallEvidence(depsRoot, prior);
|
|
1971
2487
|
else rmSync(installRecordFile(depsRoot), { force: true });
|
|
1972
2488
|
}
|
|
1973
2489
|
function inProgressOperation(gitDir) {
|
|
@@ -2181,10 +2697,13 @@ async function refreshMainCheckout({ root, appDir, settings, iosProject, emit, s
|
|
|
2181
2697
|
const plan = checkoutPlan(locallyKnownUpstream(root));
|
|
2182
2698
|
if (plan.kind === "diverged") return divergedRefusal(root, branch, plan);
|
|
2183
2699
|
if (inspectOnly && plan.kind === "fast-forward") return { mutation: `fast-forward ${plan.behind} commit${plan.behind === 1 ? "" : "s"}` };
|
|
2184
|
-
const depsBefore =
|
|
2185
|
-
const
|
|
2186
|
-
const marked = plan.kind === "fast-forward" ? depsBefore :
|
|
2187
|
-
|
|
2700
|
+
const depsBefore = dependencyStates(root, appDir);
|
|
2701
|
+
const priorInstalls = new Map(depsBefore.map((entry) => [entry.root, readInstallEvidence(entry.root)]));
|
|
2702
|
+
const marked = plan.kind === "fast-forward" ? depsBefore : [];
|
|
2703
|
+
for (const entry of marked) writeInstallEvidence(entry.root, pendingInstallEvidence(entry));
|
|
2704
|
+
const restoreMarked = (depsRoot) => {
|
|
2705
|
+
if (marked.some((entry) => entry.root === depsRoot)) restoreInstallEvidence(depsRoot, priorInstalls.get(depsRoot) ?? null);
|
|
2706
|
+
};
|
|
2188
2707
|
let head = before;
|
|
2189
2708
|
if (plan.kind === "fast-forward") {
|
|
2190
2709
|
const merged = fastForward(root, plan.upstream);
|
|
@@ -2199,43 +2718,71 @@ async function refreshMainCheckout({ root, appDir, settings, iosProject, emit, s
|
|
|
2199
2718
|
emit(checkoutFactLine(branch, plan, head));
|
|
2200
2719
|
const note = defaultBranchNote(branch, resolveDefaultBranch(root, settings));
|
|
2201
2720
|
if (note.kind !== "match") for (const line of note.lines) emit(phaseLine("", line));
|
|
2202
|
-
const
|
|
2203
|
-
const
|
|
2204
|
-
|
|
2205
|
-
const
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
cwd: dependencies.root,
|
|
2220
|
-
spawnFn: spawn,
|
|
2221
|
-
now,
|
|
2222
|
-
heartbeatMs,
|
|
2223
|
-
onHeartbeat: emit
|
|
2721
|
+
const dependencyRoots = dependencyStates(root, appDir);
|
|
2722
|
+
for (const entry of marked) if (!dependencyRoots.some((other) => other.root === entry.root)) restoreMarked(entry.root);
|
|
2723
|
+
if (!dependencyRoots.length) emit(stepLine("deps", `source ${root}: no lockfile in this repository`, "skipped"));
|
|
2724
|
+
const owed = [];
|
|
2725
|
+
for (const dependencies of dependencyRoots) {
|
|
2726
|
+
const installed = hasInstalledDependencies(dependencies.root, dependencies.installed);
|
|
2727
|
+
const depsDir = relative(root, dependencies.root);
|
|
2728
|
+
const consumed = pendingInstallEvidence(dependencies);
|
|
2729
|
+
const deps = depsPlan({
|
|
2730
|
+
lockfile: dependencies.lock,
|
|
2731
|
+
lockfileChanged: pathChangedBetween(root, before, head, gitPath(depsDir, dependencies.lock)),
|
|
2732
|
+
patchesChanged: dependencies.lock === "package-lock.json" && pathChangedBetween(root, before, head, gitPath(depsDir, "patches/*.patch")),
|
|
2733
|
+
installed,
|
|
2734
|
+
treeProblems: dependencies.lock === "package-lock.json" && installed ? installedNpmTreeProblems(dependencies.root) : null,
|
|
2735
|
+
lastInstall: priorInstalls.has(dependencies.root) ? priorInstalls.get(dependencies.root) ?? null : readInstallEvidence(dependencies.root),
|
|
2736
|
+
lockHash: consumed.hash || null,
|
|
2737
|
+
patchesHash: consumed.patches
|
|
2224
2738
|
});
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
} else {
|
|
2236
|
-
if (marked) restoreInstallEvidence(marked.root, priorInstall);
|
|
2739
|
+
const depsSubject = `source ${dependencies.root}: ${deps.reason}`;
|
|
2740
|
+
if (deps.run) {
|
|
2741
|
+
if (inspectOnly) return { mutation: deps.reason };
|
|
2742
|
+
owed.push({
|
|
2743
|
+
dependencies,
|
|
2744
|
+
subject: depsSubject
|
|
2745
|
+
});
|
|
2746
|
+
continue;
|
|
2747
|
+
}
|
|
2748
|
+
restoreMarked(dependencies.root);
|
|
2237
2749
|
emit(stepLine("deps", depsSubject, "skipped"));
|
|
2238
2750
|
}
|
|
2751
|
+
for (const { dependencies, subject: depsSubject } of owed) {
|
|
2752
|
+
const consumed = pendingInstallEvidence(dependencies);
|
|
2753
|
+
writeInstallEvidence(dependencies.root, consumed);
|
|
2754
|
+
const runStep = async (command, subject, remedy) => {
|
|
2755
|
+
const install = await runInstallCommand({
|
|
2756
|
+
command,
|
|
2757
|
+
cwd: dependencies.root,
|
|
2758
|
+
spawnFn: spawn,
|
|
2759
|
+
now,
|
|
2760
|
+
heartbeatMs,
|
|
2761
|
+
onHeartbeat: emit
|
|
2762
|
+
});
|
|
2763
|
+
emit(stepLine("deps", subject, `${command} (${formatDuration(install.durationMs)})`));
|
|
2764
|
+
const running = await awaitSpawnedWork(`\`${command}\``);
|
|
2765
|
+
if (!install.ok) return {
|
|
2766
|
+
code: DEPS_ERROR,
|
|
2767
|
+
message: String(install.reason),
|
|
2768
|
+
lines: install.lastLines,
|
|
2769
|
+
remedy: `Run \`cd ${dependencies.root} && ${remedy}\` and fix what it reports, then run warm again.`
|
|
2770
|
+
};
|
|
2771
|
+
return running;
|
|
2772
|
+
};
|
|
2773
|
+
const postinstall = skippedPostinstall(dependencies) ? "npm run postinstall" : null;
|
|
2774
|
+
const installFailure = await runStep(dependencies.command, depsSubject, postinstall ? `${dependencies.command} && ${postinstall}` : dependencies.command);
|
|
2775
|
+
if (installFailure) return installFailure;
|
|
2776
|
+
if (postinstall) {
|
|
2777
|
+
const postinstallFailure = await runStep(postinstall, `source ${dependencies.root}: ignore-scripts skipped postinstall`, postinstall);
|
|
2778
|
+
if (postinstallFailure) return postinstallFailure;
|
|
2779
|
+
}
|
|
2780
|
+
writeInstallEvidence(dependencies.root, {
|
|
2781
|
+
...consumed,
|
|
2782
|
+
completed: true,
|
|
2783
|
+
treeProblems: dependencies.lock === "package-lock.json" ? installedNpmTreeProblems(dependencies.root) : null
|
|
2784
|
+
});
|
|
2785
|
+
}
|
|
2239
2786
|
const app = relative(root, appDir) || ".";
|
|
2240
2787
|
const where = `source ${appDir}: `;
|
|
2241
2788
|
const ios = iosProject;
|
|
@@ -2272,210 +2819,6 @@ async function refreshMainCheckout({ root, appDir, settings, iosProject, emit, s
|
|
|
2272
2819
|
return podsRunning;
|
|
2273
2820
|
}
|
|
2274
2821
|
//#endregion
|
|
2275
|
-
//#region src/workspace/worktree-trash.ts
|
|
2276
|
-
const TRASH_MANIFEST = "trash.json";
|
|
2277
|
-
const TRASH_ENTRY_PREFIX = "remove-";
|
|
2278
|
-
const TRASH_ENTRIES = "entries";
|
|
2279
|
-
const TRASH_CLAIMS = "claims";
|
|
2280
|
-
const LIST_TIMEOUT_MS = 3e4;
|
|
2281
|
-
const DELETE_NICENESS = "10";
|
|
2282
|
-
function rethrowCancellation(error, signal) {
|
|
2283
|
-
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
2284
|
-
signal?.throwIfAborted();
|
|
2285
|
-
}
|
|
2286
|
-
async function isPlainDirectory(path) {
|
|
2287
|
-
const stat = await lstat(path).catch(() => null);
|
|
2288
|
-
return Boolean(stat?.isDirectory());
|
|
2289
|
-
}
|
|
2290
|
-
async function ignoredDirectories(worktree, options) {
|
|
2291
|
-
const out = await gitAsync(worktree, [
|
|
2292
|
-
"ls-files",
|
|
2293
|
-
"--others",
|
|
2294
|
-
"--ignored",
|
|
2295
|
-
"--exclude-standard",
|
|
2296
|
-
"--directory",
|
|
2297
|
-
"--no-empty-directory",
|
|
2298
|
-
"-z"
|
|
2299
|
-
], {
|
|
2300
|
-
...options.env ? childEnvironment(options.env) : {},
|
|
2301
|
-
signal: options.signal,
|
|
2302
|
-
declareFor: options.declareFor,
|
|
2303
|
-
timeoutMs: LIST_TIMEOUT_MS,
|
|
2304
|
-
untrimmed: true
|
|
2305
|
-
});
|
|
2306
|
-
const found = [];
|
|
2307
|
-
for (const entry of out.split("\0")) {
|
|
2308
|
-
if (!entry.endsWith("/")) continue;
|
|
2309
|
-
const rel = entry.slice(0, -1);
|
|
2310
|
-
const parts = rel.split("/");
|
|
2311
|
-
let plain = true;
|
|
2312
|
-
for (let depth = 1; depth <= parts.length && plain; depth++) plain = await isPlainDirectory(join(worktree, ...parts.slice(0, depth)));
|
|
2313
|
-
if (plain) found.push(rel);
|
|
2314
|
-
}
|
|
2315
|
-
return found;
|
|
2316
|
-
}
|
|
2317
|
-
/**
|
|
2318
|
-
* Moves the worktree's ignored directories, which `git worktree remove` would delete, into a new entry under
|
|
2319
|
-
* `$STIM_HOME/trash` held by an exclusive ownership claim. Null, with nothing moved, when the trash is on another
|
|
2320
|
-
* filesystem, the platform is Windows, git cannot list the entries, or the claim cannot be taken. A directory that
|
|
2321
|
-
* does not rename stays in the worktree for git to delete.
|
|
2322
|
-
*/
|
|
2323
|
-
async function moveIgnoredTreesToTrash(worktree, options = {}) {
|
|
2324
|
-
if ((options.platform ?? process.platform) === "win32") return null;
|
|
2325
|
-
let listed;
|
|
2326
|
-
try {
|
|
2327
|
-
listed = await ignoredDirectories(worktree, options);
|
|
2328
|
-
} catch (error) {
|
|
2329
|
-
rethrowCancellation(error, options.signal);
|
|
2330
|
-
return null;
|
|
2331
|
-
}
|
|
2332
|
-
if (!listed.length) return null;
|
|
2333
|
-
const root = trashRoot();
|
|
2334
|
-
let dir = null;
|
|
2335
|
-
let claim = null;
|
|
2336
|
-
const entries = listed.map((from, index) => ({
|
|
2337
|
-
from,
|
|
2338
|
-
to: String(index)
|
|
2339
|
-
}));
|
|
2340
|
-
try {
|
|
2341
|
-
await mkdir(root, { recursive: true });
|
|
2342
|
-
if (filesystemDevice(root) !== filesystemDevice(worktree)) return null;
|
|
2343
|
-
dir = await mkdtemp(join(root, TRASH_ENTRY_PREFIX));
|
|
2344
|
-
claim = (0, ownership_claim_exports.tryAcquireClaim)({
|
|
2345
|
-
root: join(dir, "claims"),
|
|
2346
|
-
mode: "exclusive",
|
|
2347
|
-
label: "worktree trash"
|
|
2348
|
-
}).acquired ?? null;
|
|
2349
|
-
if (!claim) throw new Error("the trash entry claim was not acquired");
|
|
2350
|
-
const manifest = {
|
|
2351
|
-
worktree,
|
|
2352
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2353
|
-
};
|
|
2354
|
-
await writeFile(join(dir, TRASH_MANIFEST), `${JSON.stringify(manifest)}\n`);
|
|
2355
|
-
await mkdir(join(dir, TRASH_ENTRIES));
|
|
2356
|
-
} catch (error) {
|
|
2357
|
-
if (claim) (0, ownership_claim_exports.releaseClaim)(claim);
|
|
2358
|
-
if (dir) await rm(dir, {
|
|
2359
|
-
recursive: true,
|
|
2360
|
-
force: true
|
|
2361
|
-
}).catch(() => {});
|
|
2362
|
-
rethrowCancellation(error, options.signal);
|
|
2363
|
-
return null;
|
|
2364
|
-
}
|
|
2365
|
-
const moved = [];
|
|
2366
|
-
for (const entry of entries) try {
|
|
2367
|
-
await rename(join(worktree, entry.from), join(dir, TRASH_ENTRIES, entry.to));
|
|
2368
|
-
moved.push(entry);
|
|
2369
|
-
} catch {}
|
|
2370
|
-
const trash = {
|
|
2371
|
-
dir,
|
|
2372
|
-
worktree,
|
|
2373
|
-
moved,
|
|
2374
|
-
claim
|
|
2375
|
-
};
|
|
2376
|
-
if (moved.length) return trash;
|
|
2377
|
-
await discardTrash(trash);
|
|
2378
|
-
return null;
|
|
2379
|
-
}
|
|
2380
|
-
async function discardTrash(trash) {
|
|
2381
|
-
await rmdir(join(trash.dir, TRASH_ENTRIES)).catch(() => {});
|
|
2382
|
-
await rm(join(trash.dir, TRASH_MANIFEST), { force: true }).catch(() => {});
|
|
2383
|
-
(0, ownership_claim_exports.releaseClaim)(trash.claim);
|
|
2384
|
-
await rmdir(trash.dir).catch(() => {});
|
|
2385
|
-
}
|
|
2386
|
-
/** Moves every tree back into the worktree. Returns the relative paths that stay in the trash for gc. */
|
|
2387
|
-
async function restoreTrashedTrees(trash) {
|
|
2388
|
-
const kept = [];
|
|
2389
|
-
for (const entry of trash.moved.toReversed()) {
|
|
2390
|
-
const target = join(trash.worktree, entry.from);
|
|
2391
|
-
try {
|
|
2392
|
-
if (await lstat(target).catch(() => null) || !await isPlainDirectory(dirname(target))) {
|
|
2393
|
-
kept.push(entry.from);
|
|
2394
|
-
continue;
|
|
2395
|
-
}
|
|
2396
|
-
await rename(join(trash.dir, TRASH_ENTRIES, entry.to), target);
|
|
2397
|
-
} catch {
|
|
2398
|
-
kept.push(entry.from);
|
|
2399
|
-
}
|
|
2400
|
-
}
|
|
2401
|
-
if (kept.length) (0, ownership_claim_exports.releaseClaim)(trash.claim);
|
|
2402
|
-
else await discardTrash(trash);
|
|
2403
|
-
return kept;
|
|
2404
|
-
}
|
|
2405
|
-
/** Deletes one trash entry: `rm -rf` for the bulk, then a walk that also clears read-only directories. */
|
|
2406
|
-
async function deleteTrashEntry(dir, operation) {
|
|
2407
|
-
if (process.platform !== "win32") try {
|
|
2408
|
-
await runOperationFile(operation, getExecutor(), "rm", [
|
|
2409
|
-
"-rf",
|
|
2410
|
-
"--",
|
|
2411
|
-
join(dir, TRASH_ENTRIES)
|
|
2412
|
-
], {
|
|
2413
|
-
timeoutMs: "unbounded",
|
|
2414
|
-
signal: operation?.signal,
|
|
2415
|
-
cwd: dirname(dir)
|
|
2416
|
-
});
|
|
2417
|
-
} catch (error) {
|
|
2418
|
-
rethrowCancellation(error, operation?.signal);
|
|
2419
|
-
}
|
|
2420
|
-
await removeTemporaryEntryAsync(dir, operation?.signal);
|
|
2421
|
-
}
|
|
2422
|
-
function deleteInBackground(trash) {
|
|
2423
|
-
try {
|
|
2424
|
-
(0, ownership_claim_exports.markClaimChildPending)(trash.claim);
|
|
2425
|
-
const child = getExecutor().spawn("nice", [
|
|
2426
|
-
"-n",
|
|
2427
|
-
DELETE_NICENESS,
|
|
2428
|
-
"rm",
|
|
2429
|
-
"-rf",
|
|
2430
|
-
"--",
|
|
2431
|
-
join(trash.dir, TRASH_ENTRIES),
|
|
2432
|
-
trash.dir
|
|
2433
|
-
], {
|
|
2434
|
-
cwd: dirname(trash.dir),
|
|
2435
|
-
detached: true,
|
|
2436
|
-
stdio: "ignore"
|
|
2437
|
-
});
|
|
2438
|
-
child.on?.("error", () => {});
|
|
2439
|
-
child.unref?.();
|
|
2440
|
-
const captured = child.pid === void 0 ? null : captureProcessIdentity(child.pid);
|
|
2441
|
-
if (captured?.ok === false && captured.reason.startsWith("NOT_FOUND")) {
|
|
2442
|
-
(0, ownership_claim_exports.clearClaimChild)(trash.claim);
|
|
2443
|
-
(0, ownership_claim_exports.releaseClaim)(trash.claim);
|
|
2444
|
-
return existsSync(trash.dir) ? "left" : "deleted";
|
|
2445
|
-
}
|
|
2446
|
-
if (!captured?.ok || child.pid === void 0) throw new Error("the trash deletion process was not recorded");
|
|
2447
|
-
(0, ownership_claim_exports.setClaimChild)(trash.claim, {
|
|
2448
|
-
pid: child.pid,
|
|
2449
|
-
processToken: captured.token
|
|
2450
|
-
});
|
|
2451
|
-
return "background";
|
|
2452
|
-
} catch {
|
|
2453
|
-
try {
|
|
2454
|
-
(0, ownership_claim_exports.clearClaimChild)(trash.claim);
|
|
2455
|
-
} catch {}
|
|
2456
|
-
(0, ownership_claim_exports.releaseClaim)(trash.claim);
|
|
2457
|
-
return "left";
|
|
2458
|
-
}
|
|
2459
|
-
}
|
|
2460
|
-
/**
|
|
2461
|
-
* Deletes the trash entry once git removed the worktree. A CLI command starts a detached, low-priority `rm` recorded
|
|
2462
|
-
* as the claim's child, so gc reports the entry as in use until `rm` exits. Inside a bounded report, such as the
|
|
2463
|
-
* programmatic API, every child is stopped when the report settles, so the entry is deleted before returning.
|
|
2464
|
-
* `left` means the entry waits for gc with its claim released.
|
|
2465
|
-
*/
|
|
2466
|
-
async function disposeTrash(trash, operation) {
|
|
2467
|
-
if (!getExecutor().reportSignal?.()) return deleteInBackground(trash);
|
|
2468
|
-
try {
|
|
2469
|
-
await deleteTrashEntry(trash.dir, operation);
|
|
2470
|
-
return "deleted";
|
|
2471
|
-
} catch (error) {
|
|
2472
|
-
rethrowCancellation(error, operation?.signal);
|
|
2473
|
-
return "left";
|
|
2474
|
-
} finally {
|
|
2475
|
-
(0, ownership_claim_exports.releaseClaim)(trash.claim);
|
|
2476
|
-
}
|
|
2477
|
-
}
|
|
2478
|
-
//#endregion
|
|
2479
2822
|
//#region src/commands/worktree.ts
|
|
2480
2823
|
function dependencyInstallCommand(target, dir = ".") {
|
|
2481
2824
|
const installRoot = resolve$1(target, dir);
|
|
@@ -2532,7 +2875,7 @@ function reportRefreshFailure(failure, out) {
|
|
|
2532
2875
|
out(chalk.red(`failed: ${failure.code}`));
|
|
2533
2876
|
}
|
|
2534
2877
|
function mainCheckoutAppDir(root, target, cwd) {
|
|
2535
|
-
const app = findProjectRoot$1(cwd);
|
|
2878
|
+
const app = warmedWorkspace(root, target, cwd) ?? findProjectRoot$1(cwd);
|
|
2536
2879
|
if (!app) return root;
|
|
2537
2880
|
const rel = relative$1(target, app);
|
|
2538
2881
|
if (!rel || rel.startsWith("..") || isAbsolute$1(rel)) return root;
|
|
@@ -2540,20 +2883,26 @@ function mainCheckoutAppDir(root, target, cwd) {
|
|
|
2540
2883
|
}
|
|
2541
2884
|
/**
|
|
2542
2885
|
* The app workspace a warm prepares: the nearest app below the worktree root, or else the one app registered in
|
|
2543
|
-
* the source checkout, at the same place in this worktree, or else the
|
|
2544
|
-
* root can declare react-native without being the app.
|
|
2886
|
+
* the source checkout, at the same place in this worktree, or else the one registered in this worktree, or else
|
|
2887
|
+
* the worktree root when it is the app. A monorepo root can declare react-native without being the app.
|
|
2545
2888
|
*/
|
|
2546
2889
|
function warmedWorkspace(root, target, cwd) {
|
|
2547
2890
|
const nearest = findProjectRoot$1(cwd);
|
|
2548
|
-
const app = nearest && isPathPrefix(target, nearest) && isMobileProject(nearest) ? nearest : null;
|
|
2891
|
+
const app = nearest && isPathPrefix$1(target, nearest) && isMobileProject(nearest) ? nearest : null;
|
|
2549
2892
|
if (app && app !== target) return app;
|
|
2550
|
-
const
|
|
2551
|
-
|
|
2552
|
-
const
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2893
|
+
const registered = Object.keys(loadConfig$1()?.projects ?? {});
|
|
2894
|
+
const appsUnder = (checkout) => {
|
|
2895
|
+
const apps = /* @__PURE__ */ new Set();
|
|
2896
|
+
for (const path of registered) {
|
|
2897
|
+
const rel = relative$1(checkout, path);
|
|
2898
|
+
if (rel.startsWith("..") || isAbsolute$1(rel)) continue;
|
|
2899
|
+
const mapped = resolve$1(target, rel);
|
|
2900
|
+
if (isMobileProject(mapped)) apps.add(mapped);
|
|
2901
|
+
}
|
|
2902
|
+
return apps;
|
|
2903
|
+
};
|
|
2904
|
+
const source = appsUnder(root);
|
|
2905
|
+
const apps = source.size ? source : appsUnder(target);
|
|
2557
2906
|
return apps.size === 1 ? [...apps][0] : app;
|
|
2558
2907
|
}
|
|
2559
2908
|
async function registerWarmedWorkspace(workspace, out) {
|
|
@@ -2608,7 +2957,7 @@ async function warmWorktreeTarget(opts) {
|
|
|
2608
2957
|
for (const key of scope) opts.beforeWarm?.(nativeCanonicalPath(key));
|
|
2609
2958
|
if (app) await recordWorkspaceUseAsync(app, /* @__PURE__ */ new Date(), operation.env, opts.cwd, operation.signal);
|
|
2610
2959
|
if (workspace && workspace !== app) await recordWorkspaceUseAsync(workspace, /* @__PURE__ */ new Date(), operation.env, opts.cwd, operation.signal);
|
|
2611
|
-
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")));
|
|
2960
|
+
if (!workspace) out(chalk.dim(phaseLine("project", opts.refresh ? "no single app to register here; --refresh updates only the repository root. Run warm --refresh from the app directory to refresh that app and list it in status" : "no single app to register here; run warm from the app directory to list it in status while it warms")));
|
|
2612
2961
|
else {
|
|
2613
2962
|
await registerWarmedWorkspace(workspace, out);
|
|
2614
2963
|
progress = startWarmProgress(workspace, opts.refresh ? "refresh" : "copy", { note: (line) => out(chalk.yellow(line)) });
|
|
@@ -2624,7 +2973,7 @@ async function warmWorktreeTarget(opts) {
|
|
|
2624
2973
|
out(chalk.dim(SETTING_SHAPE_REMEDY));
|
|
2625
2974
|
return null;
|
|
2626
2975
|
}
|
|
2627
|
-
for (const key of unknownSettingKeys(settings)) out(chalk.yellow(
|
|
2976
|
+
for (const key of unknownSettingKeys(settings)) out(chalk.yellow(inertSettingWarning(settings, key)));
|
|
2628
2977
|
return settings;
|
|
2629
2978
|
};
|
|
2630
2979
|
const copy = async (wait) => {
|
|
@@ -2657,12 +3006,12 @@ async function warmWorktreeTarget(opts) {
|
|
|
2657
3006
|
}
|
|
2658
3007
|
const excluded = readWorktreeExclude(root);
|
|
2659
3008
|
const patterns = excluded?.length ? excluded : settings.worktree?.exclude || [];
|
|
2660
|
-
const result = await cloneIgnoredEntries({
|
|
3009
|
+
const result = await trace.spanAsync("carry ignored entries", () => cloneIgnoredEntries({
|
|
2661
3010
|
root,
|
|
2662
3011
|
target,
|
|
2663
3012
|
patterns,
|
|
2664
3013
|
operation
|
|
2665
|
-
});
|
|
3014
|
+
}));
|
|
2666
3015
|
for (const entry of result.skipped) out(chalk.dim(phaseLine("carry", `kept ${entry.file} (${entry.reason})`)));
|
|
2667
3016
|
for (const entry of result.failed) out(chalk.yellow(phaseLine("carry", `could not copy ${entry.file}: ${entry.error}`)));
|
|
2668
3017
|
if (result.copied.length) {
|
|
@@ -2783,7 +3132,7 @@ function customIosDirs(worktree) {
|
|
|
2783
3132
|
} catch {
|
|
2784
3133
|
return [];
|
|
2785
3134
|
}
|
|
2786
|
-
return Object.keys(loadConfig$1()?.projects ?? {}).filter((path) => isPathPrefix(worktree, path)).map((path) => ({
|
|
3135
|
+
return Object.keys(loadConfig$1()?.projects ?? {}).filter((path) => isPathPrefix$1(worktree, path)).map((path) => ({
|
|
2787
3136
|
path,
|
|
2788
3137
|
ios: appIosProjectDir(path)
|
|
2789
3138
|
})).filter(({ ios }) => ios.custom && existsSync$1(ios.dir)).map(({ path, ios }) => ({
|
|
@@ -2844,13 +3193,13 @@ function nativeCanonicalPath(path) {
|
|
|
2844
3193
|
/** Other worktrees git lists inside `path`, which removing `path` would delete with it. */
|
|
2845
3194
|
function nestedWorktrees(entries, path) {
|
|
2846
3195
|
const root = nativeCanonicalPath(path);
|
|
2847
|
-
return entries.filter((candidate) => !candidate.bare && nativeCanonicalPath(candidate.path) !== root).filter((candidate) => isPathPrefix(root, nativeCanonicalPath(candidate.path))).map((candidate) => candidate.path);
|
|
3196
|
+
return entries.filter((candidate) => !candidate.bare && nativeCanonicalPath(candidate.path) !== root).filter((candidate) => isPathPrefix$1(root, nativeCanonicalPath(candidate.path))).map((candidate) => candidate.path);
|
|
2848
3197
|
}
|
|
2849
3198
|
function matchWorktreeEntry(entries, path) {
|
|
2850
3199
|
let best = null;
|
|
2851
3200
|
const target = nativeCanonicalPath(path);
|
|
2852
3201
|
(entries || []).forEach((entry, index) => {
|
|
2853
|
-
if (!entry?.path || !isPathPrefix(nativeCanonicalPath(entry.path), target)) return;
|
|
3202
|
+
if (!entry?.path || !isPathPrefix$1(nativeCanonicalPath(entry.path), target)) return;
|
|
2854
3203
|
if (!best || entry.path.length > best.path.length) best = {
|
|
2855
3204
|
...entry,
|
|
2856
3205
|
index
|
|
@@ -2886,10 +3235,11 @@ function pathArgs(lines, limit = 5) {
|
|
|
2886
3235
|
const shown = paths.slice(0, limit).map((p) => /[\s"'\\$`]/.test(p) ? JSON.stringify(p) : p);
|
|
2887
3236
|
return `${shown.join(" ")}${paths.length > shown.length ? " ..." : ""}`;
|
|
2888
3237
|
}
|
|
3238
|
+
const DIRTY_BLOCKER = "uncommitted changes or untracked files";
|
|
2889
3239
|
function removalBlockers({ dirty, unpushed }) {
|
|
2890
3240
|
const blockers = [];
|
|
2891
3241
|
if (dirty === null || unpushed === null) blockers.push("could not determine git status; re-run with --force to override");
|
|
2892
|
-
if (dirty) blockers.push(
|
|
3242
|
+
if (dirty) blockers.push(DIRTY_BLOCKER);
|
|
2893
3243
|
if (unpushed && unpushed.length) blockers.push(`${plural(unpushed.length, "commit")} not on any remote or any other local branch`);
|
|
2894
3244
|
return blockers;
|
|
2895
3245
|
}
|
|
@@ -2901,7 +3251,7 @@ function reclaimKeys(rootPath) {
|
|
|
2901
3251
|
const keys = /* @__PURE__ */ new Set([rootPath]);
|
|
2902
3252
|
const root = nativeCanonicalPath(rootPath);
|
|
2903
3253
|
if (cfg?.projects) {
|
|
2904
|
-
for (const key of Object.keys(cfg.projects)) if (isPathPrefix(rootPath, key) || isPathPrefix(root, nativeCanonicalPath(key))) keys.add(key);
|
|
3254
|
+
for (const key of Object.keys(cfg.projects)) if (isPathPrefix$1(rootPath, key) || isPathPrefix$1(root, nativeCanonicalPath(key))) keys.add(key);
|
|
2905
3255
|
}
|
|
2906
3256
|
return [...keys].toSorted();
|
|
2907
3257
|
}
|
|
@@ -3042,11 +3392,11 @@ function reportRetainedResources(root, result, output) {
|
|
|
3042
3392
|
}
|
|
3043
3393
|
function hasRegisteredProjectUnder(rootPath) {
|
|
3044
3394
|
const cfg = loadConfig$1();
|
|
3045
|
-
return Object.keys(cfg?.projects ?? {}).some((key) => isPathPrefix(rootPath, key));
|
|
3395
|
+
return Object.keys(cfg?.projects ?? {}).some((key) => isPathPrefix$1(rootPath, key));
|
|
3046
3396
|
}
|
|
3047
3397
|
function projectAtOrAbove(root, path) {
|
|
3048
3398
|
const here = nativeCanonicalPath(path);
|
|
3049
|
-
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;
|
|
3399
|
+
return Object.keys(loadConfig$1()?.projects ?? {}).filter((key) => isPathPrefix$1(nativeCanonicalPath(root), nativeCanonicalPath(key)) && isPathPrefix$1(nativeCanonicalPath(key), here)).toSorted((a, b) => b.length - a.length)[0] ?? null;
|
|
3050
3400
|
}
|
|
3051
3401
|
async function reclaimEnvironment(root, why, output, operation, beforeRemove) {
|
|
3052
3402
|
await withManagedRemoteWorktreeRemovalLock(root, () => withReclaimLocks(root, async (lockedKeys) => {
|
|
@@ -3080,6 +3430,9 @@ const REMOVAL_OUTPUT = {
|
|
|
3080
3430
|
process.exitCode = 1;
|
|
3081
3431
|
}
|
|
3082
3432
|
};
|
|
3433
|
+
function blockersOf(inspection, opts) {
|
|
3434
|
+
return opts.archiveDirty ? inspection.blockers.filter((blocker) => blocker !== DIRTY_BLOCKER) : inspection.blockers;
|
|
3435
|
+
}
|
|
3083
3436
|
function removalPath(target, cwd = process.cwd()) {
|
|
3084
3437
|
return canonicalExistingPath(target ?? cwd);
|
|
3085
3438
|
}
|
|
@@ -3186,6 +3539,25 @@ function printRemovalCleanup(result, failed, output) {
|
|
|
3186
3539
|
if (result.dereferenced.length) output.error(chalk.dim(phaseLine("workspace", `no longer referenced: ${result.dereferenced.join(", ")}`)));
|
|
3187
3540
|
for (const skipped of result.skippedDevices) output.error((failed ? chalk.dim : chalk.yellow)(phaseLine("device", `kept ${describeKeptDevice(skipped)} (${skipped.reason})`)));
|
|
3188
3541
|
}
|
|
3542
|
+
async function archiveBeforeRemoval(path, repository, branch, head, opts, output) {
|
|
3543
|
+
try {
|
|
3544
|
+
if (!head) throw new Error("its HEAD could not be read");
|
|
3545
|
+
return await archiveWorktreeChanges({
|
|
3546
|
+
path,
|
|
3547
|
+
repository,
|
|
3548
|
+
branch,
|
|
3549
|
+
head,
|
|
3550
|
+
pullRequest: opts.archiveDirty?.pullRequest ?? null,
|
|
3551
|
+
operation: opts.operation
|
|
3552
|
+
});
|
|
3553
|
+
} catch (error) {
|
|
3554
|
+
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
3555
|
+
opts.operation?.signal.throwIfAborted();
|
|
3556
|
+
output.error(chalk.red(`Refusing to remove ${path}: could not archive its uncommitted changes: ${(error instanceof Error ? error.message : String(error)).split("\n")[0]}`));
|
|
3557
|
+
output.fail();
|
|
3558
|
+
return null;
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3189
3561
|
async function removeWorktreeTarget(target, opts = {}) {
|
|
3190
3562
|
let removed = false;
|
|
3191
3563
|
await runRemove(target, opts, () => {
|
|
@@ -3337,6 +3709,13 @@ async function removeOrRefuse(target, opts, onRemoved, output) {
|
|
|
3337
3709
|
output.fail();
|
|
3338
3710
|
return;
|
|
3339
3711
|
}
|
|
3712
|
+
const cli = stimCliUseOf(path, nativeCanonicalPath, stimCliEntries(opts.operation?.env));
|
|
3713
|
+
if (cli.length) {
|
|
3714
|
+
output.error(chalk.red(`Refusing to remove ${path}: ${cli.join("; ")}.`));
|
|
3715
|
+
output.error(chalk.dim("Run stim from another checkout or install, or point PATH elsewhere, then retry."));
|
|
3716
|
+
output.fail();
|
|
3717
|
+
return;
|
|
3718
|
+
}
|
|
3340
3719
|
const project = getProject(path);
|
|
3341
3720
|
const branch = entry.branch;
|
|
3342
3721
|
const ownsBranch = Boolean(branch && project?.worktreeBranchOwned === true && project.worktreeBranch === branch);
|
|
@@ -3352,13 +3731,15 @@ async function removeOrRefuse(target, opts, onRemoved, output) {
|
|
|
3352
3731
|
output.error(chalk.dim(`HEAD is in the merged PR #${pr.number} (${pr.url}); its local-only commits stay on GitHub.`));
|
|
3353
3732
|
}
|
|
3354
3733
|
}
|
|
3355
|
-
if (inspection.
|
|
3734
|
+
if (blockersOf(inspection, opts).length && !opts.force) {
|
|
3356
3735
|
await printRemovalRefusal(path, inspection, output, opts.operation);
|
|
3357
3736
|
return;
|
|
3358
3737
|
}
|
|
3359
3738
|
const deleteOwnedBranch = Boolean(ownsBranch && approvedBranchSha && inspection.unpushed?.length === 0);
|
|
3360
3739
|
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;
|
|
3361
3740
|
const branchDeleteCwd = source.path;
|
|
3741
|
+
let archive = null;
|
|
3742
|
+
let gitRemoving = false;
|
|
3362
3743
|
await withManagedRemoteWorktreeRemovalLock(path, () => withReclaimLocks(path, async (lockedKeys) => {
|
|
3363
3744
|
options.signal?.throwIfAborted();
|
|
3364
3745
|
if ((await opts.guard?.(lockedKeys))?.length) return;
|
|
@@ -3370,16 +3751,25 @@ async function removeOrRefuse(target, opts, onRemoved, output) {
|
|
|
3370
3751
|
return;
|
|
3371
3752
|
}
|
|
3372
3753
|
const current = await inspectRemoval(path, mergedHead, opts.operation);
|
|
3373
|
-
if (current.
|
|
3754
|
+
if (blockersOf(current, opts).length && !opts.force) {
|
|
3374
3755
|
await printRemovalRefusal(path, current, output, opts.operation);
|
|
3375
3756
|
return;
|
|
3376
3757
|
}
|
|
3758
|
+
if (opts.archiveDirty && !opts.force && current.dirtyLines.length) {
|
|
3759
|
+
const saved = await archiveBeforeRemoval(path, source.path, branch ?? null, inspectedHead, opts, output);
|
|
3760
|
+
if (!saved) return;
|
|
3761
|
+
archive = saved;
|
|
3762
|
+
}
|
|
3763
|
+
const discardArchive = async () => {
|
|
3764
|
+
if (archive) await discardWorktreeArchive(archive);
|
|
3765
|
+
};
|
|
3377
3766
|
const result = await reclaimAll(path, lockedKeys, {
|
|
3378
3767
|
preserveRootProject: true,
|
|
3379
3768
|
removedBy: opts.removedBy ?? "worktree-remove",
|
|
3380
3769
|
operation: opts.operation
|
|
3381
3770
|
});
|
|
3382
3771
|
if (result.keptEntries.length) {
|
|
3772
|
+
await discardArchive();
|
|
3383
3773
|
reportRetainedResources(path, result, output);
|
|
3384
3774
|
return;
|
|
3385
3775
|
}
|
|
@@ -3390,12 +3780,14 @@ async function removeOrRefuse(target, opts, onRemoved, output) {
|
|
|
3390
3780
|
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
3391
3781
|
opts.operation?.signal.throwIfAborted();
|
|
3392
3782
|
if (!isTimeoutError(error)) throw error;
|
|
3783
|
+
await discardArchive();
|
|
3393
3784
|
await printRemovalRefusal(path, timedOutInspection(error), output, opts.operation);
|
|
3394
3785
|
output.error(chalk.dim(`The directory and Stim ownership record for ${path} were kept.`));
|
|
3395
3786
|
printRemovalCleanup(result, true, output);
|
|
3396
3787
|
return;
|
|
3397
3788
|
}
|
|
3398
3789
|
if (headMoved) {
|
|
3790
|
+
await discardArchive();
|
|
3399
3791
|
output.error(chalk.red(`Refusing to remove ${path}: its HEAD moved while its environment was reclaimed.`));
|
|
3400
3792
|
output.error(chalk.dim(`The directory and Stim ownership record for ${path} were kept.`));
|
|
3401
3793
|
printRemovalCleanup(result, true, output);
|
|
@@ -3410,10 +3802,28 @@ async function removeOrRefuse(target, opts, onRemoved, output) {
|
|
|
3410
3802
|
signal: options.signal,
|
|
3411
3803
|
declareFor: opts.operation
|
|
3412
3804
|
});
|
|
3805
|
+
let stale = null;
|
|
3806
|
+
if (archive) try {
|
|
3807
|
+
stale = await worktreeArchiveMismatch(path, archive, opts.operation);
|
|
3808
|
+
} catch (error) {
|
|
3809
|
+
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
3810
|
+
stale = `its archive could not be checked: ${error instanceof Error ? error.message : String(error)}`;
|
|
3811
|
+
}
|
|
3812
|
+
if (stale) {
|
|
3813
|
+
const unrestored = trash ? await restoreTrashedTrees(trash) : [];
|
|
3814
|
+
if (trash && unrestored.length) output.error(chalk.yellow(`Could not move ${unrestored.join(", ")} back into ${path}; ${unrestored.length === 1 ? "it stays" : "they stay"} in ${trash.dir}, which stim gc keeps while ${path} exists.`));
|
|
3815
|
+
await discardArchive();
|
|
3816
|
+
output.error(chalk.red(`Refusing to remove ${path}: ${stale}.`));
|
|
3817
|
+
output.error(chalk.dim(`The directory and Stim ownership record for ${path} were kept.`));
|
|
3818
|
+
printRemovalCleanup(result, true, output);
|
|
3819
|
+
output.fail();
|
|
3820
|
+
return;
|
|
3821
|
+
}
|
|
3822
|
+
gitRemoving = true;
|
|
3413
3823
|
try {
|
|
3414
3824
|
await removeWorktreeAsync(path, {
|
|
3415
3825
|
from: source.path,
|
|
3416
|
-
force: opts.force,
|
|
3826
|
+
force: opts.force || archive !== null,
|
|
3417
3827
|
env: options.env,
|
|
3418
3828
|
signal: options.signal,
|
|
3419
3829
|
declareFor: opts.operation
|
|
@@ -3426,10 +3836,15 @@ async function removeOrRefuse(target, opts, onRemoved, output) {
|
|
|
3426
3836
|
output.error(chalk.red(`git worktree remove failed: ${message}`));
|
|
3427
3837
|
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."));
|
|
3428
3838
|
output.error(chalk.dim(`The directory and Stim ownership record for ${path} were kept.`));
|
|
3839
|
+
if (archive) output.error(chalk.dim(`Its uncommitted changes stay archived in ${archive.dir}.`));
|
|
3429
3840
|
printRemovalCleanup(result, true, output);
|
|
3430
3841
|
output.fail();
|
|
3431
3842
|
return;
|
|
3432
3843
|
}
|
|
3844
|
+
if (archive) {
|
|
3845
|
+
opts.archiveDirty?.onArchived(archive);
|
|
3846
|
+
output.error(chalk.dim(phaseLine("archive", `saved ${plural(archive.tracked.length + archive.untracked.length, "uncommitted file")} to ${archive.dir}; restore: ${archive.restore.join(" && ")}`)));
|
|
3847
|
+
}
|
|
3433
3848
|
onRemoved();
|
|
3434
3849
|
try {
|
|
3435
3850
|
forgetStatusMeasures(path, result.reclaimedKeys);
|
|
@@ -3492,6 +3907,11 @@ async function removeOrRefuse(target, opts, onRemoved, output) {
|
|
|
3492
3907
|
}, opts.operation, opts.beforeRemove), {
|
|
3493
3908
|
signal: opts.operation?.signal,
|
|
3494
3909
|
declareSpawnsFor: opts.operation
|
|
3910
|
+
}).catch(async (error) => {
|
|
3911
|
+
if (archive && !gitRemoving) await discardWorktreeArchive(archive);
|
|
3912
|
+
throw error;
|
|
3913
|
+
}).finally(() => {
|
|
3914
|
+
if (archive) releaseWorktreeArchive(archive);
|
|
3495
3915
|
});
|
|
3496
3916
|
}
|
|
3497
3917
|
function registerRemove(worktree) {
|
|
@@ -3505,4 +3925,4 @@ function worktreeCommand(program) {
|
|
|
3505
3925
|
registerRemove(worktree);
|
|
3506
3926
|
}
|
|
3507
3927
|
//#endregion
|
|
3508
|
-
export {
|
|
3928
|
+
export { reclaimProject as A, measureSizes as C, stimCliEntries as D, serviceRunningFrom as E, sweepArchiveStaging as M, worktreeArchiveLimits as N, stimCliUseOf as O, measurePressureAsync as S, uncommittedActivityOf as T, worktreeCommand as _, matchWorktreeEntry as a, measureDiskAsync as b, reclaimKeys as c, removalBlockers as d, removalPath as f, warmedWorkspace as g, warmWorktreeTarget as h, excludeWatchmanCookies as i, deleteSelectedArchives as j, describeDereferenced 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, cacheBlocked as v, sizeScanDeferred as w, measurePressure as x, measureDisk as y };
|