stim 1.26.0 → 1.28.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-CKByBnTD.mjs} +2 -2
- package/dist/{act-C-BqJEpu.mjs → act-8iswyfRa.mjs} +319 -126
- package/dist/{activity-Lnb_EoRY.mjs → activity-DRMg4_mD.mjs} +6 -6
- package/dist/{agent-device-usage-output-DbXy5tg8.mjs → agent-device-usage-output-tE90fDdo.mjs} +5 -5
- package/dist/{android-CiNQw3kq.mjs → android-CHmSU4B4.mjs} +101 -45
- package/dist/{android-DTAcRwBC.mjs → android-CsPs-7Mg.mjs} +23 -18
- package/dist/{android-B5P81yI-.mjs → android-D577ApKG.mjs} +37 -15
- package/dist/{android-CSbLRD53.mjs → android-YWCbmkVK.mjs} +5 -5
- package/dist/{android-cas-BTVhZ0Qs.mjs → android-cas-_aDifWW0.mjs} +4 -4
- package/dist/android-cas-compiler.mjs +1 -1
- package/dist/api.d.mts +14 -4
- package/dist/api.mjs +39 -29
- package/dist/{apk-signed-tail-B46U9hXc.mjs → apk-signed-tail-CQbwM0qO.mjs} +2 -2
- package/dist/{artifact-CsAP6AGK.mjs → artifact-DYtHjxsn.mjs} +19 -24
- package/dist/{artifact-BxREhAaV.mjs → artifact-bFkfD3vf.mjs} +18 -23
- package/dist/{artifact-platform-CMRc0M_r.mjs → artifact-platform-BO2Uvk_A.mjs} +1 -1
- package/dist/auto-placement-BVqs8AJi.mjs +350 -0
- package/dist/{browser-web-rYPyqhTf.mjs → browser-web-BUaLcafY.mjs} +16 -16
- package/dist/{build-DPhApsjV.mjs → build-cQURy_KW.mjs} +35 -31
- package/dist/{build-cache-DrXNCy3T.mjs → build-cache-CZULpXVl.mjs} +43 -21
- package/dist/{build-plan-DrMMdayj.mjs → build-plan-CxoYfxsc.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-Bm_A1xX9.mjs} +562 -1604
- 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-CpbaLDa7.mjs} +24 -23
- package/dist/cli.mjs +1 -1
- package/dist/{client-D9CaRks-.mjs → client-YxCer6ab.mjs} +84 -65
- package/dist/collector-run.d.mts +10 -9
- package/dist/collector-run.mjs +42 -27
- package/dist/{command-output-BF5AR2dL.mjs → command-output-C8R1KIvi.mjs} +1 -1
- package/dist/{config-BktkY_zC.mjs → config-Cn94Jc0g.mjs} +23 -4
- package/dist/{dependencies-Dw6wfQdv.mjs → dependencies-DKeMzwXY.mjs} +108 -60
- package/dist/{dependency-state-DFB41vfr.mjs → dependency-state-CojA2o0t.mjs} +14 -7
- package/dist/{detached-entry-KmieOKgN.mjs → detached-entry-BPlSaCwC.mjs} +1 -1
- package/dist/{dev-client-DXR_tPVY.mjs → dev-client-D-BW5uiq.mjs} +2 -2
- package/dist/{device-BK-zwyd_.mjs → device-CgXfbcz5.mjs} +25 -24
- package/dist/{device-capacity-DCSvUF0l.mjs → device-capacity-Cd8lMSn3.mjs} +175 -22
- package/dist/{device-BcYeHSFu.mjs → device-eeK95Lir.mjs} +16 -16
- package/dist/device-host-worker.mjs +22 -20
- package/dist/{device-ios-BLGfXQeK.mjs → device-ios-Cc9ulihb.mjs} +251 -23
- package/dist/{device-lease-run-BmvIl7bI.mjs → device-lease-run-Mtzp4THX.mjs} +5 -5
- package/dist/{device-pool-Dluix4H3.mjs → device-pool-eRz6DtCg.mjs} +3 -3
- package/dist/{doctor-Bhg1ii70.mjs → doctor-Bz_ouYAW.mjs} +28 -16
- package/dist/{doctor-Cp-BhzTS.mjs → doctor-C2QyhejJ.mjs} +1 -1
- 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-BTW82PDC.mjs} +5 -5
- package/dist/{doctor-OnwP94ne.mjs → doctor-xf7ht-5n.mjs} +76 -32
- package/dist/{eas-build-Dsj93iM9.mjs → eas-build-Dm6q68QY.mjs} +4 -4
- package/dist/eas-fallback-DajNpxf-.mjs +87 -0
- package/dist/{eas-session-ledger-B5vygTiD.mjs → eas-session-ledger-CBFo_Uo1.mjs} +37 -8
- package/dist/{error-diagnostics-BOOm9Db7.mjs → error-diagnostics-D1QfyJzQ.mjs} +51 -25
- package/dist/{exec-VFRpu1iA.mjs → exec-aULwtNly.mjs} +57 -6
- package/dist/{gc-ldgr-hXU.mjs → gc-DM5oMm-J.mjs} +161 -45
- package/dist/{gc-C_79zvz4.mjs → gc-PLFwb0k4.mjs} +2 -2
- package/dist/{guide-D7K0X6KG.mjs → guide-C4I46XAk.mjs} +674 -213
- package/dist/{hosted-android-Bk54ej6n.mjs → hosted-android-DlaKLbW0.mjs} +6 -7
- package/dist/{hosted-client-fHO8yqzc.mjs → hosted-client-DDRm9hPJ.mjs} +51 -20
- package/dist/{hosted-logs-BfbmDM4M.mjs → hosted-logs-bSebOKbz.mjs} +8 -8
- package/dist/{hosted-logs-sync-B0pvmOk-.mjs → hosted-logs-sync-B0c53W_c.mjs} +5 -5
- package/dist/{hosted-macos-CPl0OZ9h.mjs → hosted-macos-B7-8Pv4H.mjs} +6 -6
- package/dist/{hosted-native-CpDB3-PO.mjs → hosted-native-CDe33CSr.mjs} +22 -18
- package/dist/{idle-shutdown-FfxL1KdG.mjs → idle-shutdown-CQ0QwU9J.mjs} +7 -8
- package/dist/{ios-WxpDJ5T1.mjs → ios-B1DYuOjh.mjs} +27 -11
- package/dist/{ios-B7-lToDu.mjs → ios-B1HvafEG.mjs} +8 -8
- package/dist/{ios-ENC4sRPA.mjs → ios-CsRVik64.mjs} +7 -1
- package/dist/{ios-device-CsyryvpT.mjs → ios-device-B4elVFKi.mjs} +3 -3
- package/dist/{ios-device-B6HK951C.mjs → ios-device-CiBA-er9.mjs} +1 -1
- package/dist/{ios-project-Ty5pHl9O.mjs → ios-project-CR4mZ7CH.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-BPHqMDdZ.mjs} +1 -1
- package/dist/{ios-ftxdQbJS.mjs → ios-tg4tp3q1.mjs} +94 -37
- package/dist/{js-swap-BMhXYVDS.mjs → js-swap-CVpSW1fn.mjs} +8 -7
- package/dist/{launch-DOp8RjDz.mjs → launch-DoySmhjz.mjs} +42 -38
- package/dist/lightened-DFAJgfIk.mjs +171 -0
- package/dist/{logs-s1dRIG1E.mjs → logs-BGsW7iPe.mjs} +46 -21
- package/dist/{logs-dQDki6Tt.mjs → logs-DI5ILkYB.mjs} +7 -7
- package/dist/{macos-DBBPNnIA.mjs → macos-Bo9st9Di.mjs} +73 -38
- package/dist/{macos-B0VEV3Gf.mjs → macos-C12RMJQF.mjs} +1 -1
- package/dist/macos-run.mjs +1 -1
- package/dist/maintenance-run.mjs +7 -7
- package/dist/{metro-CjC10t6E.mjs → metro-BnhXV2Mj.mjs} +4 -4
- package/dist/{metro-gateway-B6_PgD0L.mjs → metro-request-DXyKjVDF.mjs} +26 -47
- package/dist/{named-ports-BG0j1X4g.mjs → named-ports-Phoi2POD.mjs} +3 -3
- package/dist/{native-android-CL2NGiik.mjs → native-android-vxgtLD7w.mjs} +17 -18
- package/dist/{native-gradle-artifact-inputs-btSUulme.mjs → native-gradle-artifact-inputs-BN96IVw8.mjs} +5 -5
- package/dist/{native-runtime-Dq3RBcON.mjs → native-runtime-DG4Rodzj.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-BKarCoKg.mjs} +3 -3
- package/dist/{native-xcode-ios-CBXJAxfC.mjs → native-xcode-ios-8A-efGa5.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-DJCq48IJ.mjs} +3 -3
- package/dist/{page-CzEA_-5_.mjs → page-CQJlcj3x.mjs} +2 -2
- package/dist/{phone-reverses-lmj6kcPm.mjs → phone-reverses-BzTa8g1F.mjs} +18 -4
- package/dist/{placement-log-DIFPQAXp.mjs → placement-log-Cvhta5ib.mjs} +3 -3
- package/dist/{plan-placement-IiiSQhtz.mjs → plan-placement-WkZdccuN.mjs} +36 -10
- package/dist/{ports-C2_w1Z0Z.mjs → ports-UV4mJgyC.mjs} +3 -3
- package/dist/{prebuild-gmouvmJZ.mjs → prebuild-k8d_tqOq.mjs} +16 -17
- package/dist/{project-BUu6Is4k.mjs → project-Sui52ywT.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-DANlGGQf.mjs} +32 -26
- package/dist/{react-native-build-oNwqF5Wb.mjs → react-native-build-jUHnIfZ9.mjs} +9 -9
- package/dist/{react-native-doctor-BhA3KUte.mjs → react-native-doctor-DCWRjS7K.mjs} +6 -6
- package/dist/{react-native-ios-B-nf4rjN.mjs → react-native-ios-BwMsFMXi.mjs} +23 -17
- package/dist/{recordings-BldzVKq0.mjs → recordings-iy0mkm7b.mjs} +1 -1
- package/dist/{registration-CN6nlhcD.mjs → registration-BwCe64PH.mjs} +1 -1
- package/dist/{reload-C9OduiNc.mjs → reload-Bj7QyuDy.mjs} +1 -1
- package/dist/{reload-CCR7KC_l.mjs → reload-c_uEyc_X.mjs} +16 -16
- package/dist/{remote-C8S7FlPF.mjs → remote-DFlpAqdV.mjs} +23 -10
- package/dist/{result-7F5hEwQJ.mjs → result-BIcXREyC.mjs} +32 -21
- package/dist/{run-BqB0v3pt.mjs → run-y2HwyNvg.mjs} +3 -3
- package/dist/{selection-BNPBZ6T5.mjs → selection-CXgM_VK_.mjs} +37 -29
- package/dist/{server-bare-A75rDIdD.mjs → server-bare-2N4vSRZG.mjs} +2 -2
- package/dist/{server-command-CssKFOD7.mjs → server-command-qneHPcRI.mjs} +6 -6
- package/dist/{server-expo-eQfOdydb.mjs → server-expo-Ccm9zB0o.mjs} +4 -4
- package/dist/server-expo-WulkyAwT.mjs +2 -0
- package/dist/{settings-CosW5X-S.mjs → settings-CY5JZlNN.mjs} +375 -62
- package/dist/{settings-OaGQF-ri.mjs → settings-Cfd_Q6ys.mjs} +24 -3
- package/dist/settings-payload-DwQiEMCn.mjs +257 -0
- package/dist/settings-write-CJpjkeAd.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-BpmanOnk.mjs} +6 -6
- package/dist/{stage-jKQ7CP_B.mjs → stage-CmugrSG5.mjs} +2 -2
- package/dist/{start-C_wefNKB.mjs → start-CHH3zgIU.mjs} +1 -1
- package/dist/{start-_BdJ32cZ.mjs → start-DYLk6GXn.mjs} +27 -22
- package/dist/{state-C1i2nMOP.mjs → state-B2QJbDwz.mjs} +4 -4
- package/dist/{state-C4OdpOTF.mjs → state-BXC1Lfxw.mjs} +2 -2
- package/dist/{stats-tEpEEH-u.mjs → stats-DxKqhcGp.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-BHCkOZga.mjs} +51 -28
- package/dist/{status-B9M7x4CZ.mjs → status-D_Ljc89p.mjs} +1 -1
- package/dist/{status-watch-B5wDIH_o.mjs → status-watch-BmsPqYSU.mjs} +18 -7
- package/dist/status-worker.mjs +1 -1
- package/dist/{stim-desktop-6Dqp-gjX.mjs → stim-desktop-CbWo21kb.mjs} +2 -2
- package/dist/{stop-DRqQVqDJ.mjs → stop-D-dGE9gh.mjs} +21 -20
- package/dist/{stop-ySNQHXMG.mjs → stop-DQvE-P76.mjs} +10 -6
- package/dist/{stop-CmdNhGlC.mjs → stop-b2pouxAO.mjs} +2 -2
- package/dist/supervisor-run.mjs +237 -63
- package/dist/{support-BGSCBoib.mjs → support-04IhBUTT.mjs} +6 -6
- package/dist/{support-CVOCp0Ft.mjs → support-BtlJKjyQ.mjs} +12 -11
- package/dist/{swiftpm-artifact-inputs-CfUzt_0Z.mjs → swiftpm-artifact-inputs-Cj2UpmPN.mjs} +4 -4
- package/dist/{swiftpm-macos-x6HPpl_2.mjs → swiftpm-macos-BhF2stxY.mjs} +11 -12
- package/dist/{toolchain-J4J8MrAr.mjs → toolchain-mLA_km-b.mjs} +321 -15
- package/dist/{toolchain-DoavrvIA.mjs → toolchain-qktiV8xc.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-B0Vpuvpl.mjs} +731 -727
- package/dist/{web-Bihbf7-u.mjs → web-BYmK0TjL.mjs} +9 -9
- package/dist/web-CrVLVQZY.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-kIZrdlSg.mjs} +3 -3
- package/dist/{workspaces-g2lhdBXF.mjs → workspaces-CmOcceDN.mjs} +1879 -74
- package/dist/{worktree-BKuklFh_.mjs → worktree-2dk5edza.mjs} +933 -534
- package/dist/worktree-nliAoHxB.mjs +2 -0
- 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
- package/dist/worktree-Bm4cb-nU.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 {
|
|
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-kIZrdlSg.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-C8R1KIvi.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 { Ct as gitCommonDirOnDisk, Dt as isPodInstallChurn, Et as hasUncommittedWorkAsync, Ft as matchWorktreeEntry, Gt as restoreFileAsync, Ht as resolveFullRef, It as nativeCanonicalPath, Jt as warmWorktreePaths, Kt as sourceCheckoutOf, Lt as podsOutOfSync, Mt as locallyKnownUpstream, Q as resolveSettings, Rt as readWorktreeExclude, St as gitCommonDirAsync, Tt as hasRemoteAsync, Ut as resolveFullRefAsync, V as readCommittedSettings, X as resolveProjectSettings, Y as resolveIosProjectDir, _ as inertSettingWarning, _t as deleteBranchAsync, a as SETTING_SHAPE_REMEDY, bt as dirtyPathsAsync, gt as cloneIgnoredEntries, ht as branchExistsAsync, jt as listWorktreesAsync, nn as moveIgnoredTreesToTrash, qt as unpushedCommitsAsync, rn as restoreTrashedTrees, rt as settingShapeErrors, st as unknownSettingKeys, tn as disposeTrash, vt as depsOutOfSync, wt as hasPopulatedSubmodulesAsync, zt as removeWorktreeAsync } from "./settings-CY5JZlNN.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-Sui52ywT.mjs";
|
|
8
|
+
import { t as macosAppPresent } from "./state-BXC1Lfxw.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-CsPs-7Mg.mjs";
|
|
11
|
+
import { c as pidExists, d as resolveProjectMetro, p as signalProcessTreeAsync, r as killMetroTreeAsync } from "./metro-BnhXV2Mj.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-Bm_A1xX9.mjs";
|
|
13
|
+
import { o as readWebRecord } from "./state-B2QJbDwz.mjs";
|
|
14
|
+
import { C as stopTunnel, E as withManagedRemoteWorktreeRemovalLock, O as withManagedTunnelRemovalLock, _ as emptyWorkspaceDir, ht as teardownOwnedIosSim, mt as teardownOwnedBrowser, o as collectWorkspaceOutputs, pt as teardownOwnedAvd, v as withIdleWorkspace, x as resolveSupervisorTarget } from "./workspaces-CmOcceDN.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-mLA_km-b.mjs";
|
|
17
|
+
import { D as releaseWorkspaceLeases, t as agentDeviceDaemon } from "./activity-DRMg4_mD.mjs";
|
|
18
|
+
import { R as readHostMemoryPressure, z as readHostMemoryPressureAsync } from "./ios-B1DYuOjh.mjs";
|
|
19
|
+
import { t as clearNamedPorts } from "./named-ports-Phoi2POD.mjs";
|
|
20
20
|
import { t as resolveMaintenanceSettings } from "./settings-Djl-mpjX.mjs";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import { r as
|
|
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-DlaKLbW0.mjs";
|
|
22
|
+
import { a as stopHostedIos, n as removePhoneReverses } from "./phone-reverses-BzTa8g1F.mjs";
|
|
23
|
+
import { r as stopMacosApp } from "./stop-DQvE-P76.mjs";
|
|
24
|
+
import { n as verifyCollectorOwnership } from "./ownership-DJCq48IJ.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-B0Vpuvpl.mjs";
|
|
27
|
+
import { r as workspaceRecordingEnabled } from "./recordings-iy0mkm7b.mjs";
|
|
28
|
+
import { i as installedNpmTreeProblems, n as hasInstalledDependencies, t as dependencyState } from "./dependency-state-CojA2o0t.mjs";
|
|
29
|
+
import { a as discoverXcodeProject } from "./react-native-build-jUHnIfZ9.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 }) => ({
|
|
@@ -2832,31 +3181,10 @@ function excludePodChurn(lines, iosDirs = []) {
|
|
|
2832
3181
|
restore
|
|
2833
3182
|
};
|
|
2834
3183
|
}
|
|
2835
|
-
function nativeCanonicalPath(path) {
|
|
2836
|
-
const missing = [];
|
|
2837
|
-
for (let existing = resolve$1(path);; existing = dirname$1(existing)) try {
|
|
2838
|
-
return resolve$1(realpathSync$1.native(existing), ...missing);
|
|
2839
|
-
} catch {
|
|
2840
|
-
if (dirname$1(existing) === existing) return resolve$1(path);
|
|
2841
|
-
missing.unshift(basename$1(existing));
|
|
2842
|
-
}
|
|
2843
|
-
}
|
|
2844
3184
|
/** Other worktrees git lists inside `path`, which removing `path` would delete with it. */
|
|
2845
3185
|
function nestedWorktrees(entries, path) {
|
|
2846
3186
|
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);
|
|
2848
|
-
}
|
|
2849
|
-
function matchWorktreeEntry(entries, path) {
|
|
2850
|
-
let best = null;
|
|
2851
|
-
const target = nativeCanonicalPath(path);
|
|
2852
|
-
(entries || []).forEach((entry, index) => {
|
|
2853
|
-
if (!entry?.path || !isPathPrefix(nativeCanonicalPath(entry.path), target)) return;
|
|
2854
|
-
if (!best || entry.path.length > best.path.length) best = {
|
|
2855
|
-
...entry,
|
|
2856
|
-
index
|
|
2857
|
-
};
|
|
2858
|
-
});
|
|
2859
|
-
return best;
|
|
3187
|
+
return entries.filter((candidate) => !candidate.bare && nativeCanonicalPath(candidate.path) !== root).filter((candidate) => isPathPrefix$1(root, nativeCanonicalPath(candidate.path))).map((candidate) => candidate.path);
|
|
2860
3188
|
}
|
|
2861
3189
|
function removalRemedy(dirtyLines, { worktree = "<worktree>" } = {}) {
|
|
2862
3190
|
const tracked = [];
|
|
@@ -2886,10 +3214,11 @@ function pathArgs(lines, limit = 5) {
|
|
|
2886
3214
|
const shown = paths.slice(0, limit).map((p) => /[\s"'\\$`]/.test(p) ? JSON.stringify(p) : p);
|
|
2887
3215
|
return `${shown.join(" ")}${paths.length > shown.length ? " ..." : ""}`;
|
|
2888
3216
|
}
|
|
3217
|
+
const DIRTY_BLOCKER = "uncommitted changes or untracked files";
|
|
2889
3218
|
function removalBlockers({ dirty, unpushed }) {
|
|
2890
3219
|
const blockers = [];
|
|
2891
3220
|
if (dirty === null || unpushed === null) blockers.push("could not determine git status; re-run with --force to override");
|
|
2892
|
-
if (dirty) blockers.push(
|
|
3221
|
+
if (dirty) blockers.push(DIRTY_BLOCKER);
|
|
2893
3222
|
if (unpushed && unpushed.length) blockers.push(`${plural(unpushed.length, "commit")} not on any remote or any other local branch`);
|
|
2894
3223
|
return blockers;
|
|
2895
3224
|
}
|
|
@@ -2901,7 +3230,7 @@ function reclaimKeys(rootPath) {
|
|
|
2901
3230
|
const keys = /* @__PURE__ */ new Set([rootPath]);
|
|
2902
3231
|
const root = nativeCanonicalPath(rootPath);
|
|
2903
3232
|
if (cfg?.projects) {
|
|
2904
|
-
for (const key of Object.keys(cfg.projects)) if (isPathPrefix(rootPath, key) || isPathPrefix(root, nativeCanonicalPath(key))) keys.add(key);
|
|
3233
|
+
for (const key of Object.keys(cfg.projects)) if (isPathPrefix$1(rootPath, key) || isPathPrefix$1(root, nativeCanonicalPath(key))) keys.add(key);
|
|
2905
3234
|
}
|
|
2906
3235
|
return [...keys].toSorted();
|
|
2907
3236
|
}
|
|
@@ -3042,11 +3371,11 @@ function reportRetainedResources(root, result, output) {
|
|
|
3042
3371
|
}
|
|
3043
3372
|
function hasRegisteredProjectUnder(rootPath) {
|
|
3044
3373
|
const cfg = loadConfig$1();
|
|
3045
|
-
return Object.keys(cfg?.projects ?? {}).some((key) => isPathPrefix(rootPath, key));
|
|
3374
|
+
return Object.keys(cfg?.projects ?? {}).some((key) => isPathPrefix$1(rootPath, key));
|
|
3046
3375
|
}
|
|
3047
3376
|
function projectAtOrAbove(root, path) {
|
|
3048
3377
|
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;
|
|
3378
|
+
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
3379
|
}
|
|
3051
3380
|
async function reclaimEnvironment(root, why, output, operation, beforeRemove) {
|
|
3052
3381
|
await withManagedRemoteWorktreeRemovalLock(root, () => withReclaimLocks(root, async (lockedKeys) => {
|
|
@@ -3080,6 +3409,9 @@ const REMOVAL_OUTPUT = {
|
|
|
3080
3409
|
process.exitCode = 1;
|
|
3081
3410
|
}
|
|
3082
3411
|
};
|
|
3412
|
+
function blockersOf(inspection, opts) {
|
|
3413
|
+
return opts.archiveDirty ? inspection.blockers.filter((blocker) => blocker !== DIRTY_BLOCKER) : inspection.blockers;
|
|
3414
|
+
}
|
|
3083
3415
|
function removalPath(target, cwd = process.cwd()) {
|
|
3084
3416
|
return canonicalExistingPath(target ?? cwd);
|
|
3085
3417
|
}
|
|
@@ -3186,6 +3518,25 @@ function printRemovalCleanup(result, failed, output) {
|
|
|
3186
3518
|
if (result.dereferenced.length) output.error(chalk.dim(phaseLine("workspace", `no longer referenced: ${result.dereferenced.join(", ")}`)));
|
|
3187
3519
|
for (const skipped of result.skippedDevices) output.error((failed ? chalk.dim : chalk.yellow)(phaseLine("device", `kept ${describeKeptDevice(skipped)} (${skipped.reason})`)));
|
|
3188
3520
|
}
|
|
3521
|
+
async function archiveBeforeRemoval(path, repository, branch, head, opts, output) {
|
|
3522
|
+
try {
|
|
3523
|
+
if (!head) throw new Error("its HEAD could not be read");
|
|
3524
|
+
return await archiveWorktreeChanges({
|
|
3525
|
+
path,
|
|
3526
|
+
repository,
|
|
3527
|
+
branch,
|
|
3528
|
+
head,
|
|
3529
|
+
pullRequest: opts.archiveDirty?.pullRequest ?? null,
|
|
3530
|
+
operation: opts.operation
|
|
3531
|
+
});
|
|
3532
|
+
} catch (error) {
|
|
3533
|
+
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
3534
|
+
opts.operation?.signal.throwIfAborted();
|
|
3535
|
+
output.error(chalk.red(`Refusing to remove ${path}: could not archive its uncommitted changes: ${(error instanceof Error ? error.message : String(error)).split("\n")[0]}`));
|
|
3536
|
+
output.fail();
|
|
3537
|
+
return null;
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3189
3540
|
async function removeWorktreeTarget(target, opts = {}) {
|
|
3190
3541
|
let removed = false;
|
|
3191
3542
|
await runRemove(target, opts, () => {
|
|
@@ -3337,6 +3688,13 @@ async function removeOrRefuse(target, opts, onRemoved, output) {
|
|
|
3337
3688
|
output.fail();
|
|
3338
3689
|
return;
|
|
3339
3690
|
}
|
|
3691
|
+
const cli = stimCliUseOf(path, nativeCanonicalPath, stimCliEntries(opts.operation?.env));
|
|
3692
|
+
if (cli.length) {
|
|
3693
|
+
output.error(chalk.red(`Refusing to remove ${path}: ${cli.join("; ")}.`));
|
|
3694
|
+
output.error(chalk.dim("Run stim from another checkout or install, or point PATH elsewhere, then retry."));
|
|
3695
|
+
output.fail();
|
|
3696
|
+
return;
|
|
3697
|
+
}
|
|
3340
3698
|
const project = getProject(path);
|
|
3341
3699
|
const branch = entry.branch;
|
|
3342
3700
|
const ownsBranch = Boolean(branch && project?.worktreeBranchOwned === true && project.worktreeBranch === branch);
|
|
@@ -3352,13 +3710,15 @@ async function removeOrRefuse(target, opts, onRemoved, output) {
|
|
|
3352
3710
|
output.error(chalk.dim(`HEAD is in the merged PR #${pr.number} (${pr.url}); its local-only commits stay on GitHub.`));
|
|
3353
3711
|
}
|
|
3354
3712
|
}
|
|
3355
|
-
if (inspection.
|
|
3713
|
+
if (blockersOf(inspection, opts).length && !opts.force) {
|
|
3356
3714
|
await printRemovalRefusal(path, inspection, output, opts.operation);
|
|
3357
3715
|
return;
|
|
3358
3716
|
}
|
|
3359
3717
|
const deleteOwnedBranch = Boolean(ownsBranch && approvedBranchSha && inspection.unpushed?.length === 0);
|
|
3360
3718
|
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
3719
|
const branchDeleteCwd = source.path;
|
|
3720
|
+
let archive = null;
|
|
3721
|
+
let gitRemoving = false;
|
|
3362
3722
|
await withManagedRemoteWorktreeRemovalLock(path, () => withReclaimLocks(path, async (lockedKeys) => {
|
|
3363
3723
|
options.signal?.throwIfAborted();
|
|
3364
3724
|
if ((await opts.guard?.(lockedKeys))?.length) return;
|
|
@@ -3370,16 +3730,25 @@ async function removeOrRefuse(target, opts, onRemoved, output) {
|
|
|
3370
3730
|
return;
|
|
3371
3731
|
}
|
|
3372
3732
|
const current = await inspectRemoval(path, mergedHead, opts.operation);
|
|
3373
|
-
if (current.
|
|
3733
|
+
if (blockersOf(current, opts).length && !opts.force) {
|
|
3374
3734
|
await printRemovalRefusal(path, current, output, opts.operation);
|
|
3375
3735
|
return;
|
|
3376
3736
|
}
|
|
3737
|
+
if (opts.archiveDirty && !opts.force && current.dirtyLines.length) {
|
|
3738
|
+
const saved = await archiveBeforeRemoval(path, source.path, branch ?? null, inspectedHead, opts, output);
|
|
3739
|
+
if (!saved) return;
|
|
3740
|
+
archive = saved;
|
|
3741
|
+
}
|
|
3742
|
+
const discardArchive = async () => {
|
|
3743
|
+
if (archive) await discardWorktreeArchive(archive);
|
|
3744
|
+
};
|
|
3377
3745
|
const result = await reclaimAll(path, lockedKeys, {
|
|
3378
3746
|
preserveRootProject: true,
|
|
3379
3747
|
removedBy: opts.removedBy ?? "worktree-remove",
|
|
3380
3748
|
operation: opts.operation
|
|
3381
3749
|
});
|
|
3382
3750
|
if (result.keptEntries.length) {
|
|
3751
|
+
await discardArchive();
|
|
3383
3752
|
reportRetainedResources(path, result, output);
|
|
3384
3753
|
return;
|
|
3385
3754
|
}
|
|
@@ -3390,12 +3759,14 @@ async function removeOrRefuse(target, opts, onRemoved, output) {
|
|
|
3390
3759
|
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
3391
3760
|
opts.operation?.signal.throwIfAborted();
|
|
3392
3761
|
if (!isTimeoutError(error)) throw error;
|
|
3762
|
+
await discardArchive();
|
|
3393
3763
|
await printRemovalRefusal(path, timedOutInspection(error), output, opts.operation);
|
|
3394
3764
|
output.error(chalk.dim(`The directory and Stim ownership record for ${path} were kept.`));
|
|
3395
3765
|
printRemovalCleanup(result, true, output);
|
|
3396
3766
|
return;
|
|
3397
3767
|
}
|
|
3398
3768
|
if (headMoved) {
|
|
3769
|
+
await discardArchive();
|
|
3399
3770
|
output.error(chalk.red(`Refusing to remove ${path}: its HEAD moved while its environment was reclaimed.`));
|
|
3400
3771
|
output.error(chalk.dim(`The directory and Stim ownership record for ${path} were kept.`));
|
|
3401
3772
|
printRemovalCleanup(result, true, output);
|
|
@@ -3410,10 +3781,28 @@ async function removeOrRefuse(target, opts, onRemoved, output) {
|
|
|
3410
3781
|
signal: options.signal,
|
|
3411
3782
|
declareFor: opts.operation
|
|
3412
3783
|
});
|
|
3784
|
+
let stale = null;
|
|
3785
|
+
if (archive) try {
|
|
3786
|
+
stale = await worktreeArchiveMismatch(path, archive, opts.operation);
|
|
3787
|
+
} catch (error) {
|
|
3788
|
+
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
3789
|
+
stale = `its archive could not be checked: ${error instanceof Error ? error.message : String(error)}`;
|
|
3790
|
+
}
|
|
3791
|
+
if (stale) {
|
|
3792
|
+
const unrestored = trash ? await restoreTrashedTrees(trash) : [];
|
|
3793
|
+
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.`));
|
|
3794
|
+
await discardArchive();
|
|
3795
|
+
output.error(chalk.red(`Refusing to remove ${path}: ${stale}.`));
|
|
3796
|
+
output.error(chalk.dim(`The directory and Stim ownership record for ${path} were kept.`));
|
|
3797
|
+
printRemovalCleanup(result, true, output);
|
|
3798
|
+
output.fail();
|
|
3799
|
+
return;
|
|
3800
|
+
}
|
|
3801
|
+
gitRemoving = true;
|
|
3413
3802
|
try {
|
|
3414
3803
|
await removeWorktreeAsync(path, {
|
|
3415
3804
|
from: source.path,
|
|
3416
|
-
force: opts.force,
|
|
3805
|
+
force: opts.force || archive !== null,
|
|
3417
3806
|
env: options.env,
|
|
3418
3807
|
signal: options.signal,
|
|
3419
3808
|
declareFor: opts.operation
|
|
@@ -3426,10 +3815,15 @@ async function removeOrRefuse(target, opts, onRemoved, output) {
|
|
|
3426
3815
|
output.error(chalk.red(`git worktree remove failed: ${message}`));
|
|
3427
3816
|
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
3817
|
output.error(chalk.dim(`The directory and Stim ownership record for ${path} were kept.`));
|
|
3818
|
+
if (archive) output.error(chalk.dim(`Its uncommitted changes stay archived in ${archive.dir}.`));
|
|
3429
3819
|
printRemovalCleanup(result, true, output);
|
|
3430
3820
|
output.fail();
|
|
3431
3821
|
return;
|
|
3432
3822
|
}
|
|
3823
|
+
if (archive) {
|
|
3824
|
+
opts.archiveDirty?.onArchived(archive);
|
|
3825
|
+
output.error(chalk.dim(phaseLine("archive", `saved ${plural(archive.tracked.length + archive.untracked.length, "uncommitted file")} to ${archive.dir}; restore: ${archive.restore.join(" && ")}`)));
|
|
3826
|
+
}
|
|
3433
3827
|
onRemoved();
|
|
3434
3828
|
try {
|
|
3435
3829
|
forgetStatusMeasures(path, result.reclaimedKeys);
|
|
@@ -3492,6 +3886,11 @@ async function removeOrRefuse(target, opts, onRemoved, output) {
|
|
|
3492
3886
|
}, opts.operation, opts.beforeRemove), {
|
|
3493
3887
|
signal: opts.operation?.signal,
|
|
3494
3888
|
declareSpawnsFor: opts.operation
|
|
3889
|
+
}).catch(async (error) => {
|
|
3890
|
+
if (archive && !gitRemoving) await discardWorktreeArchive(archive);
|
|
3891
|
+
throw error;
|
|
3892
|
+
}).finally(() => {
|
|
3893
|
+
if (archive) releaseWorktreeArchive(archive);
|
|
3495
3894
|
});
|
|
3496
3895
|
}
|
|
3497
3896
|
function registerRemove(worktree) {
|
|
@@ -3505,4 +3904,4 @@ function worktreeCommand(program) {
|
|
|
3505
3904
|
registerRemove(worktree);
|
|
3506
3905
|
}
|
|
3507
3906
|
//#endregion
|
|
3508
|
-
export {
|
|
3907
|
+
export { deleteSelectedArchives as A, sizeScanDeferred as C, stimCliUseOf as D, stimCliEntries as E, worktreeArchiveLimits as M, describeDereferenced as O, measureSizes as S, serviceRunningFrom as T, cacheBlocked as _, nestedWorktrees as a, measurePressure as b, registerRemove as c, removalPath as d, removalRemedy as f, worktreeCommand as g, warmedWorkspace as h, excludeWatchmanCookies as i, sweepArchiveStaging as j, reclaimProject as k, registerWarm as l, warmWorktreeTarget as m, dependencyInstallCommand as n, porcelainPath as o, removeWorktreeTarget as p, excludePodChurn as r, reclaimKeys as s, customIosDirs as t, removalBlockers as u, measureDisk as v, uncommittedActivityOf as w, measurePressureAsync as x, measureDiskAsync as y };
|