stim 1.25.0 → 1.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{acquire-artifact-zA4S7RHE.mjs → acquire-artifact-BKN2Yuhg.mjs} +2 -2
- package/dist/{act-BbHS3oV8.mjs → act-ROviTF_t.mjs} +415 -118
- package/dist/{activity-BpjwrQXU.mjs → activity-Cd-ysFes.mjs} +6 -7
- package/dist/{agent-device-usage-output-Bnkerfod.mjs → agent-device-usage-output-C5nCKWp9.mjs} +5 -5
- package/dist/{android-CNZgRC-4.mjs → android--HBgpvpW.mjs} +5 -5
- package/dist/{android-BGwuK3Mc.mjs → android-Cd6w7_qw.mjs} +134 -57
- package/dist/{android-saBeB4lr.mjs → android-Deb9X3_Y.mjs} +37 -15
- package/dist/{android-Cjcj8MCw.mjs → android-DriNoAVy.mjs} +99 -47
- package/dist/{android-cas-BVqh2Zep.mjs → android-cas-D5e6ecUg.mjs} +4 -4
- package/dist/android-cas-compiler.mjs +1 -1
- package/dist/{android-project-CTYaETc0.d.mts → android-project-DUGMbq28.d.mts} +55 -1
- package/dist/api.d.mts +20 -7
- package/dist/api.mjs +42 -32
- package/dist/apk-signed-tail-BHQHx3Rm.mjs +142 -0
- package/dist/{artifact-Co8zixVx.mjs → artifact-0AGJYB9O.mjs} +28 -28
- package/dist/{artifact-DN8JNwqS.mjs → artifact-faemIZv5.mjs} +40 -30
- package/dist/{artifact-platform-CMRc0M_r.mjs → artifact-platform-BO2Uvk_A.mjs} +1 -1
- package/dist/{attempt-DW-mQqM7.mjs → attempt-ClyJlZPT.mjs} +4 -2
- package/dist/auto-placement-z-H5pJza.mjs +350 -0
- package/dist/{browser-web-CCgZsHmI.mjs → browser-web-slD8AvLp.mjs} +16 -17
- package/dist/{build-cache-CKCzxzxA.mjs → build-cache-gBJ-WKzA.mjs} +141 -37
- package/dist/{build-CAiw8Ezm.mjs → build-hS0QyFlQ.mjs} +38 -31
- package/dist/{build-plan-zCuHGlA8.mjs → build-plan-CBqYbzYM.mjs} +7 -6
- package/dist/{cache-manifest-BPXRONJz.mjs → cache-manifest-DEjqGGat.mjs} +1 -1
- package/dist/cache-manifest.mjs +1 -1
- package/dist/{budget-HyShrIpL.mjs → ccache-BWTze1Ls.mjs} +634 -1514
- package/dist/{cdp-BG3FJCCx.mjs → cdp-C3svvXiE.mjs} +1 -1
- package/dist/{chrome-BvV8ssI3.mjs → chrome-DyDWurWG.mjs} +1 -1
- package/dist/{cli-C4w607Pz.mjs → cli-ftXDLto1.mjs} +24 -23
- package/dist/cli.mjs +1 -1
- package/dist/{client-BQsmyp_-.mjs → client-xsdxrhWA.mjs} +77 -116
- package/dist/collector-run.d.mts +10 -9
- package/dist/collector-run.mjs +42 -27
- package/dist/{command-output-BBvS1H6v.mjs → command-output-Dj-WT4BP.mjs} +1 -1
- package/dist/{config-BZhXY15H.mjs → config-Cn94Jc0g.mjs} +343 -18
- package/dist/{launch-DscF1QfM.mjs → dependencies-BGokZ-Ng.mjs} +330 -279
- package/dist/{dependency-state-BwNCYG5Q.mjs → dependency-state-C3OtgG94.mjs} +14 -7
- package/dist/{detached-entry-KmieOKgN.mjs → detached-entry-BPlSaCwC.mjs} +1 -1
- package/dist/{dev-client-CHoXXZeg.mjs → dev-client-BMca82xo.mjs} +2 -2
- package/dist/{device-F5-4z9ef.mjs → device-BJm0irrq.mjs} +16 -16
- package/dist/{device-capacity-ZYaRALv5.mjs → device-capacity-6LP7QsaA.mjs} +103 -25
- package/dist/device-host-worker.mjs +19 -21
- package/dist/{device-ios-DkHBrmvH.mjs → device-ios-534c14fz.mjs} +288 -28
- package/dist/{device-lease-run-ogEeJ62K.mjs → device-lease-run-DwsGfjUN.mjs} +5 -5
- package/dist/{device-pool-BIg8MU8l.mjs → device-pool-BnQqvs6o.mjs} +3 -3
- package/dist/{device-B9FqTBYW.mjs → device-tM41-TUq.mjs} +25 -24
- package/dist/{doctor-A_Nr_fDe.mjs → doctor-Dbx1ZpD2.mjs} +1 -1
- package/dist/{doctor-k3P3PizH.mjs → doctor-DyqE9n1J.mjs} +28 -16
- package/dist/{doctor-Bm8TVpGn.mjs → doctor-F-3VUUyN.mjs} +128 -35
- package/dist/{doctor-fingerprint-zlujA1Gs.mjs → doctor-fingerprint-o7ZI9YLn.mjs} +9 -3
- package/dist/doctor-fingerprint.mjs +1 -1
- package/dist/{doctor-watchman-BovI1y3-.mjs → doctor-watchman-CxDbyrrt.mjs} +5 -5
- package/dist/{eas-build-CyDKNTBP.mjs → eas-build-CjNSWmjo.mjs} +4 -4
- package/dist/eas-fallback-9fqb46kW.mjs +87 -0
- package/dist/{eas-session-ledger-BMCA1gdO.mjs → eas-session-ledger-DrP4OuuM.mjs} +6 -6
- package/dist/{error-diagnostics-B1Mj_-7h.mjs → error-diagnostics-snej9k4j.mjs} +51 -25
- package/dist/{exec-VFRpu1iA.mjs → exec-aULwtNly.mjs} +57 -6
- package/dist/{gc-eaz403W6.mjs → gc-BRwAY3qT.mjs} +2 -2
- package/dist/{gc-B9jcnmIk.mjs → gc-CGZ1Yg88.mjs} +218 -72
- package/dist/{guide-DR0K0xW9.mjs → guide-_rfEh_zM.mjs} +839 -222
- package/dist/{hosted-android-Bvb2-ZxD.mjs → hosted-android-BMKzUNnY.mjs} +4 -4
- package/dist/{hosted-client-rsRqysJc.mjs → hosted-client-D7Pk_Oo1.mjs} +13 -9
- package/dist/{hosted-logs-CWKGYWNX.mjs → hosted-logs-B1gqQuva.mjs} +18 -12
- package/dist/hosted-logs-sync-Dg3qrZmV.mjs +195 -0
- package/dist/{hosted-macos-d0pv9Tth.mjs → hosted-macos-B9w4H8hc.mjs} +6 -6
- package/dist/{hosted-native-BxTizGU2.mjs → hosted-native-CdgaiuyP.mjs} +17 -17
- package/dist/{idle-shutdown-BIyfl0Zj.mjs → idle-shutdown-Cbvo5KEh.mjs} +21 -9
- package/dist/{ios-BRVM8b0P.mjs → ios-B5HlVMhQ.mjs} +138 -55
- package/dist/{ios-Dvt97HgQ.mjs → ios-Bk4mWbtS.mjs} +8 -8
- package/dist/{ios-ENC4sRPA.mjs → ios-CsRVik64.mjs} +7 -1
- package/dist/{ios-BQzvJlzw.mjs → ios-E26qSpkb.mjs} +94 -11
- package/dist/{ios-device-CKSvk0bu.mjs → ios-device-C_QkIf1Z.mjs} +3 -3
- package/dist/{ios-device-B6HK951C.mjs → ios-device-CiBA-er9.mjs} +1 -1
- package/dist/{ios-project-DCUDo06I.mjs → ios-project-D8Jcy_br.mjs} +22 -9
- package/dist/{ios-signing-4c_qXvQm.mjs → ios-signing-BJzvDPfU.mjs} +2 -2
- package/dist/{ios-state-CT9nV5SP.mjs → ios-state-BZ03NFgF.mjs} +1 -1
- package/dist/{js-swap-Z9HbC4cj.mjs → js-swap-Dg2a945i.mjs} +8 -7
- package/dist/{launch-D0lPvFFu.mjs → launch-CRyBeNsA.mjs} +53 -44
- package/dist/lightened-BCsdAGj_.mjs +171 -0
- package/dist/{logs-BsIRre8x.mjs → logs-CC_-ZzEE.mjs} +10 -174
- package/dist/{logs-Crs9y34Z.mjs → logs-aJQb-RDg.mjs} +112 -38
- package/dist/{macos-CcUlJPMu.mjs → macos-BGmTpnv9.mjs} +1 -1
- package/dist/{macos-D477nfzH.mjs → macos-DlCBhMw5.mjs} +73 -39
- package/dist/macos-run.mjs +1 -1
- package/dist/maintenance-run.mjs +30 -13
- package/dist/{metro-1pJgCX4U.mjs → metro-CvyD-xwe.mjs} +4 -4
- package/dist/{metro-gateway-Bp9gJQAN.mjs → metro-gateway-BnHdqMO_.mjs} +3 -4
- package/dist/{named-ports-BVbn8SvC.mjs → named-ports-BCyMWA6b.mjs} +3 -3
- package/dist/{native-android-BvMizJn1.mjs → native-android-DlpxCyOP.mjs} +17 -18
- package/dist/{native-gradle-artifact-inputs-BJscK4vN.mjs → native-gradle-artifact-inputs-C13qT7R_.mjs} +5 -5
- package/dist/{native-runtime-B9w_Qyz3.mjs → native-runtime-DqaIBO5w.mjs} +9 -9
- package/dist/{native-source-D-4Dh4JU.mjs → native-source-D1AzWiuP.mjs} +1 -1
- package/dist/{native-xcode-inputs-Cv4l-obn.mjs → native-xcode-inputs-B1W8SrxB.mjs} +3 -3
- package/dist/{native-xcode-ios-DGrg90hE.mjs → native-xcode-ios-QPP0Rm68.mjs} +17 -18
- package/dist/{ndjson-CgE_o3Dl.mjs → ndjson-Dy2W4vRa.mjs} +2 -2
- package/dist/offload-worker.d.mts +3 -1
- package/dist/offload-worker.mjs +34 -25
- package/dist/{operation-exec-CUZifp6Z.mjs → operation-exec-Dp4FuIGR.mjs} +1 -1
- package/dist/{ownership-jxxz-19m.mjs → ownership-Bx-0cku-.mjs} +3 -3
- package/dist/{page-CzEA_-5_.mjs → page-CQJlcj3x.mjs} +2 -2
- package/dist/{phone-reverses-CYb7RRAx.mjs → phone-reverses-C9yAaKEv.mjs} +18 -4
- package/dist/{placement-log-DjOXJRIH.mjs → placement-log-BXUbiZM6.mjs} +3 -3
- package/dist/{plan-placement-BqKgKHCi.mjs → plan-placement-UxnQjv5Z.mjs} +36 -10
- package/dist/{ports-DtS1Amb0.mjs → ports-uUrghqWN.mjs} +3 -3
- package/dist/prebuild-BDpToDgR.mjs +1246 -0
- package/dist/{project-BiC68mIp.mjs → project-DYj-HVx6.mjs} +17 -18
- package/dist/{pull-request-CbNlkFok.mjs → pull-request-BzLmzAIQ.mjs} +2 -2
- package/dist/pull-requests.mjs +1 -1
- package/dist/{react-native-android-DRYqoJc4.mjs → react-native-android-DDBccgJw.mjs} +56 -36
- package/dist/{react-native-build-CwZB-agu.mjs → react-native-build-DaK2IFyP.mjs} +9 -9
- package/dist/{react-native-doctor-BCrytK7h.mjs → react-native-doctor-B-Mw6DcR.mjs} +50 -9
- package/dist/{react-native-ios-Cr1buNFo.mjs → react-native-ios-DjYwUF9U.mjs} +45 -25
- package/dist/{recordings-BaoQ0t-J.mjs → recordings-C20iUZfp.mjs} +1 -1
- package/dist/{registration-CN6nlhcD.mjs → registration-BwCe64PH.mjs} +1 -1
- package/dist/{reload-Beftw2X3.mjs → reload-DJBeF_rC.mjs} +1 -1
- package/dist/{reload-FbSgMF8x.mjs → reload-fUdkH2_p.mjs} +16 -16
- package/dist/{remote-BuZj6AyU.mjs → remote-y6n99WMW.mjs} +22 -9
- package/dist/{result-CR2vk2cj.mjs → result-CTqBw2Te.mjs} +32 -21
- package/dist/{run-BtzE09Dl.mjs → run-DaCeNKRm.mjs} +3 -3
- package/dist/{selection-D5cxEl3t.mjs → selection-Bs8phwhN.mjs} +183 -62
- package/dist/{server-bare-CH9X0RLn.mjs → server-bare-BOh-iZEj.mjs} +2 -2
- package/dist/{server-command-CaBbDNU2.mjs → server-command-8CuA51Cv.mjs} +6 -6
- package/dist/{server-expo-Ck8EIidM.mjs → server-expo-ABQs2XFA.mjs} +4 -4
- package/dist/server-expo-Ba_Y21vB.mjs +2 -0
- package/dist/{settings-B5tCoq9N.mjs → settings-BDVI8Ivc.mjs} +331 -62
- package/dist/{settings-DzH9mGQZ.mjs → settings-CMHhQNwI.mjs} +24 -3
- package/dist/settings-payload-cm60Zhib.mjs +257 -0
- package/dist/settings-write-BdNRLlBn.mjs +185 -0
- package/dist/settings.schema.json +84 -39
- package/dist/{simslim-BjxszUAb.mjs → simslim-DCQwzoPy.mjs} +4 -3
- package/dist/{slot-launch-QeuJMlnJ.mjs → slot-launch-0l7RqSpr.mjs} +6 -6
- package/dist/{stage-D8xlRU2D.mjs → stage-F4cAfJGi.mjs} +2 -2
- package/dist/{start-C1JLjkNw.mjs → start-DcqnF9p8.mjs} +1 -1
- package/dist/{start-YMpNwLhh.mjs → start-DrPwER_2.mjs} +27 -23
- package/dist/{state-DMRNP7-4.mjs → state-BX1y5syV.mjs} +4 -5
- package/dist/{state-DPaM3u5K.mjs → state-CZxLyetD.mjs} +2 -3
- package/dist/{stats-CP7STzZw.mjs → stats-GxuCbles.mjs} +5 -5
- package/dist/{stats-report-n2U5plhk.mjs → stats-report-B86Cv7Uc.mjs} +1 -1
- package/dist/{status-Fect2w9R.mjs → status-7ULDEJ1e.mjs} +1 -1
- package/dist/{status-CRLiJ-O_.mjs → status-BfnK9pUd.mjs} +25 -26
- package/dist/{status-cNw6i0_t.mjs → status-Bj7ZYvvc.mjs} +1 -1
- package/dist/{status-watch-Bc2J4TZn.mjs → status-watch-BfabQn03.mjs} +5 -5
- package/dist/status-worker.mjs +1 -1
- package/dist/{stim-desktop-DS0mzR8U.mjs → stim-desktop-CbWo21kb.mjs} +2 -2
- package/dist/{stop-COP6PQqw.mjs → stop-CH79S3IZ.mjs} +5 -6
- package/dist/{stop-Db9Kh-UA.mjs → stop-D-EAr25B.mjs} +25 -22
- package/dist/{stop-Br7LKxBE.mjs → stop-x1Yu3He4.mjs} +2 -2
- package/dist/supervisor-run.d.mts +1 -0
- package/dist/supervisor-run.mjs +17 -18
- package/dist/{support-qKcbgmI5.mjs → support-Be0UeVZU.mjs} +10 -10
- package/dist/{support-Cxj9CmII.mjs → support-CNg_YsOk.mjs} +6 -6
- package/dist/{swiftpm-artifact-inputs-iY9fsmOc.mjs → swiftpm-artifact-inputs-uPqYvGBv.mjs} +4 -4
- package/dist/{swiftpm-macos-Bp_sn9P4.mjs → swiftpm-macos-Ch4YjY_A.mjs} +11 -12
- package/dist/toolchain-CbCG6C5Z.mjs +2413 -0
- package/dist/toolchain-JYAOquPg.mjs +2 -0
- package/dist/{trigger-CBMe2YLH.mjs → trigger-DMou-X7j.mjs} +3 -3
- package/dist/trigger-qp2ji5wE.mjs +2 -0
- package/dist/{warm-progress-BV3ZPmcY.mjs → warm-progress-C0RsbngN.mjs} +731 -728
- package/dist/{web-B4Yjyt1G.mjs → web-Cq_AQ4DK.mjs} +9 -10
- package/dist/web-Je6abcpz.mjs +2 -0
- package/dist/web-run.mjs +9 -9
- package/dist/windows-report.mjs +1 -1
- package/dist/{workspace-state-CPSp3mav.mjs → workspace-state-BjqdjnJR.mjs} +46 -8
- package/dist/{workspaces-C9w4aeXF.mjs → workspaces-DFbj07jp.mjs} +1801 -76
- package/dist/{worktree-CclFIe1Z.mjs → worktree-COAJ3-OD.mjs} +934 -511
- package/dist/{worktree-Ct9Gk3KL.mjs → worktree-JGUd_53L.mjs} +1 -1
- package/package.json +8 -8
- package/dist/ccache-h1FKhZeg.mjs +0 -143
- package/dist/created-devices-D2Aca-xu.mjs +0 -52
- package/dist/eas-fallback-gaZi-dJL.mjs +0 -381
- package/dist/hosted-ios-RtVcJH3u.mjs +0 -15
- package/dist/native-run-D5OJsGwA.mjs +0 -276
- package/dist/paths-DQtTgsBf.mjs +0 -42
- package/dist/prebuild-JUaqOLpc.mjs +0 -501
- package/dist/server-expo-C16Zf2k7.mjs +0 -2
- package/dist/settings-write-n0zwfohx.mjs +0 -360
- package/dist/toolchain-DB1aeqxO.mjs +0 -2
- package/dist/toolchain-VzLJzZ7O.mjs +0 -908
- package/dist/trigger-BLsHgAGR.mjs +0 -2
- package/dist/web-DS4UDWvf.mjs +0 -2
- package/dist/xcode-CiIIDnuD.mjs +0 -910
|
@@ -1,211 +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 {
|
|
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 { a as
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { o as readWebRecord } from "./state-
|
|
14
|
-
import {
|
|
1
|
+
import { D as ownership_claim_exports, S as process_identity_exports, b as operationState, l as WorkspaceProcessCleanupError, m as declareSpawnsOn, n as isTimeoutError, t as getExecutor, v as stopDeclaringSpawnsOn } from "./exec-aULwtNly.mjs";
|
|
2
|
+
import { E as workspaceDir, a as recordWorkspaceUseAsync, l as withWorkspaceStateLockAsync, r as readWorkspaceState$1, w as workspaceAgentDeviceDir } from "./workspace-state-BjqdjnJR.mjs";
|
|
3
|
+
import { E as upsertProject, G as projectDeviceSlots, L as measuredDirectorySizeAsync, O as withConfigLockAsync$1, S as removeProject, _ as loadConfig$1, a as clearDevice, d as getConfigDir, g as isPathPrefix$1, j as acquireAvdClaim, p as getProject } from "./config-Cn94Jc0g.mjs";
|
|
4
|
+
import { a as formatElapsed, h as shortUdid, i as formatDuration, l as phaseLine, p as releasedLeaseFact, u as plural } from "./command-output-Dj-WT4BP.mjs";
|
|
5
|
+
import { c as gitQuiet, l as gitQuietAsync, o as git, r as childEnvironment, s as gitAsync, t as runOperationFile } from "./operation-exec-Dp4FuIGR.mjs";
|
|
6
|
+
import { At as listWorktreesAsync, Ct as hasPopulatedSubmodulesAsync, Et as isPodInstallChurn, Ft as removeWorktreeAsync, Ht as sourceCheckoutOf, Nt as podsOutOfSync, Pt as readWorktreeExclude, Q as resolveSettings, Rt as resolveFullRef, St as gitCommonDirOnDisk, Tt as hasUncommittedWorkAsync, Ut as unpushedCommitsAsync, V as readCommittedSettings, Vt as restoreFileAsync, Wt as warmWorktreePaths, X as resolveProjectSettings, Xt as moveIgnoredTreesToTrash, Y as resolveIosProjectDir, Yt as disposeTrash, Zt as restoreTrashedTrees, _ as inertSettingWarning, _t as depsOutOfSync, a as SETTING_SHAPE_REMEDY, gt as deleteBranchAsync, ht as cloneIgnoredEntries, jt as locallyKnownUpstream, mt as branchExistsAsync, rt as settingShapeErrors, st as unknownSettingKeys, wt as hasRemoteAsync, xt as gitCommonDirAsync, yt as dirtyPathsAsync, zt as resolveFullRefAsync } from "./settings-BDVI8Ivc.mjs";
|
|
7
|
+
import { F as readRemoteSessionId, M as readMetroTunnel, O as clearManagedMetroTunnel, P as readRemoteSession, T as MODE_COMMAND, i as findProjectRoot$1, k as clearRemoteSession, o as isMobileProject } from "./project-DYj-HVx6.mjs";
|
|
8
|
+
import { t as macosAppPresent } from "./state-CZxLyetD.mjs";
|
|
9
|
+
import { a as workspaceLinkLine, o as workspaceLinks } from "./stim-desktop-CbWo21kb.mjs";
|
|
10
|
+
import { ft as POOL_SETTING_REMEDY, vt as parkedMaxSetting } from "./android-DriNoAVy.mjs";
|
|
11
|
+
import { c as pidExists, d as resolveProjectMetro, p as signalProcessTreeAsync, r as killMetroTreeAsync } from "./metro-CvyD-xwe.mjs";
|
|
12
|
+
import { A as diskPressureLevel, D as requestDevServerStop, En as runProcessOperation, G as planCacheEmptying, Ln as resolveEasCliBin, N as lowestFree, Ot as endRecordedSession, _ as resolveBudget, g as readVolumeSpaceAsync, h as readVolumeSpace, k as withdrawDevServerStopRequest, p as machineNumber, ur as discoverCaches } from "./ccache-BWTze1Ls.mjs";
|
|
13
|
+
import { o as readWebRecord } from "./state-BX1y5syV.mjs";
|
|
14
|
+
import { D as withManagedTunnelRemovalLock, S as stopTunnel, T as withManagedRemoteWorktreeRemovalLock, _ as withIdleWorkspace, b as resolveSupervisorTarget, dt as teardownOwnedBrowser, ft as teardownOwnedIosSim, g as emptyWorkspaceDir, o as collectWorkspaceOutputs, ut as teardownOwnedAvd } from "./workspaces-DFbj07jp.mjs";
|
|
15
15
|
import { t as canonicalPath } from "./paths-BsJMoPP2.mjs";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import { t as clearNamedPorts } from "./named-ports-BVbn8SvC.mjs";
|
|
21
|
-
import { r as HEARTBEAT_INTERVAL_MS, x as podInstallCommand } from "./xcode-CiIIDnuD.mjs";
|
|
16
|
+
import { D as runCaptured, O as runPodInstall, S as DEPS_ERROR, T as readPodState, Y as podInstallCommand, ct as cacheUser, j as HEARTBEAT_INTERVAL_MS, w as podsAreStale } from "./toolchain-CbCG6C5Z.mjs";
|
|
17
|
+
import { D as releaseWorkspaceLeases, t as agentDeviceDaemon } from "./activity-Cd-ysFes.mjs";
|
|
18
|
+
import { R as readHostMemoryPressure, z as readHostMemoryPressureAsync } from "./ios-E26qSpkb.mjs";
|
|
19
|
+
import { t as clearNamedPorts } from "./named-ports-BCyMWA6b.mjs";
|
|
22
20
|
import { t as resolveMaintenanceSettings } from "./settings-Djl-mpjX.mjs";
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import { n as
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import { a as
|
|
32
|
-
import { o as pullRequestLookupAsync, r as endedPullRequest } from "./pull-request-CbNlkFok.mjs";
|
|
33
|
-
import { r as workspaceRecordingEnabled } from "./recordings-BaoQ0t-J.mjs";
|
|
21
|
+
import { i as stopHostedAndroid } from "./hosted-android-BMKzUNnY.mjs";
|
|
22
|
+
import { a as stopHostedIos, n as removePhoneReverses } from "./phone-reverses-C9yAaKEv.mjs";
|
|
23
|
+
import { n as stopMacosApp } from "./stop-CH79S3IZ.mjs";
|
|
24
|
+
import { n as verifyCollectorOwnership } from "./ownership-Bx-0cku-.mjs";
|
|
25
|
+
import { o as pullRequestLookupAsync, r as endedPullRequest } from "./pull-request-BzLmzAIQ.mjs";
|
|
26
|
+
import { c as mergeStateAsync, d as forgetStatusMeasures, t as startWarmProgress } from "./warm-progress-C0RsbngN.mjs";
|
|
27
|
+
import { r as workspaceRecordingEnabled } from "./recordings-C20iUZfp.mjs";
|
|
28
|
+
import { i as installedNpmTreeProblems, n as hasInstalledDependencies, t as dependencyState } from "./dependency-state-C3OtgG94.mjs";
|
|
29
|
+
import { a as discoverXcodeProject } from "./react-native-build-DaK2IFyP.mjs";
|
|
34
30
|
import { claimRemoveCommand, clearFreeClaimSet, readClaimSet, releaseClaim, tryAcquireClaim } from "@stim-cli/core/ownership-claim";
|
|
35
31
|
import { createHash, randomUUID } from "node:crypto";
|
|
36
32
|
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
37
33
|
import { cpus, freemem, homedir, loadavg, totalmem } from "node:os";
|
|
38
34
|
import { basename, dirname, join, relative } from "node:path";
|
|
39
|
-
import { buildCacheRoot, configDir, metroCacheRoot, unitLines, unitWords, withDirLockTransaction, workspaceName } from "@stim-cli/core";
|
|
40
|
-
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";
|
|
41
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";
|
|
42
38
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
43
|
-
import { captureProcessIdentity } from "@stim-cli/core/process-identity";
|
|
44
39
|
import chalk from "chalk";
|
|
45
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";
|
|
46
|
-
import { chmod, lstat, mkdir,
|
|
41
|
+
import { chmod, lstat, mkdir, readdir, rename, rm, rmdir, stat, utimes, writeFile } from "node:fs/promises";
|
|
47
42
|
import { createHash as createHash$1 } from "crypto";
|
|
48
|
-
//#region src/maintenance/pressure.ts
|
|
49
|
-
/**
|
|
50
|
-
* 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
|
|
51
|
-
* under the budget floor, and stops once free disk is back above it. Its urgent threshold only sets the reported level.
|
|
52
|
-
*/
|
|
53
|
-
function levelFloors(budget, settings) {
|
|
54
|
-
const floor = Math.max(budget.minFreeDiskMb, budget.hardFloorDiskMb);
|
|
55
|
-
return {
|
|
56
|
-
1: floor,
|
|
57
|
-
2: floor,
|
|
58
|
-
3: Math.max(settings.criticalFreeDiskGb * 1024, budget.hardFloorDiskMb),
|
|
59
|
-
urgent: settings.urgentFreeDiskGb * 1024
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
function diskPressureLevel(freeMb, budget, settings) {
|
|
63
|
-
const floors = levelFloors(budget, settings);
|
|
64
|
-
if (freeMb < floors[3]) return 3;
|
|
65
|
-
if (freeMb < floors.urgent) return 2;
|
|
66
|
-
return freeMb < floors[1] ? 1 : 0;
|
|
67
|
-
}
|
|
68
|
-
function lowestFree(disk) {
|
|
69
|
-
return disk.length ? Math.min(...disk.map((volume) => volume.freeMb)) : Infinity;
|
|
70
|
-
}
|
|
71
|
-
/** Whether an action planned at a pressure level may still run with this much free disk. */
|
|
72
|
-
function levelStillShort(action, freeMb, budget, settings) {
|
|
73
|
-
return action.level === void 0 || freeMb < levelFloors(budget, settings)[action.level];
|
|
74
|
-
}
|
|
75
|
-
/** The protection window in milliseconds: protectRecentHours, or maintenance.pressureProtectMinutes for actions of level 2 and 3. */
|
|
76
|
-
function protectWindowMs(action, settings) {
|
|
77
|
-
return action.level === void 0 ? settings.protectRecentHours * 36e5 : settings.pressureProtectMinutes * 6e4;
|
|
78
|
-
}
|
|
79
|
-
//#endregion
|
|
80
|
-
//#region src/maintenance/measure.ts
|
|
81
|
-
function measureDisk() {
|
|
82
|
-
return readVolumeSpace([
|
|
83
|
-
configDir(),
|
|
84
|
-
...Object.keys(loadConfig()?.projects ?? {}),
|
|
85
|
-
buildWorkerRoot(loadConfig())
|
|
86
|
-
]);
|
|
87
|
-
}
|
|
88
|
-
function measureDiskAsync(operation) {
|
|
89
|
-
return readVolumeSpaceAsync([
|
|
90
|
-
configDir(),
|
|
91
|
-
...Object.keys(loadConfig()?.projects ?? {}),
|
|
92
|
-
buildWorkerRoot(loadConfig())
|
|
93
|
-
], operation.signal);
|
|
94
|
-
}
|
|
95
|
-
/** The disk pressure level of the fullest volume Stim writes to, measured now. */
|
|
96
|
-
function currentDiskLevel(disk = measureDisk()) {
|
|
97
|
-
return diskPressureLevel(lowestFree(disk), resolveBudget().budget, resolveMaintenanceSettings());
|
|
98
|
-
}
|
|
99
|
-
function measurePressure(settings, previous, now) {
|
|
100
|
-
return pressureFromObservations(settings, previous, now, readHostMemoryPressure(), measureDisk());
|
|
101
|
-
}
|
|
102
|
-
async function measurePressureAsync(settings, previous, now, operation) {
|
|
103
|
-
return pressureFromObservations(settings, previous, now, await readHostMemoryPressureAsync(void 0, void 0, operation.signal, operation), await readVolumeSpaceAsync([
|
|
104
|
-
configDir(),
|
|
105
|
-
...Object.keys(loadConfig()?.projects ?? {}),
|
|
106
|
-
buildWorkerRoot(loadConfig())
|
|
107
|
-
], operation.signal));
|
|
108
|
-
}
|
|
109
|
-
function pressureFromObservations(settings, previous, now, level, disk) {
|
|
110
|
-
const availableBytes = process.platform === "darwin" ? null : freemem();
|
|
111
|
-
const warningSince = level === "warning" ? previous?.warningSince ?? now : null;
|
|
112
|
-
return {
|
|
113
|
-
disk,
|
|
114
|
-
memory: {
|
|
115
|
-
level,
|
|
116
|
-
availableBytes,
|
|
117
|
-
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))
|
|
118
|
-
},
|
|
119
|
-
warningSince
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
function sizeScanDeferred(settings) {
|
|
123
|
-
return loadavg()[0] / Math.max(1, cpus().length) > settings.maxLoadPerCore;
|
|
124
|
-
}
|
|
125
|
-
function cacheBlocked(entry, discovered = discoverCaches(), env = process.env) {
|
|
126
|
-
return planCacheEmptying([{
|
|
127
|
-
...entry,
|
|
128
|
-
note: "",
|
|
129
|
-
prune: "entries",
|
|
130
|
-
...discovered.some((cache) => canonicalPath(cache.dir) === canonicalPath(entry.dir) && cache.source === "registered") ? { source: "registered" } : {}
|
|
131
|
-
}], false, env)[0]?.machineGlobal ?? (entry.category === "compilation-cache" ? cacheUser(entry.dir) : void 0);
|
|
132
|
-
}
|
|
133
|
-
async function measureSizes(now, failure) {
|
|
134
|
-
const sizes = [];
|
|
135
|
-
const outputs = await collectWorkspaceOutputs({
|
|
136
|
-
olderThan: null,
|
|
137
|
-
now,
|
|
138
|
-
sizeTimeoutMs: 12e4
|
|
139
|
-
});
|
|
140
|
-
for (const entry of outputs.workspaces) {
|
|
141
|
-
if (entry.bytes === null) {
|
|
142
|
-
failure(entry.dir, entry.projectRoot ?? void 0);
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
sizes.push({
|
|
146
|
-
name: "workspace build outputs",
|
|
147
|
-
category: "workspace-outputs",
|
|
148
|
-
dir: entry.dir,
|
|
149
|
-
bytes: entry.bytes,
|
|
150
|
-
measuredAt: now,
|
|
151
|
-
...entry.projectRoot ? { workspace: canonicalPath(entry.projectRoot) } : {},
|
|
152
|
-
idleDays: entry.idleDays,
|
|
153
|
-
...!entry.willClear ? { blocked: entry.keptReason ?? "workspace ownership is unresolved" } : {}
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
const known = [
|
|
157
|
-
{
|
|
158
|
-
name: "shared build cache",
|
|
159
|
-
dir: buildCacheRoot(),
|
|
160
|
-
category: "build-cache"
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
name: "Metro transform caches",
|
|
164
|
-
dir: metroCacheRoot(),
|
|
165
|
-
category: "metro-cache"
|
|
166
|
-
},
|
|
167
|
-
{
|
|
168
|
-
name: "ccache",
|
|
169
|
-
dir: sharedCcache(),
|
|
170
|
-
category: "ccache"
|
|
171
|
-
},
|
|
172
|
-
{
|
|
173
|
-
name: "Swift compilation cache",
|
|
174
|
-
dir: sharedCompilationCache(),
|
|
175
|
-
category: "compilation-cache"
|
|
176
|
-
}
|
|
177
|
-
];
|
|
178
|
-
const discovered = discoverCaches();
|
|
179
|
-
for (const cache of discovered) {
|
|
180
|
-
if (cache.source !== "registered" && !canonicalPath(cache.dir).startsWith(`${canonicalPath(configDir())}/`)) continue;
|
|
181
|
-
if (cache.prune === "report-only") continue;
|
|
182
|
-
const dir = canonicalPath(cache.dir);
|
|
183
|
-
if (known.some((item) => dir === canonicalPath(item.dir) || dir.startsWith(`${canonicalPath(item.dir)}/`))) continue;
|
|
184
|
-
const category = cache.name.startsWith("Metro transform cache") ? "metro-cache" : /^(Expo build cache|native build cache|Build cache)$/.test(cache.name) ? "build-cache" : "other";
|
|
185
|
-
known.push({
|
|
186
|
-
name: cache.name,
|
|
187
|
-
dir: cache.dir,
|
|
188
|
-
category
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
for (const entry of known) {
|
|
192
|
-
if (!existsSync(entry.dir)) continue;
|
|
193
|
-
const bytes = await measuredDirectorySizeAsync(entry.dir, { timeoutMs: 12e4 });
|
|
194
|
-
if (bytes === null) {
|
|
195
|
-
failure(entry.dir);
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
const blocked = cacheBlocked(entry, discovered);
|
|
199
|
-
sizes.push({
|
|
200
|
-
...entry,
|
|
201
|
-
bytes,
|
|
202
|
-
measuredAt: now,
|
|
203
|
-
...blocked ? { blocked } : {}
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
return sizes;
|
|
207
|
-
}
|
|
208
|
-
//#endregion
|
|
209
43
|
//#region src/workspace/pods-relocate.ts
|
|
210
44
|
const CHECKSUMS_HEADER = "SPEC CHECKSUMS:\n";
|
|
211
45
|
const GENERATED_DIRS = [
|
|
@@ -830,6 +664,95 @@ async function removeKind(record, kind) {
|
|
|
830
664
|
function logRemovalOrder(path) {
|
|
831
665
|
return /\.ndjson\.\d+$/.test(path) ? 0 : /^build-.*\.ndjson$/.test(basename(path)) ? 1 : 2;
|
|
832
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
|
+
}
|
|
833
756
|
async function enforceArchiveRetention(now = Date.now(), operation) {
|
|
834
757
|
const signal = operation?.signal;
|
|
835
758
|
const env = operation?.env ?? process.env;
|
|
@@ -903,6 +826,7 @@ async function enforceArchiveRetention(now = Date.now(), operation) {
|
|
|
903
826
|
record.expires.record = new Date(Date.parse(record.removedAt) + limits.maxAgeDays * DAY).toISOString();
|
|
904
827
|
if (JSON.stringify(record.expires) !== before) await writeRecord(archiveDir(record.id), record);
|
|
905
828
|
}
|
|
829
|
+
await pruneWorktreeArchives(now, limits, signal);
|
|
906
830
|
}, {
|
|
907
831
|
waitMs: 3e4,
|
|
908
832
|
signal
|
|
@@ -1458,7 +1382,7 @@ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwned
|
|
|
1458
1382
|
//#region src/workspace/service-guard.ts
|
|
1459
1383
|
const SERVICE_IN_USE = { code: "STIM_WORKTREE_SERVICE" };
|
|
1460
1384
|
function inside(folder, canonical) {
|
|
1461
|
-
return (value) => typeof value === "string" && value.startsWith("/") && isPathPrefix(folder, canonical(value));
|
|
1385
|
+
return (value) => typeof value === "string" && value.startsWith("/") && isPathPrefix$1(folder, canonical(value));
|
|
1462
1386
|
}
|
|
1463
1387
|
function serviceUseOf(plist, file, folder, canonical) {
|
|
1464
1388
|
if (typeof plist !== "object" || plist === null) return null;
|
|
@@ -1537,37 +1461,561 @@ function serviceRunningFrom(folder, canonical, agentsDir = join(homedir(), "Libr
|
|
|
1537
1461
|
argument: null
|
|
1538
1462
|
};
|
|
1539
1463
|
}
|
|
1540
|
-
for (const name of names) {
|
|
1541
|
-
const file = join(agentsDir, name);
|
|
1542
|
-
const label = name.replace(/\.plist$/, "");
|
|
1543
|
-
try {
|
|
1544
|
-
if (!readFileSync(file).includes("StimService")) continue;
|
|
1545
|
-
} catch {
|
|
1546
|
-
return {
|
|
1547
|
-
label,
|
|
1548
|
-
plist: file,
|
|
1549
|
-
argument: null
|
|
1550
|
-
};
|
|
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"
|
|
1551
1988
|
}
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
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;
|
|
1568
2009
|
}
|
|
2010
|
+
const blocked = cacheBlocked(entry, discovered);
|
|
2011
|
+
sizes.push({
|
|
2012
|
+
...entry,
|
|
2013
|
+
bytes,
|
|
2014
|
+
measuredAt: now,
|
|
2015
|
+
...blocked ? { blocked } : {}
|
|
2016
|
+
});
|
|
1569
2017
|
}
|
|
1570
|
-
return
|
|
2018
|
+
return sizes;
|
|
1571
2019
|
}
|
|
1572
2020
|
//#endregion
|
|
1573
2021
|
//#region src/engine/warm-claim.ts
|
|
@@ -1827,9 +2275,11 @@ function checkoutFactLine(branch, plan, head) {
|
|
|
1827
2275
|
/**
|
|
1828
2276
|
* Whether to install, decided against the lockfile as it stands now rather than against what HEAD did.
|
|
1829
2277
|
* A lockfile that did not move is not evidence that its dependencies are installed: an install that was
|
|
1830
|
-
* 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.
|
|
1831
2281
|
*/
|
|
1832
|
-
function depsPlan({ lockfile, lockfileChanged, installed,
|
|
2282
|
+
function depsPlan({ lockfile, lockfileChanged, patchesChanged, installed, treeProblems, lastInstall, lockHash, patchesHash }) {
|
|
1833
2283
|
if (!lockfile) return {
|
|
1834
2284
|
run: false,
|
|
1835
2285
|
reason: "no lockfile in this repository"
|
|
@@ -1838,7 +2288,7 @@ function depsPlan({ lockfile, lockfileChanged, installed, treeValid, lastInstall
|
|
|
1838
2288
|
run: true,
|
|
1839
2289
|
reason: "no installed dependencies"
|
|
1840
2290
|
};
|
|
1841
|
-
if (
|
|
2291
|
+
if (treeProblems && lastInstall?.treeProblems !== treeProblems) return {
|
|
1842
2292
|
run: true,
|
|
1843
2293
|
reason: `the installed tree does not match ${lockfile}`
|
|
1844
2294
|
};
|
|
@@ -1855,6 +2305,10 @@ function depsPlan({ lockfile, lockfileChanged, installed, treeValid, lastInstall
|
|
|
1855
2305
|
run: true,
|
|
1856
2306
|
reason: `${lockfile} does not match the last completed install`
|
|
1857
2307
|
};
|
|
2308
|
+
if (lastInstall.patches !== patchesHash) return {
|
|
2309
|
+
run: true,
|
|
2310
|
+
reason: "patches/ does not match the last completed install"
|
|
2311
|
+
};
|
|
1858
2312
|
return {
|
|
1859
2313
|
run: false,
|
|
1860
2314
|
reason: `${lockfile} matches the last completed install`
|
|
@@ -1864,6 +2318,10 @@ function depsPlan({ lockfile, lockfileChanged, installed, treeValid, lastInstall
|
|
|
1864
2318
|
run: true,
|
|
1865
2319
|
reason: `${lockfile} changed`
|
|
1866
2320
|
};
|
|
2321
|
+
if (patchesChanged) return {
|
|
2322
|
+
run: true,
|
|
2323
|
+
reason: "patches/ changed"
|
|
2324
|
+
};
|
|
1867
2325
|
return {
|
|
1868
2326
|
run: false,
|
|
1869
2327
|
reason: `${lockfile} unchanged`
|
|
@@ -1875,18 +2333,51 @@ function depsPlan({ lockfile, lockfileChanged, installed, treeValid, lastInstall
|
|
|
1875
2333
|
* nothing about this one, and no record at all is the state of every repository before its first refresh.
|
|
1876
2334
|
*/
|
|
1877
2335
|
function incompleteInstallRefusal(root, appDir) {
|
|
1878
|
-
const dependencies
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
}
|
|
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
|
+
}
|
|
1890
2381
|
}
|
|
1891
2382
|
function podsPlan({ hasIos, hasPodfile, podfileLockChanged, stale, dir = "ios/" }) {
|
|
1892
2383
|
if (!hasIos) return {
|
|
@@ -1938,6 +2429,22 @@ function lockfileHash(depsRoot, lockfile) {
|
|
|
1938
2429
|
return null;
|
|
1939
2430
|
}
|
|
1940
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
|
+
}
|
|
1941
2448
|
function readInstallEvidence(depsRoot) {
|
|
1942
2449
|
try {
|
|
1943
2450
|
const parsed = JSON.parse(readFileSync(installRecordFile(depsRoot), "utf-8"));
|
|
@@ -1946,21 +2453,19 @@ function readInstallEvidence(depsRoot) {
|
|
|
1946
2453
|
return {
|
|
1947
2454
|
lock: parsed.lock,
|
|
1948
2455
|
hash: parsed.hash,
|
|
1949
|
-
|
|
2456
|
+
patches: typeof parsed.patches === "string" ? parsed.patches : "",
|
|
2457
|
+
completed: parsed.completed === true,
|
|
2458
|
+
treeProblems: typeof parsed.treeProblems === "string" ? parsed.treeProblems : null
|
|
1950
2459
|
};
|
|
1951
2460
|
} catch {
|
|
1952
2461
|
return null;
|
|
1953
2462
|
}
|
|
1954
2463
|
}
|
|
1955
|
-
function writeInstallEvidence(depsRoot,
|
|
2464
|
+
function writeInstallEvidence(depsRoot, evidence) {
|
|
1956
2465
|
const file = installRecordFile(depsRoot);
|
|
1957
2466
|
const tmp = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
1958
2467
|
mkdirSync(join(getConfigDir(), "warm-installs"), { recursive: true });
|
|
1959
|
-
writeFileSync(tmp, JSON.stringify(
|
|
1960
|
-
lock: lockfile,
|
|
1961
|
-
hash: hash ?? "",
|
|
1962
|
-
completed
|
|
1963
|
-
}));
|
|
2468
|
+
writeFileSync(tmp, JSON.stringify(evidence));
|
|
1964
2469
|
try {
|
|
1965
2470
|
renameSync(tmp, file);
|
|
1966
2471
|
} catch (error) {
|
|
@@ -1968,8 +2473,17 @@ function writeInstallEvidence(depsRoot, lockfile, hash, completed) {
|
|
|
1968
2473
|
throw error;
|
|
1969
2474
|
}
|
|
1970
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
|
+
}
|
|
1971
2485
|
function restoreInstallEvidence(depsRoot, prior) {
|
|
1972
|
-
if (prior) writeInstallEvidence(depsRoot, prior
|
|
2486
|
+
if (prior) writeInstallEvidence(depsRoot, prior);
|
|
1973
2487
|
else rmSync(installRecordFile(depsRoot), { force: true });
|
|
1974
2488
|
}
|
|
1975
2489
|
function inProgressOperation(gitDir) {
|
|
@@ -2183,10 +2697,13 @@ async function refreshMainCheckout({ root, appDir, settings, iosProject, emit, s
|
|
|
2183
2697
|
const plan = checkoutPlan(locallyKnownUpstream(root));
|
|
2184
2698
|
if (plan.kind === "diverged") return divergedRefusal(root, branch, plan);
|
|
2185
2699
|
if (inspectOnly && plan.kind === "fast-forward") return { mutation: `fast-forward ${plan.behind} commit${plan.behind === 1 ? "" : "s"}` };
|
|
2186
|
-
const depsBefore =
|
|
2187
|
-
const
|
|
2188
|
-
const marked = plan.kind === "fast-forward" ? depsBefore :
|
|
2189
|
-
|
|
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
|
+
};
|
|
2190
2707
|
let head = before;
|
|
2191
2708
|
if (plan.kind === "fast-forward") {
|
|
2192
2709
|
const merged = fastForward(root, plan.upstream);
|
|
@@ -2201,43 +2718,71 @@ async function refreshMainCheckout({ root, appDir, settings, iosProject, emit, s
|
|
|
2201
2718
|
emit(checkoutFactLine(branch, plan, head));
|
|
2202
2719
|
const note = defaultBranchNote(branch, resolveDefaultBranch(root, settings));
|
|
2203
2720
|
if (note.kind !== "match") for (const line of note.lines) emit(phaseLine("", line));
|
|
2204
|
-
const
|
|
2205
|
-
const
|
|
2206
|
-
|
|
2207
|
-
const
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
cwd: dependencies.root,
|
|
2222
|
-
spawnFn: spawn,
|
|
2223
|
-
now,
|
|
2224
|
-
heartbeatMs,
|
|
2225
|
-
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
|
|
2226
2738
|
});
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
} else {
|
|
2238
|
-
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);
|
|
2239
2749
|
emit(stepLine("deps", depsSubject, "skipped"));
|
|
2240
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
|
+
}
|
|
2241
2786
|
const app = relative(root, appDir) || ".";
|
|
2242
2787
|
const where = `source ${appDir}: `;
|
|
2243
2788
|
const ios = iosProject;
|
|
@@ -2274,205 +2819,6 @@ async function refreshMainCheckout({ root, appDir, settings, iosProject, emit, s
|
|
|
2274
2819
|
return podsRunning;
|
|
2275
2820
|
}
|
|
2276
2821
|
//#endregion
|
|
2277
|
-
//#region src/workspace/worktree-trash.ts
|
|
2278
|
-
const TRASH_MANIFEST = "trash.json";
|
|
2279
|
-
const TRASH_ENTRY_PREFIX = "remove-";
|
|
2280
|
-
const TRASH_ENTRIES = "entries";
|
|
2281
|
-
const TRASH_CLAIMS = "claims";
|
|
2282
|
-
const LIST_TIMEOUT_MS = 3e4;
|
|
2283
|
-
const DELETE_NICENESS = "10";
|
|
2284
|
-
function rethrowCancellation(error, signal) {
|
|
2285
|
-
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
2286
|
-
signal?.throwIfAborted();
|
|
2287
|
-
}
|
|
2288
|
-
async function isPlainDirectory(path) {
|
|
2289
|
-
const stat = await lstat(path).catch(() => null);
|
|
2290
|
-
return Boolean(stat?.isDirectory());
|
|
2291
|
-
}
|
|
2292
|
-
async function ignoredDirectories(worktree, options) {
|
|
2293
|
-
const out = await gitAsync(worktree, [
|
|
2294
|
-
"ls-files",
|
|
2295
|
-
"--others",
|
|
2296
|
-
"--ignored",
|
|
2297
|
-
"--exclude-standard",
|
|
2298
|
-
"--directory",
|
|
2299
|
-
"--no-empty-directory",
|
|
2300
|
-
"-z"
|
|
2301
|
-
], {
|
|
2302
|
-
...options.env ? childEnvironment(options.env) : {},
|
|
2303
|
-
signal: options.signal,
|
|
2304
|
-
declareFor: options.declareFor,
|
|
2305
|
-
timeoutMs: LIST_TIMEOUT_MS,
|
|
2306
|
-
untrimmed: true
|
|
2307
|
-
});
|
|
2308
|
-
const found = [];
|
|
2309
|
-
for (const entry of out.split("\0")) {
|
|
2310
|
-
if (!entry.endsWith("/")) continue;
|
|
2311
|
-
const rel = entry.slice(0, -1);
|
|
2312
|
-
const parts = rel.split("/");
|
|
2313
|
-
let plain = true;
|
|
2314
|
-
for (let depth = 1; depth <= parts.length && plain; depth++) plain = await isPlainDirectory(join(worktree, ...parts.slice(0, depth)));
|
|
2315
|
-
if (plain) found.push(rel);
|
|
2316
|
-
}
|
|
2317
|
-
return found;
|
|
2318
|
-
}
|
|
2319
|
-
/**
|
|
2320
|
-
* Moves the worktree's ignored directories, which `git worktree remove` would delete, into a new entry under
|
|
2321
|
-
* `$STIM_HOME/trash` held by an exclusive ownership claim. Null, with nothing moved, when the trash is on another
|
|
2322
|
-
* filesystem, the platform is Windows, git cannot list the entries, or the claim cannot be taken. A directory that
|
|
2323
|
-
* does not rename stays in the worktree for git to delete.
|
|
2324
|
-
*/
|
|
2325
|
-
async function moveIgnoredTreesToTrash(worktree, options = {}) {
|
|
2326
|
-
if ((options.platform ?? process.platform) === "win32") return null;
|
|
2327
|
-
let listed;
|
|
2328
|
-
try {
|
|
2329
|
-
listed = await ignoredDirectories(worktree, options);
|
|
2330
|
-
} catch (error) {
|
|
2331
|
-
rethrowCancellation(error, options.signal);
|
|
2332
|
-
return null;
|
|
2333
|
-
}
|
|
2334
|
-
if (!listed.length) return null;
|
|
2335
|
-
const root = trashRoot();
|
|
2336
|
-
let dir = null;
|
|
2337
|
-
let claim = null;
|
|
2338
|
-
const entries = listed.map((from, index) => ({
|
|
2339
|
-
from,
|
|
2340
|
-
to: String(index)
|
|
2341
|
-
}));
|
|
2342
|
-
try {
|
|
2343
|
-
await mkdir(root, { recursive: true });
|
|
2344
|
-
if (filesystemDevice(root) !== filesystemDevice(worktree)) return null;
|
|
2345
|
-
dir = await mkdtemp(join(root, TRASH_ENTRY_PREFIX));
|
|
2346
|
-
claim = (0, ownership_claim_exports.tryAcquireClaim)({
|
|
2347
|
-
root: join(dir, "claims"),
|
|
2348
|
-
mode: "exclusive",
|
|
2349
|
-
label: "worktree trash"
|
|
2350
|
-
}).acquired ?? null;
|
|
2351
|
-
if (!claim) throw new Error("the trash entry claim was not acquired");
|
|
2352
|
-
const manifest = {
|
|
2353
|
-
worktree,
|
|
2354
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2355
|
-
};
|
|
2356
|
-
await writeFile(join(dir, TRASH_MANIFEST), `${JSON.stringify(manifest)}\n`);
|
|
2357
|
-
await mkdir(join(dir, TRASH_ENTRIES));
|
|
2358
|
-
} catch (error) {
|
|
2359
|
-
if (claim) (0, ownership_claim_exports.releaseClaim)(claim);
|
|
2360
|
-
if (dir) await rm(dir, {
|
|
2361
|
-
recursive: true,
|
|
2362
|
-
force: true
|
|
2363
|
-
}).catch(() => {});
|
|
2364
|
-
rethrowCancellation(error, options.signal);
|
|
2365
|
-
return null;
|
|
2366
|
-
}
|
|
2367
|
-
const moved = [];
|
|
2368
|
-
for (const entry of entries) try {
|
|
2369
|
-
await rename(join(worktree, entry.from), join(dir, TRASH_ENTRIES, entry.to));
|
|
2370
|
-
moved.push(entry);
|
|
2371
|
-
} catch {}
|
|
2372
|
-
const trash = {
|
|
2373
|
-
dir,
|
|
2374
|
-
worktree,
|
|
2375
|
-
moved,
|
|
2376
|
-
claim
|
|
2377
|
-
};
|
|
2378
|
-
if (moved.length) return trash;
|
|
2379
|
-
await discardTrash(trash);
|
|
2380
|
-
return null;
|
|
2381
|
-
}
|
|
2382
|
-
async function discardTrash(trash) {
|
|
2383
|
-
await rmdir(join(trash.dir, TRASH_ENTRIES)).catch(() => {});
|
|
2384
|
-
await rm(join(trash.dir, TRASH_MANIFEST), { force: true }).catch(() => {});
|
|
2385
|
-
(0, ownership_claim_exports.releaseClaim)(trash.claim);
|
|
2386
|
-
await rmdir(trash.dir).catch(() => {});
|
|
2387
|
-
}
|
|
2388
|
-
/** Moves every tree back into the worktree. Returns the relative paths that stay in the trash for gc. */
|
|
2389
|
-
async function restoreTrashedTrees(trash) {
|
|
2390
|
-
const kept = [];
|
|
2391
|
-
for (const entry of trash.moved.toReversed()) {
|
|
2392
|
-
const target = join(trash.worktree, entry.from);
|
|
2393
|
-
try {
|
|
2394
|
-
if (await lstat(target).catch(() => null) || !await isPlainDirectory(dirname(target))) {
|
|
2395
|
-
kept.push(entry.from);
|
|
2396
|
-
continue;
|
|
2397
|
-
}
|
|
2398
|
-
await rename(join(trash.dir, TRASH_ENTRIES, entry.to), target);
|
|
2399
|
-
} catch {
|
|
2400
|
-
kept.push(entry.from);
|
|
2401
|
-
}
|
|
2402
|
-
}
|
|
2403
|
-
if (kept.length) (0, ownership_claim_exports.releaseClaim)(trash.claim);
|
|
2404
|
-
else await discardTrash(trash);
|
|
2405
|
-
return kept;
|
|
2406
|
-
}
|
|
2407
|
-
/** Deletes one trash entry: `rm -rf` for the bulk, then a walk that also clears read-only directories. */
|
|
2408
|
-
async function deleteTrashEntry(dir, operation) {
|
|
2409
|
-
if (process.platform !== "win32") try {
|
|
2410
|
-
await runOperationFile(operation, getExecutor(), "rm", [
|
|
2411
|
-
"-rf",
|
|
2412
|
-
"--",
|
|
2413
|
-
join(dir, TRASH_ENTRIES)
|
|
2414
|
-
], {
|
|
2415
|
-
timeoutMs: "unbounded",
|
|
2416
|
-
signal: operation?.signal,
|
|
2417
|
-
cwd: dirname(dir)
|
|
2418
|
-
});
|
|
2419
|
-
} catch (error) {
|
|
2420
|
-
rethrowCancellation(error, operation?.signal);
|
|
2421
|
-
}
|
|
2422
|
-
await removeTemporaryEntryAsync(dir, operation?.signal);
|
|
2423
|
-
}
|
|
2424
|
-
function deleteInBackground(trash) {
|
|
2425
|
-
try {
|
|
2426
|
-
(0, ownership_claim_exports.markClaimChildPending)(trash.claim);
|
|
2427
|
-
const child = getExecutor().spawn("nice", [
|
|
2428
|
-
"-n",
|
|
2429
|
-
DELETE_NICENESS,
|
|
2430
|
-
"rm",
|
|
2431
|
-
"-rf",
|
|
2432
|
-
"--",
|
|
2433
|
-
join(trash.dir, TRASH_ENTRIES),
|
|
2434
|
-
trash.dir
|
|
2435
|
-
], {
|
|
2436
|
-
cwd: dirname(trash.dir),
|
|
2437
|
-
detached: true,
|
|
2438
|
-
stdio: "ignore"
|
|
2439
|
-
});
|
|
2440
|
-
child.on?.("error", () => {});
|
|
2441
|
-
child.unref?.();
|
|
2442
|
-
const captured = child.pid === void 0 ? null : captureProcessIdentity(child.pid);
|
|
2443
|
-
if (!captured?.ok || child.pid === void 0) throw new Error("the trash deletion process was not recorded");
|
|
2444
|
-
(0, ownership_claim_exports.setClaimChild)(trash.claim, {
|
|
2445
|
-
pid: child.pid,
|
|
2446
|
-
processToken: captured.token
|
|
2447
|
-
});
|
|
2448
|
-
return true;
|
|
2449
|
-
} catch {
|
|
2450
|
-
try {
|
|
2451
|
-
(0, ownership_claim_exports.clearClaimChild)(trash.claim);
|
|
2452
|
-
} catch {}
|
|
2453
|
-
(0, ownership_claim_exports.releaseClaim)(trash.claim);
|
|
2454
|
-
return false;
|
|
2455
|
-
}
|
|
2456
|
-
}
|
|
2457
|
-
/**
|
|
2458
|
-
* Deletes the trash entry once git removed the worktree. A CLI command starts a detached, low-priority `rm` recorded
|
|
2459
|
-
* as the claim's child, so gc reports the entry as in use until `rm` exits. Inside a bounded report, such as the
|
|
2460
|
-
* programmatic API, every child is stopped when the report settles, so the entry is deleted before returning.
|
|
2461
|
-
* `left` means the entry waits for gc with its claim released.
|
|
2462
|
-
*/
|
|
2463
|
-
async function disposeTrash(trash, operation) {
|
|
2464
|
-
if (!getExecutor().reportSignal?.()) return deleteInBackground(trash) ? "background" : "left";
|
|
2465
|
-
try {
|
|
2466
|
-
await deleteTrashEntry(trash.dir, operation);
|
|
2467
|
-
return "deleted";
|
|
2468
|
-
} catch (error) {
|
|
2469
|
-
rethrowCancellation(error, operation?.signal);
|
|
2470
|
-
return "left";
|
|
2471
|
-
} finally {
|
|
2472
|
-
(0, ownership_claim_exports.releaseClaim)(trash.claim);
|
|
2473
|
-
}
|
|
2474
|
-
}
|
|
2475
|
-
//#endregion
|
|
2476
2822
|
//#region src/commands/worktree.ts
|
|
2477
2823
|
function dependencyInstallCommand(target, dir = ".") {
|
|
2478
2824
|
const installRoot = resolve$1(target, dir);
|
|
@@ -2529,7 +2875,7 @@ function reportRefreshFailure(failure, out) {
|
|
|
2529
2875
|
out(chalk.red(`failed: ${failure.code}`));
|
|
2530
2876
|
}
|
|
2531
2877
|
function mainCheckoutAppDir(root, target, cwd) {
|
|
2532
|
-
const app = findProjectRoot$1(cwd);
|
|
2878
|
+
const app = warmedWorkspace(root, target, cwd) ?? findProjectRoot$1(cwd);
|
|
2533
2879
|
if (!app) return root;
|
|
2534
2880
|
const rel = relative$1(target, app);
|
|
2535
2881
|
if (!rel || rel.startsWith("..") || isAbsolute$1(rel)) return root;
|
|
@@ -2537,20 +2883,26 @@ function mainCheckoutAppDir(root, target, cwd) {
|
|
|
2537
2883
|
}
|
|
2538
2884
|
/**
|
|
2539
2885
|
* The app workspace a warm prepares: the nearest app below the worktree root, or else the one app registered in
|
|
2540
|
-
* the source checkout, at the same place in this worktree, or else the
|
|
2541
|
-
* 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.
|
|
2542
2888
|
*/
|
|
2543
2889
|
function warmedWorkspace(root, target, cwd) {
|
|
2544
2890
|
const nearest = findProjectRoot$1(cwd);
|
|
2545
|
-
const app = nearest && isPathPrefix(target, nearest) && isMobileProject(nearest) ? nearest : null;
|
|
2891
|
+
const app = nearest && isPathPrefix$1(target, nearest) && isMobileProject(nearest) ? nearest : null;
|
|
2546
2892
|
if (app && app !== target) return app;
|
|
2547
|
-
const
|
|
2548
|
-
|
|
2549
|
-
const
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
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);
|
|
2554
2906
|
return apps.size === 1 ? [...apps][0] : app;
|
|
2555
2907
|
}
|
|
2556
2908
|
async function registerWarmedWorkspace(workspace, out) {
|
|
@@ -2605,7 +2957,7 @@ async function warmWorktreeTarget(opts) {
|
|
|
2605
2957
|
for (const key of scope) opts.beforeWarm?.(nativeCanonicalPath(key));
|
|
2606
2958
|
if (app) await recordWorkspaceUseAsync(app, /* @__PURE__ */ new Date(), operation.env, opts.cwd, operation.signal);
|
|
2607
2959
|
if (workspace && workspace !== app) await recordWorkspaceUseAsync(workspace, /* @__PURE__ */ new Date(), operation.env, opts.cwd, operation.signal);
|
|
2608
|
-
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")));
|
|
2609
2961
|
else {
|
|
2610
2962
|
await registerWarmedWorkspace(workspace, out);
|
|
2611
2963
|
progress = startWarmProgress(workspace, opts.refresh ? "refresh" : "copy", { note: (line) => out(chalk.yellow(line)) });
|
|
@@ -2621,7 +2973,7 @@ async function warmWorktreeTarget(opts) {
|
|
|
2621
2973
|
out(chalk.dim(SETTING_SHAPE_REMEDY));
|
|
2622
2974
|
return null;
|
|
2623
2975
|
}
|
|
2624
|
-
for (const key of unknownSettingKeys(settings)) out(chalk.yellow(
|
|
2976
|
+
for (const key of unknownSettingKeys(settings)) out(chalk.yellow(inertSettingWarning(settings, key)));
|
|
2625
2977
|
return settings;
|
|
2626
2978
|
};
|
|
2627
2979
|
const copy = async (wait) => {
|
|
@@ -2654,12 +3006,12 @@ async function warmWorktreeTarget(opts) {
|
|
|
2654
3006
|
}
|
|
2655
3007
|
const excluded = readWorktreeExclude(root);
|
|
2656
3008
|
const patterns = excluded?.length ? excluded : settings.worktree?.exclude || [];
|
|
2657
|
-
const result = await cloneIgnoredEntries({
|
|
3009
|
+
const result = await trace.spanAsync("carry ignored entries", () => cloneIgnoredEntries({
|
|
2658
3010
|
root,
|
|
2659
3011
|
target,
|
|
2660
3012
|
patterns,
|
|
2661
3013
|
operation
|
|
2662
|
-
});
|
|
3014
|
+
}));
|
|
2663
3015
|
for (const entry of result.skipped) out(chalk.dim(phaseLine("carry", `kept ${entry.file} (${entry.reason})`)));
|
|
2664
3016
|
for (const entry of result.failed) out(chalk.yellow(phaseLine("carry", `could not copy ${entry.file}: ${entry.error}`)));
|
|
2665
3017
|
if (result.copied.length) {
|
|
@@ -2780,7 +3132,7 @@ function customIosDirs(worktree) {
|
|
|
2780
3132
|
} catch {
|
|
2781
3133
|
return [];
|
|
2782
3134
|
}
|
|
2783
|
-
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) => ({
|
|
2784
3136
|
path,
|
|
2785
3137
|
ios: appIosProjectDir(path)
|
|
2786
3138
|
})).filter(({ ios }) => ios.custom && existsSync$1(ios.dir)).map(({ path, ios }) => ({
|
|
@@ -2841,13 +3193,13 @@ function nativeCanonicalPath(path) {
|
|
|
2841
3193
|
/** Other worktrees git lists inside `path`, which removing `path` would delete with it. */
|
|
2842
3194
|
function nestedWorktrees(entries, path) {
|
|
2843
3195
|
const root = nativeCanonicalPath(path);
|
|
2844
|
-
return entries.filter((candidate) => !candidate.bare && nativeCanonicalPath(candidate.path) !== root).filter((candidate) => isPathPrefix(root, nativeCanonicalPath(candidate.path))).map((candidate) => candidate.path);
|
|
3196
|
+
return entries.filter((candidate) => !candidate.bare && nativeCanonicalPath(candidate.path) !== root).filter((candidate) => isPathPrefix$1(root, nativeCanonicalPath(candidate.path))).map((candidate) => candidate.path);
|
|
2845
3197
|
}
|
|
2846
3198
|
function matchWorktreeEntry(entries, path) {
|
|
2847
3199
|
let best = null;
|
|
2848
3200
|
const target = nativeCanonicalPath(path);
|
|
2849
3201
|
(entries || []).forEach((entry, index) => {
|
|
2850
|
-
if (!entry?.path || !isPathPrefix(nativeCanonicalPath(entry.path), target)) return;
|
|
3202
|
+
if (!entry?.path || !isPathPrefix$1(nativeCanonicalPath(entry.path), target)) return;
|
|
2851
3203
|
if (!best || entry.path.length > best.path.length) best = {
|
|
2852
3204
|
...entry,
|
|
2853
3205
|
index
|
|
@@ -2883,10 +3235,11 @@ function pathArgs(lines, limit = 5) {
|
|
|
2883
3235
|
const shown = paths.slice(0, limit).map((p) => /[\s"'\\$`]/.test(p) ? JSON.stringify(p) : p);
|
|
2884
3236
|
return `${shown.join(" ")}${paths.length > shown.length ? " ..." : ""}`;
|
|
2885
3237
|
}
|
|
3238
|
+
const DIRTY_BLOCKER = "uncommitted changes or untracked files";
|
|
2886
3239
|
function removalBlockers({ dirty, unpushed }) {
|
|
2887
3240
|
const blockers = [];
|
|
2888
3241
|
if (dirty === null || unpushed === null) blockers.push("could not determine git status; re-run with --force to override");
|
|
2889
|
-
if (dirty) blockers.push(
|
|
3242
|
+
if (dirty) blockers.push(DIRTY_BLOCKER);
|
|
2890
3243
|
if (unpushed && unpushed.length) blockers.push(`${plural(unpushed.length, "commit")} not on any remote or any other local branch`);
|
|
2891
3244
|
return blockers;
|
|
2892
3245
|
}
|
|
@@ -2898,7 +3251,7 @@ function reclaimKeys(rootPath) {
|
|
|
2898
3251
|
const keys = /* @__PURE__ */ new Set([rootPath]);
|
|
2899
3252
|
const root = nativeCanonicalPath(rootPath);
|
|
2900
3253
|
if (cfg?.projects) {
|
|
2901
|
-
for (const key of Object.keys(cfg.projects)) if (isPathPrefix(rootPath, key) || isPathPrefix(root, nativeCanonicalPath(key))) keys.add(key);
|
|
3254
|
+
for (const key of Object.keys(cfg.projects)) if (isPathPrefix$1(rootPath, key) || isPathPrefix$1(root, nativeCanonicalPath(key))) keys.add(key);
|
|
2902
3255
|
}
|
|
2903
3256
|
return [...keys].toSorted();
|
|
2904
3257
|
}
|
|
@@ -3039,11 +3392,11 @@ function reportRetainedResources(root, result, output) {
|
|
|
3039
3392
|
}
|
|
3040
3393
|
function hasRegisteredProjectUnder(rootPath) {
|
|
3041
3394
|
const cfg = loadConfig$1();
|
|
3042
|
-
return Object.keys(cfg?.projects ?? {}).some((key) => isPathPrefix(rootPath, key));
|
|
3395
|
+
return Object.keys(cfg?.projects ?? {}).some((key) => isPathPrefix$1(rootPath, key));
|
|
3043
3396
|
}
|
|
3044
3397
|
function projectAtOrAbove(root, path) {
|
|
3045
3398
|
const here = nativeCanonicalPath(path);
|
|
3046
|
-
return Object.keys(loadConfig$1()?.projects ?? {}).filter((key) => isPathPrefix(nativeCanonicalPath(root), nativeCanonicalPath(key)) && isPathPrefix(nativeCanonicalPath(key), here)).toSorted((a, b) => b.length - a.length)[0] ?? null;
|
|
3399
|
+
return Object.keys(loadConfig$1()?.projects ?? {}).filter((key) => isPathPrefix$1(nativeCanonicalPath(root), nativeCanonicalPath(key)) && isPathPrefix$1(nativeCanonicalPath(key), here)).toSorted((a, b) => b.length - a.length)[0] ?? null;
|
|
3047
3400
|
}
|
|
3048
3401
|
async function reclaimEnvironment(root, why, output, operation, beforeRemove) {
|
|
3049
3402
|
await withManagedRemoteWorktreeRemovalLock(root, () => withReclaimLocks(root, async (lockedKeys) => {
|
|
@@ -3077,6 +3430,9 @@ const REMOVAL_OUTPUT = {
|
|
|
3077
3430
|
process.exitCode = 1;
|
|
3078
3431
|
}
|
|
3079
3432
|
};
|
|
3433
|
+
function blockersOf(inspection, opts) {
|
|
3434
|
+
return opts.archiveDirty ? inspection.blockers.filter((blocker) => blocker !== DIRTY_BLOCKER) : inspection.blockers;
|
|
3435
|
+
}
|
|
3080
3436
|
function removalPath(target, cwd = process.cwd()) {
|
|
3081
3437
|
return canonicalExistingPath(target ?? cwd);
|
|
3082
3438
|
}
|
|
@@ -3183,6 +3539,25 @@ function printRemovalCleanup(result, failed, output) {
|
|
|
3183
3539
|
if (result.dereferenced.length) output.error(chalk.dim(phaseLine("workspace", `no longer referenced: ${result.dereferenced.join(", ")}`)));
|
|
3184
3540
|
for (const skipped of result.skippedDevices) output.error((failed ? chalk.dim : chalk.yellow)(phaseLine("device", `kept ${describeKeptDevice(skipped)} (${skipped.reason})`)));
|
|
3185
3541
|
}
|
|
3542
|
+
async function archiveBeforeRemoval(path, repository, branch, head, opts, output) {
|
|
3543
|
+
try {
|
|
3544
|
+
if (!head) throw new Error("its HEAD could not be read");
|
|
3545
|
+
return await archiveWorktreeChanges({
|
|
3546
|
+
path,
|
|
3547
|
+
repository,
|
|
3548
|
+
branch,
|
|
3549
|
+
head,
|
|
3550
|
+
pullRequest: opts.archiveDirty?.pullRequest ?? null,
|
|
3551
|
+
operation: opts.operation
|
|
3552
|
+
});
|
|
3553
|
+
} catch (error) {
|
|
3554
|
+
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
3555
|
+
opts.operation?.signal.throwIfAborted();
|
|
3556
|
+
output.error(chalk.red(`Refusing to remove ${path}: could not archive its uncommitted changes: ${(error instanceof Error ? error.message : String(error)).split("\n")[0]}`));
|
|
3557
|
+
output.fail();
|
|
3558
|
+
return null;
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3186
3561
|
async function removeWorktreeTarget(target, opts = {}) {
|
|
3187
3562
|
let removed = false;
|
|
3188
3563
|
await runRemove(target, opts, () => {
|
|
@@ -3334,6 +3709,13 @@ async function removeOrRefuse(target, opts, onRemoved, output) {
|
|
|
3334
3709
|
output.fail();
|
|
3335
3710
|
return;
|
|
3336
3711
|
}
|
|
3712
|
+
const cli = stimCliUseOf(path, nativeCanonicalPath, stimCliEntries(opts.operation?.env));
|
|
3713
|
+
if (cli.length) {
|
|
3714
|
+
output.error(chalk.red(`Refusing to remove ${path}: ${cli.join("; ")}.`));
|
|
3715
|
+
output.error(chalk.dim("Run stim from another checkout or install, or point PATH elsewhere, then retry."));
|
|
3716
|
+
output.fail();
|
|
3717
|
+
return;
|
|
3718
|
+
}
|
|
3337
3719
|
const project = getProject(path);
|
|
3338
3720
|
const branch = entry.branch;
|
|
3339
3721
|
const ownsBranch = Boolean(branch && project?.worktreeBranchOwned === true && project.worktreeBranch === branch);
|
|
@@ -3349,13 +3731,15 @@ async function removeOrRefuse(target, opts, onRemoved, output) {
|
|
|
3349
3731
|
output.error(chalk.dim(`HEAD is in the merged PR #${pr.number} (${pr.url}); its local-only commits stay on GitHub.`));
|
|
3350
3732
|
}
|
|
3351
3733
|
}
|
|
3352
|
-
if (inspection.
|
|
3734
|
+
if (blockersOf(inspection, opts).length && !opts.force) {
|
|
3353
3735
|
await printRemovalRefusal(path, inspection, output, opts.operation);
|
|
3354
3736
|
return;
|
|
3355
3737
|
}
|
|
3356
3738
|
const deleteOwnedBranch = Boolean(ownsBranch && approvedBranchSha && inspection.unpushed?.length === 0);
|
|
3357
3739
|
const retainedBranchReason = !branch ? null : !ownsBranch ? "Stim did not create it" : inspection.unpushed === null ? "Stim could not verify whether it has unique commits" : inspection.unpushed.length > 0 ? `it has ${plural(inspection.unpushed.length, "unique commit")}` : null;
|
|
3358
3740
|
const branchDeleteCwd = source.path;
|
|
3741
|
+
let archive = null;
|
|
3742
|
+
let gitRemoving = false;
|
|
3359
3743
|
await withManagedRemoteWorktreeRemovalLock(path, () => withReclaimLocks(path, async (lockedKeys) => {
|
|
3360
3744
|
options.signal?.throwIfAborted();
|
|
3361
3745
|
if ((await opts.guard?.(lockedKeys))?.length) return;
|
|
@@ -3367,16 +3751,25 @@ async function removeOrRefuse(target, opts, onRemoved, output) {
|
|
|
3367
3751
|
return;
|
|
3368
3752
|
}
|
|
3369
3753
|
const current = await inspectRemoval(path, mergedHead, opts.operation);
|
|
3370
|
-
if (current.
|
|
3754
|
+
if (blockersOf(current, opts).length && !opts.force) {
|
|
3371
3755
|
await printRemovalRefusal(path, current, output, opts.operation);
|
|
3372
3756
|
return;
|
|
3373
3757
|
}
|
|
3758
|
+
if (opts.archiveDirty && !opts.force && current.dirtyLines.length) {
|
|
3759
|
+
const saved = await archiveBeforeRemoval(path, source.path, branch ?? null, inspectedHead, opts, output);
|
|
3760
|
+
if (!saved) return;
|
|
3761
|
+
archive = saved;
|
|
3762
|
+
}
|
|
3763
|
+
const discardArchive = async () => {
|
|
3764
|
+
if (archive) await discardWorktreeArchive(archive);
|
|
3765
|
+
};
|
|
3374
3766
|
const result = await reclaimAll(path, lockedKeys, {
|
|
3375
3767
|
preserveRootProject: true,
|
|
3376
3768
|
removedBy: opts.removedBy ?? "worktree-remove",
|
|
3377
3769
|
operation: opts.operation
|
|
3378
3770
|
});
|
|
3379
3771
|
if (result.keptEntries.length) {
|
|
3772
|
+
await discardArchive();
|
|
3380
3773
|
reportRetainedResources(path, result, output);
|
|
3381
3774
|
return;
|
|
3382
3775
|
}
|
|
@@ -3387,12 +3780,14 @@ async function removeOrRefuse(target, opts, onRemoved, output) {
|
|
|
3387
3780
|
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
3388
3781
|
opts.operation?.signal.throwIfAborted();
|
|
3389
3782
|
if (!isTimeoutError(error)) throw error;
|
|
3783
|
+
await discardArchive();
|
|
3390
3784
|
await printRemovalRefusal(path, timedOutInspection(error), output, opts.operation);
|
|
3391
3785
|
output.error(chalk.dim(`The directory and Stim ownership record for ${path} were kept.`));
|
|
3392
3786
|
printRemovalCleanup(result, true, output);
|
|
3393
3787
|
return;
|
|
3394
3788
|
}
|
|
3395
3789
|
if (headMoved) {
|
|
3790
|
+
await discardArchive();
|
|
3396
3791
|
output.error(chalk.red(`Refusing to remove ${path}: its HEAD moved while its environment was reclaimed.`));
|
|
3397
3792
|
output.error(chalk.dim(`The directory and Stim ownership record for ${path} were kept.`));
|
|
3398
3793
|
printRemovalCleanup(result, true, output);
|
|
@@ -3407,10 +3802,28 @@ async function removeOrRefuse(target, opts, onRemoved, output) {
|
|
|
3407
3802
|
signal: options.signal,
|
|
3408
3803
|
declareFor: opts.operation
|
|
3409
3804
|
});
|
|
3805
|
+
let stale = null;
|
|
3806
|
+
if (archive) try {
|
|
3807
|
+
stale = await worktreeArchiveMismatch(path, archive, opts.operation);
|
|
3808
|
+
} catch (error) {
|
|
3809
|
+
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
3810
|
+
stale = `its archive could not be checked: ${error instanceof Error ? error.message : String(error)}`;
|
|
3811
|
+
}
|
|
3812
|
+
if (stale) {
|
|
3813
|
+
const unrestored = trash ? await restoreTrashedTrees(trash) : [];
|
|
3814
|
+
if (trash && unrestored.length) output.error(chalk.yellow(`Could not move ${unrestored.join(", ")} back into ${path}; ${unrestored.length === 1 ? "it stays" : "they stay"} in ${trash.dir}, which stim gc keeps while ${path} exists.`));
|
|
3815
|
+
await discardArchive();
|
|
3816
|
+
output.error(chalk.red(`Refusing to remove ${path}: ${stale}.`));
|
|
3817
|
+
output.error(chalk.dim(`The directory and Stim ownership record for ${path} were kept.`));
|
|
3818
|
+
printRemovalCleanup(result, true, output);
|
|
3819
|
+
output.fail();
|
|
3820
|
+
return;
|
|
3821
|
+
}
|
|
3822
|
+
gitRemoving = true;
|
|
3410
3823
|
try {
|
|
3411
3824
|
await removeWorktreeAsync(path, {
|
|
3412
3825
|
from: source.path,
|
|
3413
|
-
force: opts.force,
|
|
3826
|
+
force: opts.force || archive !== null,
|
|
3414
3827
|
env: options.env,
|
|
3415
3828
|
signal: options.signal,
|
|
3416
3829
|
declareFor: opts.operation
|
|
@@ -3423,10 +3836,15 @@ async function removeOrRefuse(target, opts, onRemoved, output) {
|
|
|
3423
3836
|
output.error(chalk.red(`git worktree remove failed: ${message}`));
|
|
3424
3837
|
if (process.platform === "win32" && /Permission denied/i.test(message)) output.error(chalk.dim("Another process holds a file or directory inside the worktree. An adb server or emulator started from it keeps its working directory open; `adb kill-server` releases the server, and Sysinternals handle.exe lists other holders."));
|
|
3425
3838
|
output.error(chalk.dim(`The directory and Stim ownership record for ${path} were kept.`));
|
|
3839
|
+
if (archive) output.error(chalk.dim(`Its uncommitted changes stay archived in ${archive.dir}.`));
|
|
3426
3840
|
printRemovalCleanup(result, true, output);
|
|
3427
3841
|
output.fail();
|
|
3428
3842
|
return;
|
|
3429
3843
|
}
|
|
3844
|
+
if (archive) {
|
|
3845
|
+
opts.archiveDirty?.onArchived(archive);
|
|
3846
|
+
output.error(chalk.dim(phaseLine("archive", `saved ${plural(archive.tracked.length + archive.untracked.length, "uncommitted file")} to ${archive.dir}; restore: ${archive.restore.join(" && ")}`)));
|
|
3847
|
+
}
|
|
3430
3848
|
onRemoved();
|
|
3431
3849
|
try {
|
|
3432
3850
|
forgetStatusMeasures(path, result.reclaimedKeys);
|
|
@@ -3489,6 +3907,11 @@ async function removeOrRefuse(target, opts, onRemoved, output) {
|
|
|
3489
3907
|
}, opts.operation, opts.beforeRemove), {
|
|
3490
3908
|
signal: opts.operation?.signal,
|
|
3491
3909
|
declareSpawnsFor: opts.operation
|
|
3910
|
+
}).catch(async (error) => {
|
|
3911
|
+
if (archive && !gitRemoving) await discardWorktreeArchive(archive);
|
|
3912
|
+
throw error;
|
|
3913
|
+
}).finally(() => {
|
|
3914
|
+
if (archive) releaseWorktreeArchive(archive);
|
|
3492
3915
|
});
|
|
3493
3916
|
}
|
|
3494
3917
|
function registerRemove(worktree) {
|
|
@@ -3502,4 +3925,4 @@ function worktreeCommand(program) {
|
|
|
3502
3925
|
registerRemove(worktree);
|
|
3503
3926
|
}
|
|
3504
3927
|
//#endregion
|
|
3505
|
-
export {
|
|
3928
|
+
export { reclaimProject as A, measureSizes as C, stimCliEntries as D, serviceRunningFrom as E, sweepArchiveStaging as M, worktreeArchiveLimits as N, stimCliUseOf as O, measurePressureAsync as S, uncommittedActivityOf as T, worktreeCommand as _, matchWorktreeEntry as a, measureDiskAsync as b, reclaimKeys as c, removalBlockers as d, removalPath as f, warmedWorkspace as g, warmWorktreeTarget as h, excludeWatchmanCookies as i, deleteSelectedArchives as j, describeDereferenced as k, registerRemove as l, removeWorktreeTarget as m, dependencyInstallCommand as n, nestedWorktrees as o, removalRemedy as p, excludePodChurn as r, porcelainPath as s, customIosDirs as t, registerWarm as u, cacheBlocked as v, sizeScanDeferred as w, measurePressure as x, measureDisk as y };
|