stim 1.3.0 → 1.3.1

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.
Files changed (34) hide show
  1. package/README.md +4 -3
  2. package/dist/{android-CqcW1hrP.mjs → android-CT-PkXUh.mjs} +22 -19
  3. package/dist/{android-cas-BR5YJBYv.mjs → android-cas-DXorPCi_.mjs} +2 -2
  4. package/dist/{android-BqVIJoG7.mjs → android-hMj83VpN.mjs} +12 -6
  5. package/dist/{build-slots-C3-R4XQr.mjs → build-slots-C4AouyNz.mjs} +1 -1
  6. package/dist/cli.mjs +12 -12
  7. package/dist/{deps-Spz8ozgo.mjs → deps-0Yt0Inly.mjs} +1 -1
  8. package/dist/{dev-client-BjisHDB5.mjs → dev-client-VfSZNLT4.mjs} +4 -4
  9. package/dist/{device-BkiW-gkk.mjs → device-Bbv45Z9Z.mjs} +3 -3
  10. package/dist/{device-lease-BJYybF-2.mjs → device-lease-CJwK7OZr.mjs} +1 -1
  11. package/dist/{device-pool-CPbLGfrB.mjs → device-pool-CcBNTvM7.mjs} +1 -1
  12. package/dist/{device-remote-AkYCKS8U.mjs → device-remote-DAA0Zm6Z.mjs} +1 -1
  13. package/dist/{doctor-Bmv86UUW.mjs → doctor-B4EITgQB.mjs} +5 -5
  14. package/dist/{doctor-fAMXC4vl.mjs → doctor-BueDfLYG.mjs} +2 -2
  15. package/dist/{error-diagnostics-DHC31rXA.mjs → error-diagnostics-DwhNBFDU.mjs} +3 -3
  16. package/dist/{gc-GGu-8ULN.mjs → gc-BRu62v4o.mjs} +7 -7
  17. package/dist/{guide-92CPA21s.mjs → guide-D95E0u3i.mjs} +111 -61
  18. package/dist/{ios-BSXFeO5H.mjs → ios-DAju3TVy.mjs} +12 -12
  19. package/dist/{logs-DPNF4C9s.mjs → logs-CAdwW6c2.mjs} +29 -4
  20. package/dist/{logs-query-R-Cqc6Qv.mjs → logs-query-Cdp5aaCH.mjs} +1 -1
  21. package/dist/{ownership-BpCKiRlQ.mjs → ownership-DXcol4oU.mjs} +1 -1
  22. package/dist/{ownership-claim-Bf2CAfw3.mjs → ownership-claim-BtPtJVmq.mjs} +35 -21
  23. package/dist/{reclaim-iW9Cad7k.mjs → reclaim-CUmwucWG.mjs} +4 -4
  24. package/dist/{reload-DazHDSFA.mjs → reload-CWpXoOgN.mjs} +2 -2
  25. package/dist/{slot-launch-cci0xvmC.mjs → slot-launch-CZklomj6.mjs} +1 -1
  26. package/dist/{start-mObjQH2W.mjs → start-B5G4NjiA.mjs} +3 -3
  27. package/dist/{status-BxQNqu9t.mjs → status-CI9clRSb.mjs} +6 -6
  28. package/dist/{stop-CrHGcjrV.mjs → stop-CQhffpmX.mjs} +2 -2
  29. package/dist/{stop-DimGXEKY.mjs → stop-CjlgDk0a.mjs} +5 -5
  30. package/dist/{teardown-ClFp1Aax.mjs → teardown--gESs9ck.mjs} +103 -4
  31. package/dist/{workspace-process-lock-3NcQnZ0X.mjs → workspace-process-lock-IBj1Towr.mjs} +1 -1
  32. package/dist/{worktree-DxTgN1CJ.mjs → worktree-DtQGO9-s.mjs} +116 -106
  33. package/dist/{xcode-D7sAp-Zy.mjs → xcode-xnxyvJkD.mjs} +81 -44
  34. package/package.json +5 -5
@@ -5,13 +5,13 @@ import { f as listWorktrees } from "./worktree-Eq6TLH8N.mjs";
5
5
  import { a as findProjectRoot, c as projectShortcut } from "./project-fKmuIaMI.mjs";
6
6
  import { a as isMetroRunning, i as resolveProjectMetro } from "./metro-CORqXyTL.mjs";
7
7
  import { a as formatElapsed, r as clockTime, u as plural } from "./command-output-8wHMEde0.mjs";
8
- import { N as resolveOwnedAvdSerial } from "./android-BqVIJoG7.mjs";
9
- import { a as POOL_SETTING_REMEDY, c as parkedMaxSetting, l as readParked } from "./teardown-ClFp1Aax.mjs";
8
+ import { N as resolveOwnedAvdSerial } from "./android-hMj83VpN.mjs";
9
+ import { a as POOL_SETTING_REMEDY, c as parkedMaxSetting, l as readParked } from "./teardown--gESs9ck.mjs";
10
10
  import { u as listAllIosSims } from "./ios-Deh5js5V.mjs";
11
- import { i as listLeaseFiles } from "./device-lease-BJYybF-2.mjs";
12
- import { s as queryLogs } from "./logs-query-R-Cqc6Qv.mjs";
13
- import { t as resolveSupervisorTarget } from "./ownership-BpCKiRlQ.mjs";
14
- import { i as readSupervisorState } from "./stop-DimGXEKY.mjs";
11
+ import { i as listLeaseFiles } from "./device-lease-CJwK7OZr.mjs";
12
+ import { c as queryLogs } from "./logs-query-Cdp5aaCH.mjs";
13
+ import { t as resolveSupervisorTarget } from "./ownership-DXcol4oU.mjs";
14
+ import { i as readSupervisorState } from "./stop-CjlgDk0a.mjs";
15
15
  import { existsSync } from "fs";
16
16
  import { totalmem } from "os";
17
17
  import chalk from "chalk";
@@ -1,3 +1,3 @@
1
- import { t as resolveSupervisorTarget } from "./ownership-BpCKiRlQ.mjs";
2
- import { a as resolveCollectorTargets, i as readSupervisorState, n as clearSupervisorState, o as runStop, r as readCollectorState, s as stopCommand, t as clearCollectorState } from "./stop-DimGXEKY.mjs";
1
+ import { t as resolveSupervisorTarget } from "./ownership-DXcol4oU.mjs";
2
+ import { a as resolveCollectorTargets, i as readSupervisorState, n as clearSupervisorState, o as runStop, r as readCollectorState, s as stopCommand, t as clearCollectorState } from "./stop-CjlgDk0a.mjs";
3
3
  export { clearCollectorState, clearSupervisorState, stopCommand as default, readCollectorState, readSupervisorState, resolveCollectorTargets, resolveSupervisorTarget, runStop };
@@ -4,11 +4,11 @@ import { a as findProjectRoot } from "./project-fKmuIaMI.mjs";
4
4
  import { i as resolveProjectMetro, n as killMetroTree, r as pidExists } from "./metro-CORqXyTL.mjs";
5
5
  import { n as verifyCollectorOwnership } from "./ownership-8O073GIC.mjs";
6
6
  import { f as releasedLeaseFact, l as phaseLine, u as plural } from "./command-output-8wHMEde0.mjs";
7
- import { n as teardownOwnedIosSim, t as teardownOwnedAvd, x as resolveEasCliBin } from "./teardown-ClFp1Aax.mjs";
8
- import { t as withWorkspaceProcessLock } from "./workspace-process-lock-3NcQnZ0X.mjs";
9
- import { l as releaseWorkspaceLeases } from "./device-lease-BJYybF-2.mjs";
10
- import { i as endRecordedSession } from "./device-remote-AkYCKS8U.mjs";
11
- import { r as stopTunnel, t as resolveSupervisorTarget } from "./ownership-BpCKiRlQ.mjs";
7
+ import { n as teardownOwnedIosSim, t as teardownOwnedAvd, x as resolveEasCliBin } from "./teardown--gESs9ck.mjs";
8
+ import { t as withWorkspaceProcessLock } from "./workspace-process-lock-IBj1Towr.mjs";
9
+ import { l as releaseWorkspaceLeases } from "./device-lease-CJwK7OZr.mjs";
10
+ import { i as endRecordedSession } from "./device-remote-DAA0Zm6Z.mjs";
11
+ import { r as stopTunnel, t as resolveSupervisorTarget } from "./ownership-DXcol4oU.mjs";
12
12
  import chalk from "chalk";
13
13
  //#region src/commands/stop.ts
14
14
  const DEFAULT_WAIT_MS = 1e4;
@@ -4,7 +4,8 @@ import { r as createNdjsonWriter } from "./ndjson-BZwI1NBv.mjs";
4
4
  import { D as workspaceLogsDir } from "./state-B3XA2wp2.mjs";
5
5
  import { l as resolvePackageJson } from "./project-fKmuIaMI.mjs";
6
6
  import { n as stripAnsi } from "./process-output-C7IlIzd7.mjs";
7
- import { E as ownedAvdMatchesConfiguration, F as shutdownAndroidEmulator, I as waitForAndroidEmulatorShutdown, N as resolveOwnedAvdSerial, O as ownedAvdSystemImage, S as listOrphanedAvdDirectories, T as ownedAvdDirectory, l as avdStorageRoots, o as assertOwnedAvdStopped, p as deleteAvd, s as avdPathExists } from "./android-BqVIJoG7.mjs";
7
+ import { l as phaseLine } from "./command-output-8wHMEde0.mjs";
8
+ import { E as ownedAvdMatchesConfiguration, F as shutdownAndroidEmulator, I as waitForAndroidEmulatorShutdown, N as resolveOwnedAvdSerial, O as ownedAvdSystemImage, S as listOrphanedAvdDirectories, T as ownedAvdDirectory, l as avdStorageRoots, o as assertOwnedAvdStopped, p as deleteAvd, s as avdPathExists } from "./android-hMj83VpN.mjs";
8
9
  import { n as expoBinFromPackage, r as expoBinPath, s as findBinUpward } from "./server-expo-DizT4OMk.mjs";
9
10
  import { C as shutdownIosSim, _ as renameIosSim, a as deleteIosSim, d as listIosDeviceTypes, g as parseRuntimeVersion, h as parkedSimName, o as deleteParkedIosSim, r as clearAppDataContainer, s as findAppDataContainer, x as resolveOwnedIosSim } from "./ios-Deh5js5V.mjs";
10
11
  import { createRequire } from "node:module";
@@ -720,6 +721,77 @@ function dropParked(platform, udid) {
720
721
  return removeParkedAfter(platform, udid, () => {}) !== null;
721
722
  }
722
723
  //#endregion
724
+ //#region src/agent-device-cleanup.ts
725
+ function parseAgentDeviceSessions(output) {
726
+ const result = JSON.parse(output);
727
+ if (result?.success !== true || !Array.isArray(result.data?.sessions)) throw new Error("agent-device did not return a successful session list");
728
+ return result.data.sessions.flatMap((entry) => {
729
+ if (!entry || typeof entry !== "object") return [];
730
+ const row = entry;
731
+ const id = row.platform === "ios" ? row.device_udid : row.id;
732
+ if (row.platform !== "ios" && row.platform !== "android" || typeof row.name !== "string" || !row.name.trim() || typeof id !== "string" || !id || typeof row.createdAt !== "number" || !Number.isFinite(row.createdAt) || row.platform === "ios" && row.id !== void 0 && row.id !== id) return [];
733
+ return [{
734
+ name: row.name,
735
+ platform: row.platform,
736
+ id,
737
+ createdAt: row.createdAt
738
+ }];
739
+ });
740
+ }
741
+ function report(message) {
742
+ process.stderr.write(`${phaseLine("device", message)}\n`);
743
+ }
744
+ function closeOwnedDeviceSessions(device, stillOwned) {
745
+ const exec = getExecutor();
746
+ const deadline = Date.now() + 15e3;
747
+ const run = (args) => {
748
+ const remaining = deadline - Date.now();
749
+ if (remaining <= 0) throw new Error("agent-device cleanup timed out");
750
+ return exec.runFile("agent-device", [
751
+ ...args,
752
+ "--json",
753
+ "--daemon-transport",
754
+ "socket"
755
+ ], {
756
+ timeoutMs: Math.min(5e3, remaining),
757
+ killSignal: "SIGKILL"
758
+ });
759
+ };
760
+ const list = () => parseAgentDeviceSessions(run([
761
+ "session",
762
+ "list",
763
+ "--session",
764
+ "stim-teardown-inventory"
765
+ ]));
766
+ try {
767
+ if (!exec.runQuiet("command -v agent-device", {
768
+ timeoutMs: 2e3,
769
+ killSignal: "SIGKILL"
770
+ })) return;
771
+ const sessions = list().filter((session) => session.platform === device.platform && session.id === device.id);
772
+ for (const session of sessions) try {
773
+ if (!list().some((current) => current.name === session.name && current.platform === session.platform && current.id === session.id && current.createdAt === session.createdAt)) continue;
774
+ if (!stillOwned()) return;
775
+ if (JSON.parse(run([
776
+ "close",
777
+ "--session",
778
+ session.name,
779
+ "--session-lock",
780
+ "reject",
781
+ "--platform",
782
+ device.platform,
783
+ device.platform === "ios" ? "--udid" : "--serial",
784
+ device.id
785
+ ]))?.success !== true) throw new Error("agent-device did not confirm session close");
786
+ report(`closed agent-device session ${session.name} on ${device.id}`);
787
+ } catch (error) {
788
+ report(`could not close agent-device session ${session.name} on ${device.id}: ${error.message}`);
789
+ }
790
+ } catch (error) {
791
+ report(`could not list agent-device sessions for ${device.id}: ${error.message}`);
792
+ }
793
+ }
794
+ //#endregion
723
795
  //#region src/teardown.ts
724
796
  function teardownParkedAvd(avdName) {
725
797
  try {
@@ -746,7 +818,13 @@ function teardownParkedAvd(avdName) {
746
818
  }
747
819
  function teardownParkedIosSim(udid, { label, deleteSim = deleteParkedIosSim } = {}) {
748
820
  try {
749
- const removed = removeParkedAfter("ios", udid, () => deleteSim(udid));
821
+ const removed = removeParkedAfter("ios", udid, () => {
822
+ closeOwnedDeviceSessions({
823
+ platform: "ios",
824
+ id: udid
825
+ }, () => Boolean(resolveOwnedIosSim(udid).sim));
826
+ deleteSim(udid);
827
+ });
750
828
  if (!removed) return {
751
829
  status: "skipped",
752
830
  kind: "not-parked",
@@ -812,6 +890,17 @@ function teardownOwnedIosSim(udid, { del = false, label, park } = {}) {
812
890
  reason: `sim is now named "${resolved.notOwned}", not Stim-owned by name`
813
891
  };
814
892
  if (resolved.missing) return { status: "missing" };
893
+ closeOwnedDeviceSessions({
894
+ platform: "ios",
895
+ id: udid
896
+ }, () => Boolean(resolveOwnedIosSim(udid).sim));
897
+ const current = resolveOwnedIosSim(udid);
898
+ if (current.missing) return { status: "missing" };
899
+ if (current.notOwned) return {
900
+ status: "skipped",
901
+ kind: "not-owned",
902
+ reason: "simulator ownership changed before shutdown"
903
+ };
815
904
  shutdownIosSim(udid);
816
905
  const sim = resolved.sim;
817
906
  if (del && park && park.max > 0) try {
@@ -916,8 +1005,18 @@ function teardownOwnedAvd(avdName, { del = false, park, owner, orphanedDirectory
916
1005
  reason: "AVD registration appeared; its data was kept."
917
1006
  };
918
1007
  const serial = resolved.serial;
919
- if (serial) waitForShutdown(avdName, (timeoutMs) => shutdownAndroidEmulator(serial, timeoutMs));
920
- else assertStopped(avdName);
1008
+ if (serial) {
1009
+ const stillOwned = () => {
1010
+ const current = resolveAvd(avdName);
1011
+ return !current.notOwned && !current.missing && current.serial === serial;
1012
+ };
1013
+ closeOwnedDeviceSessions({
1014
+ platform: "android",
1015
+ id: serial
1016
+ }, stillOwned);
1017
+ if (!stillOwned()) throw new Error(`Owned AVD ${avdName} changed before shutdown; it was kept.`);
1018
+ waitForShutdown(avdName, (timeoutMs) => shutdownAndroidEmulator(serial, timeoutMs));
1019
+ } else assertStopped(avdName);
921
1020
  if (del) {
922
1021
  const current = resolveAvd(avdName);
923
1022
  if (current.notOwned) return {
@@ -1,4 +1,4 @@
1
- import { f as releaseClaim, h as tryAcquireClaim } from "./ownership-claim-Bf2CAfw3.mjs";
1
+ import { d as releaseClaim, m as tryAcquireClaim } from "./ownership-claim-BtPtJVmq.mjs";
2
2
  import { join } from "node:path";
3
3
  //#region src/engine/workspace-process-lock.ts
4
4
  const DEFAULT_WAIT_MS = 6e4;
@@ -4,14 +4,14 @@ import { c as readMetroTunnel, u as readRemoteSession } from "./state-B3XA2wp2.m
4
4
  import { _ as resolveFullRef, b as warmWorktreePaths, c as hasRemote, d as isPodInstallChurn, f as listWorktrees, h as removeWorktree, i as depsOutOfSync, l as hasUncommittedWork, m as readWorktreeExclude, n as cloneIgnoredEntries, o as dirtyPaths, p as podsOutOfSync, r as deleteBranch, s as gitCommonDir, t as branchExists, u as isMainWorkingTree, v as restoreFile, y as unpushedCommits } from "./worktree-Eq6TLH8N.mjs";
5
5
  import { a as findProjectRoot } from "./project-fKmuIaMI.mjs";
6
6
  import { a as formatElapsed, f as releasedLeaseFact, i as formatDuration, l as phaseLine, m as shortUdid, u as plural } from "./command-output-8wHMEde0.mjs";
7
- import { a as clearClaimChild, c as isClaimUnavailable, d as readClaimSet, f as releaseClaim, h as tryAcquireClaim, i as claimRemoveCommand, l as markClaimChildPending, m as settleClaim, p as setClaimChild, r as claimFailure, s as isClaimRefusal, u as processGroupAlive } from "./ownership-claim-Bf2CAfw3.mjs";
7
+ import { a as clearClaimChild, c as markClaimChildPending, d as releaseClaim, f as setClaimChild, i as claimRemoveCommand, l as processGroupAlive, m as tryAcquireClaim, n as ClaimUnavailableError, p as settleClaim, r as claimFailure } from "./ownership-claim-BtPtJVmq.mjs";
8
8
  import { A as resolveSettings, I as unknownSettingKeys, N as settingShapeErrors, a as SETTING_SHAPE_REMEDY } from "./settings-DEJf6_kJ.mjs";
9
- import { a as POOL_SETTING_REMEDY, c as parkedMaxSetting } from "./teardown-ClFp1Aax.mjs";
10
- import { g as podInstallCommand, r as HEARTBEAT_INTERVAL_MS } from "./xcode-D7sAp-Zy.mjs";
11
- import { a as installedNpmTreeIsValid, i as hasInstalledDependencies, o as locallyKnownUpstream, t as dependencyState } from "./doctor-Bmv86UUW.mjs";
12
- import { c as withManagedTunnelRemovalLock, o as withManagedRemoteWorktreeRemovalLock } from "./ownership-BpCKiRlQ.mjs";
13
- import { n as reclaimProject } from "./reclaim-iW9Cad7k.mjs";
14
- import { a as runPodInstall, i as runCaptured, n as podsAreStale, r as readPodState, t as DEPS_ERROR } from "./deps-Spz8ozgo.mjs";
9
+ import { a as POOL_SETTING_REMEDY, c as parkedMaxSetting } from "./teardown--gESs9ck.mjs";
10
+ import { g as podInstallCommand, r as HEARTBEAT_INTERVAL_MS } from "./xcode-xnxyvJkD.mjs";
11
+ import { a as installedNpmTreeIsValid, i as hasInstalledDependencies, o as locallyKnownUpstream, t as dependencyState } from "./doctor-B4EITgQB.mjs";
12
+ import { c as withManagedTunnelRemovalLock, o as withManagedRemoteWorktreeRemovalLock } from "./ownership-DXcol4oU.mjs";
13
+ import { n as reclaimProject } from "./reclaim-CUmwucWG.mjs";
14
+ import { a as runPodInstall, i as runCaptured, n as podsAreStale, r as readPodState, t as DEPS_ERROR } from "./deps-0Yt0Inly.mjs";
15
15
  import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
16
16
  import { createHash, randomUUID } from "node:crypto";
17
17
  import { join, relative } from "node:path";
@@ -56,57 +56,10 @@ function asHolder(holder) {
56
56
  function waitingLine(holder, elapsedMs) {
57
57
  return phaseLine("lock", `waiting ${formatElapsed(elapsedMs)} for ${warmCommand(holder.phase)} (pid ${holder.pid}) -- stim guide lifecycle options`);
58
58
  }
59
- function warmClaimAcquiredLine(wait) {
60
- if (!wait.holder || wait.waitedMs <= 0) return phaseLine("lock", "acquired");
61
- return phaseLine("lock", `acquired (waited ${formatElapsed(wait.waitedMs)} for ${warmCommand(wait.holder.phase)} pid ${wait.holder.pid}) -- stim guide lifecycle options`);
62
- }
63
- function warmClaimUnavailableLine(reason) {
64
- return phaseLine("lock", `unavailable (${reason}); copying without it`);
65
- }
66
- /**
67
- * The reason a copy may proceed without the claim, or null when it must refuse. Plain `warm` worked on an
68
- * unwritable STIM_HOME before any claim existed and only reads, so the states in which no claim can be
69
- * recorded at all degrade to an unsynchronised copy: no process identity, a claim store the filesystem
70
- * rejects, and a claim store whose own path holds a file. A claim record Stim cannot resolve and a wait
71
- * that ran out are refusals: neither state can exist unless the claim does, so refusing them takes
72
- * nothing away.
73
- */
74
- function warmClaimDegradation(error) {
75
- if (isClaimUnavailable(error)) return error.message;
76
- if (isClaimRefusal(error)) return error.reason === "the claim path is a file, not a claim directory" ? `${error.claimPath}: ${error.reason}` : null;
77
- const code = error?.code;
78
- if (typeof code === "string" && code.startsWith("STIM_")) return null;
79
- return error?.message ?? String(error);
80
- }
81
- /**
82
- * What a copy that could not record a claim of its own would overlap if it went ahead anyway. Reading the
83
- * claim set classifies without writing anything and without refusing, so it works in exactly the states
84
- * that made the claim unrecordable -- a read-only STIM_HOME included. It is a read before a copy rather
85
- * than a held claim, so a refresh that starts after it is not covered; it closes the window in which one
86
- * is already installing, which is the window a refresh actually occupies.
87
- */
88
- function warmClaimBlocker(repositoryRoot) {
89
- const survey = readClaimSet(warmClaimPath(repositoryRoot));
90
- const refresh = survey.live.find((holder) => holder.mode === "exclusive");
91
- if (refresh) return {
92
- kind: "refresh",
93
- holder: asHolder(refresh)
94
- };
95
- const unresolved = survey.unresolved.find((problem) => problem.path.endsWith(".claim"));
96
- if (unresolved) return {
97
- kind: "unresolved",
98
- path: unresolved.path,
99
- reason: unresolved.reason
100
- };
101
- return null;
102
- }
103
- function warmClaimBlockedLine(reason, blocker) {
104
- const what = blocker.kind === "refresh" ? `${warmCommand(blocker.holder.phase)} (pid ${blocker.holder.pid}) holds this repository` : `a claim at ${blocker.path} cannot be resolved: ${blocker.reason}`;
105
- return phaseLine("lock", `unavailable (${reason}); ${what}`);
106
- }
107
- function warmClaimBlockedRefusal(blocker) {
108
- if (blocker.kind === "refresh") return "Refusing to copy from a source checkout a refresh is rewriting, even without a claim of its own. Wait for that refresh to finish, then run warm again.";
109
- return `Refusing to copy past a claim Stim cannot resolve. If nothing is warming, remove it and run warm again:\n ${claimRemoveCommand(blocker.path)}`;
59
+ function warmClaimAcquiredLine(wait, detail = "") {
60
+ const acquired = `acquired${detail ? ` ${detail}` : ""}`;
61
+ if (!wait.holder || wait.waitedMs < 1e3) return phaseLine("lock", acquired);
62
+ return phaseLine("lock", `${acquired} (waited ${formatElapsed(wait.waitedMs)} for ${warmCommand(wait.holder.phase)} pid ${wait.holder.pid}) -- stim guide lifecycle options`);
110
63
  }
111
64
  function installerWaitingLine(pid, elapsedMs) {
112
65
  return phaseLine("lock", `waiting on the install this refresh spawned (pid ${pid}, ${formatElapsed(elapsedMs)} elapsed) -- stim guide lifecycle options`);
@@ -183,7 +136,7 @@ function timeoutError(holder, elapsedMs, root) {
183
136
  error.code = "STIM_LOCK_TIMEOUT";
184
137
  return error;
185
138
  }
186
- async function acquireWarmClaim({ repositoryRoot, phase, now = Date.now, sleep = defaultSleep, pollMs = WARM_CLAIM_POLL_MS, progressMs = WARM_CLAIM_PROGRESS_MS, ceilingMs = WARM_CLAIM_CEILING_MS, out = () => {} }) {
139
+ async function acquireWarmClaim({ repositoryRoot, phase, mode = phase === "refresh" ? "exclusive" : "shared", now = Date.now, sleep = defaultSleep, pollMs = WARM_CLAIM_POLL_MS, progressMs = WARM_CLAIM_PROGRESS_MS, ceilingMs = WARM_CLAIM_CEILING_MS, out = () => {} }) {
187
140
  const root = warmClaimPath(repositoryRoot);
188
141
  const started = now();
189
142
  let lastProgress = started;
@@ -193,7 +146,7 @@ async function acquireWarmClaim({ repositoryRoot, phase, now = Date.now, sleep =
193
146
  for (;;) {
194
147
  const attempt = pending ? settleClaim(pending) : tryAcquireClaim({
195
148
  root,
196
- mode: phase === "refresh" ? "exclusive" : "shared",
149
+ mode,
197
150
  label: WARM_CLAIM_LABEL
198
151
  });
199
152
  const acquired = attempt.acquired;
@@ -228,6 +181,12 @@ async function acquireWarmClaim({ repositoryRoot, phase, now = Date.now, sleep =
228
181
  }
229
182
  } catch (error) {
230
183
  releaseClaim(pending);
184
+ if ([
185
+ "EACCES",
186
+ "EPERM",
187
+ "EROFS",
188
+ "ENOENT"
189
+ ].includes(error?.code ?? "")) throw new ClaimUnavailableError(error.message, { claimPath: root });
231
190
  throw error;
232
191
  }
233
192
  }
@@ -521,6 +480,38 @@ function branchRemote(root, branch) {
521
480
  `branch.${branch}.remote`
522
481
  ])?.trim() || "origin";
523
482
  }
483
+ function remoteUpstreamIsCurrent(root, branch) {
484
+ const exec = getExecutor();
485
+ const merge = exec.runFileQuiet("git", [
486
+ "-C",
487
+ root,
488
+ "config",
489
+ "--get-all",
490
+ `branch.${branch}.merge`
491
+ ])?.trim();
492
+ if (!merge) return true;
493
+ if (!merge.startsWith("refs/") || merge.includes("\n")) return false;
494
+ const remote = branchRemote(root, branch);
495
+ if (remote === ".") return true;
496
+ const local = resolveFullRef(root, "@{upstream}");
497
+ if (!local) return false;
498
+ const advertised = exec.runFileQuiet("git", [
499
+ "-C",
500
+ root,
501
+ "ls-remote",
502
+ "--exit-code",
503
+ "--refs",
504
+ "--",
505
+ remote,
506
+ merge
507
+ ], {
508
+ timeoutMs: FETCH_TIMEOUT_MS,
509
+ env: { GIT_TERMINAL_PROMPT: "0" }
510
+ });
511
+ if (advertised === null) return false;
512
+ const refs = advertised.trim().split("\n").map((line) => line.split(/\s+/));
513
+ return refs.length === 1 && refs[0]?.[0] === local && refs[0]?.[1] === merge;
514
+ }
524
515
  function gitPath(...parts) {
525
516
  return parts.filter((part) => part && part !== ".").join("/");
526
517
  }
@@ -603,7 +594,7 @@ function fastForward(root, upstream) {
603
594
  };
604
595
  }
605
596
  }
606
- async function refreshMainCheckout({ root, appDir, settings, emit, spawnFn = null, now = Date.now, heartbeatMs = HEARTBEAT_INTERVAL_MS, installer = NO_INSTALLER_CLAIM }) {
597
+ async function refreshMainCheckout({ root, appDir, settings, emit, spawnFn = null, now = Date.now, heartbeatMs = HEARTBEAT_INTERVAL_MS, installer = NO_INSTALLER_CLAIM, inspectOnly = false }) {
607
598
  const state = readMainCheckoutState(root);
608
599
  const refusal = mainCheckoutRefusal(root, state);
609
600
  if (refusal) return refusal;
@@ -640,7 +631,9 @@ async function refreshMainCheckout({ root, appDir, settings, emit, spawnFn = nul
640
631
  };
641
632
  };
642
633
  const before = resolveFullRef(root, "HEAD") ?? "";
643
- if (getExecutor().runFileQuiet("git", [
634
+ if (inspectOnly) {
635
+ if (!remoteUpstreamIsCurrent(root, branch)) return { mutation: "fetch upstream" };
636
+ } else if (getExecutor().runFileQuiet("git", [
644
637
  "-C",
645
638
  root,
646
639
  "fetch",
@@ -652,6 +645,7 @@ async function refreshMainCheckout({ root, appDir, settings, emit, spawnFn = nul
652
645
  }) === null) emit(phaseLine("checkout", "could not fetch; continuing with the local state"));
653
646
  const plan = checkoutPlan(locallyKnownUpstream(root));
654
647
  if (plan.kind === "diverged") return divergedRefusal(root, branch, plan);
648
+ if (inspectOnly && plan.kind === "fast-forward") return { mutation: `fast-forward ${plan.behind} commit${plan.behind === 1 ? "" : "s"}` };
655
649
  const depsBefore = dependencyState(existsSync(appDir) ? appDir : root);
656
650
  const priorInstall = depsBefore ? readInstallEvidence(depsBefore.root) : null;
657
651
  const marked = plan.kind === "fast-forward" ? depsBefore : null;
@@ -683,6 +677,7 @@ async function refreshMainCheckout({ root, appDir, settings, emit, spawnFn = nul
683
677
  });
684
678
  const depsSubject = `source ${dependencies?.root ?? root}: ${deps.reason}`;
685
679
  if (deps.run && dependencies) {
680
+ if (inspectOnly) return { mutation: deps.reason };
686
681
  writeInstallEvidence(dependencies.root, dependencies.lock, consumed, false);
687
682
  const install = await runInstallCommand({
688
683
  command: dependencies.command,
@@ -719,6 +714,7 @@ async function refreshMainCheckout({ root, appDir, settings, emit, spawnFn = nul
719
714
  emit(stepLine("pods", `${where}${pods.reason}`, "skipped"));
720
715
  return null;
721
716
  }
717
+ if (inspectOnly) return { mutation: pods.reason };
722
718
  const result = await runPodInstall(appDir, null, {
723
719
  spawnFn: spawn,
724
720
  now,
@@ -793,31 +789,6 @@ function registerWarm(worktree) {
793
789
  for (const key of unknownSettingKeys(settings)) console.error(chalk.yellow(`Warning: setting "${key}" is not read by Stim and will be ignored.`));
794
790
  return settings;
795
791
  };
796
- if (opts.refresh) {
797
- const settings = readSettings();
798
- if (!settings) {
799
- process.exitCode = 1;
800
- return;
801
- }
802
- const failure = await withWarmClaim({
803
- repositoryRoot: root,
804
- phase: "refresh",
805
- out: console.error
806
- }, (hold) => {
807
- console.error(warmClaimAcquiredLine(hold.wait));
808
- return refreshMainCheckout({
809
- root,
810
- appDir: mainCheckoutAppDir(root, target),
811
- settings,
812
- emit: (line) => console.error(line),
813
- installer: hold.installer
814
- });
815
- });
816
- if (failure) {
817
- reportRefreshFailure(failure);
818
- return;
819
- }
820
- }
821
792
  const copy = (wait) => {
822
793
  if (wait?.holder) console.error(warmClaimAcquiredLine(wait));
823
794
  const incomplete = incompleteInstallRefusal(root, mainCheckoutAppDir(root, target));
@@ -846,30 +817,69 @@ function registerWarm(worktree) {
846
817
  console.error(phaseLine("carry", `${result.failed.length ? "incomplete" : "complete"}: ${result.copied.length} ignored entries copied, ${result.skipped.length} kept, ${result.failed.length} failed`));
847
818
  if (result.failed.length) process.exitCode = 1;
848
819
  };
849
- let hold = null;
850
- try {
851
- hold = await acquireWarmClaim({
820
+ if (opts.refresh) {
821
+ const inspect = await acquireWarmClaim({
852
822
  repositoryRoot: root,
853
- phase: "copy",
823
+ phase: "refresh",
824
+ mode: "shared",
854
825
  out: console.error
855
826
  });
856
- } catch (error) {
857
- const degraded = opts.refresh ? null : warmClaimDegradation(error);
858
- if (degraded === null) throw error;
859
- const blocker = warmClaimBlocker(root);
860
- if (blocker) {
861
- console.error(chalk.dim(warmClaimBlockedLine(degraded, blocker)));
862
- console.error(chalk.red(warmClaimBlockedRefusal(blocker)));
863
- throw error;
827
+ let mutation;
828
+ try {
829
+ const settings = readSettings();
830
+ if (!settings) {
831
+ process.exitCode = 1;
832
+ return;
833
+ }
834
+ const lines = [];
835
+ const result = await refreshMainCheckout({
836
+ root,
837
+ appDir: mainCheckoutAppDir(root, target),
838
+ settings,
839
+ emit: (line) => lines.push(line),
840
+ inspectOnly: true
841
+ });
842
+ if (!result || !("mutation" in result)) {
843
+ console.error(warmClaimAcquiredLine(inspect.wait, result ? "shared" : "shared (seed current)"));
844
+ for (const line of lines) console.error(line);
845
+ if (result) reportRefreshFailure(result);
846
+ else copy(null);
847
+ return;
848
+ }
849
+ mutation = result.mutation;
850
+ } finally {
851
+ inspect.release();
864
852
  }
865
- console.error(chalk.dim(warmClaimUnavailableLine(degraded)));
866
- }
867
- if (!hold) copy(null);
868
- else try {
869
- copy(hold.wait);
870
- } finally {
871
- hold.release();
853
+ const failure = await withWarmClaim({
854
+ repositoryRoot: root,
855
+ phase: "refresh",
856
+ out: console.error
857
+ }, async (hold) => {
858
+ console.error(warmClaimAcquiredLine(hold.wait, `exclusive (${mutation})`));
859
+ const settings = readSettings();
860
+ if (!settings) {
861
+ process.exitCode = 1;
862
+ return null;
863
+ }
864
+ return refreshMainCheckout({
865
+ root,
866
+ appDir: mainCheckoutAppDir(root, target),
867
+ settings,
868
+ emit: (line) => console.error(line),
869
+ installer: hold.installer
870
+ });
871
+ });
872
+ if (failure) {
873
+ reportRefreshFailure(failure);
874
+ return;
875
+ }
876
+ if (process.exitCode) return;
872
877
  }
878
+ await withWarmClaim({
879
+ repositoryRoot: root,
880
+ phase: "copy",
881
+ out: console.error
882
+ }, (hold) => copy(hold.wait));
873
883
  } catch (error) {
874
884
  const code = error?.code;
875
885
  console.error(chalk.red(`Could not warm this worktree: ${error.message}`));