@devrouter/cli 0.0.61 → 0.0.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/devrouter.js CHANGED
@@ -2450,7 +2450,7 @@ function loadRepoConfig(repoPath, read = (file) => import_node_fs6.default.readF
2450
2450
  const config = parseConfig(parsed ?? {}, configPath);
2451
2451
  const requiredVersion = config.devrouter?.version;
2452
2452
  if (requiredVersion && !hasWarnedVersionMismatch) {
2453
- const cliVersion = true ? "0.0.61" : "0.0.0-dev";
2453
+ const cliVersion = true ? "0.0.63" : "0.0.0-dev";
2454
2454
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
2455
2455
  hasWarnedVersionMismatch = true;
2456
2456
  process.stderr.write(
@@ -4558,6 +4558,19 @@ function parseManagedStopJson(output2) {
4558
4558
  throw new Error("Managed stop Docker response is malformed.");
4559
4559
  }
4560
4560
  }
4561
+ function resolveManagedStopEndpoint() {
4562
+ const endpoint = process.env.DOCKER_HOST && !process.env.DOCKER_CONTEXT ? process.env.DOCKER_HOST : parseManagedStopJson(
4563
+ runManagedStopDocker([
4564
+ "context",
4565
+ "inspect",
4566
+ "--format",
4567
+ "{{json .Endpoints.docker.Host}}"
4568
+ ])
4569
+ );
4570
+ if (typeof endpoint !== "string") throw new Error("Docker endpoint is unavailable.");
4571
+ supportsManagedStopBaseline(endpoint);
4572
+ return endpoint;
4573
+ }
4561
4574
  function inspectManagedStopDaemon(endpoint) {
4562
4575
  const value = parseManagedStopJson(
4563
4576
  runManagedStopDocker(["info", "--format", "{{json .ID}}"], endpoint)
@@ -4579,6 +4592,28 @@ function inspectManagedStopRunnerId(endpoint, containerId) {
4579
4592
  throw new Error("Provider container binding is unavailable.");
4580
4593
  return value;
4581
4594
  }
4595
+ function inspectProviderRunnerContainers(endpoint, runnerId) {
4596
+ if (!runnerId || runnerId.length > 4096 || /[\r\n\0]/.test(runnerId))
4597
+ throw new Error("Provider runner identity is malformed.");
4598
+ const ids = parseDockerLines(
4599
+ runManagedStopDocker(
4600
+ [
4601
+ "ps",
4602
+ "-a",
4603
+ "--no-trunc",
4604
+ "--filter",
4605
+ `label=dev.containers.id=${runnerId}`,
4606
+ "--format",
4607
+ "{{.ID}}"
4608
+ ],
4609
+ endpoint
4610
+ )
4611
+ );
4612
+ if (ids.length > 256) throw new Error("Provider runner population exceeds its bound.");
4613
+ ids.forEach(assertFullContainerId);
4614
+ assertUniqueContainerIds(ids);
4615
+ return ids;
4616
+ }
4582
4617
  function inspectManagedStopWorkspaceIds(endpoint, composeDirectory) {
4583
4618
  const ids = parseDockerLines(
4584
4619
  runManagedStopDocker(
@@ -4599,6 +4634,33 @@ function inspectManagedStopWorkspaceIds(endpoint, composeDirectory) {
4599
4634
  assertUniqueContainerIds(ids);
4600
4635
  return ids;
4601
4636
  }
4637
+ function hasExactManagedStopMissingObjectError(stderr, id3) {
4638
+ if (typeof stderr !== "string") return false;
4639
+ const line = stderr.endsWith("\r\n") ? stderr.slice(0, -2) : stderr.endsWith("\n") ? stderr.slice(0, -1) : stderr;
4640
+ return line.toLowerCase() === `error: no such object: ${id3}`;
4641
+ }
4642
+ function assertManagedStopContainersAbsent(endpoint, ids) {
4643
+ assertManagedStopEndpoint(endpoint);
4644
+ if (!Array.isArray(ids) || ids.length === 0 || ids.length > 256) {
4645
+ throw new Error("Managed stop absence proof requires between 1 and 256 container ids.");
4646
+ }
4647
+ ids.forEach(assertFullContainerId);
4648
+ assertUniqueContainerIds(ids);
4649
+ const env = { ...process.env };
4650
+ delete env.DOCKER_CONTEXT;
4651
+ delete env.DOCKER_HOST;
4652
+ for (const id3 of ids) {
4653
+ const result = (0, import_node_child_process6.spawnSync)("docker", ["--host", endpoint, "inspect", "--format", "{{.Id}}", id3], {
4654
+ env,
4655
+ encoding: "utf-8",
4656
+ timeout: MANAGED_STOP_DOCKER_TIMEOUT_MS,
4657
+ maxBuffer: MANAGED_STOP_DOCKER_MAX_BUFFER
4658
+ });
4659
+ if (result.status !== 1 || result.error || result.signal || result.stdout !== "" && result.stdout !== "\n" && result.stdout !== "\r\n" || !hasExactManagedStopMissingObjectError(result.stderr, id3)) {
4660
+ throw new Error("Managed stop Docker inspection did not prove container absence.");
4661
+ }
4662
+ }
4663
+ }
4602
4664
  function stopPinnedManagedContainer(endpoint, containerId) {
4603
4665
  assertFullContainerId(containerId);
4604
4666
  runManagedStopDocker(["stop", containerId], endpoint, 3e4);
@@ -7106,6 +7168,8 @@ function buildOnboardingPrompt(options = {}) {
7106
7168
  `- When a workspace is active: hosts auto-namespace (\`web.localhost\` \u2192 \`web.<ws>.localhost\`), \`${WORKSPACE_PLACEHOLDER}\` in \`upstream\` is substituted with the token, and the docker \`router\` key is suffixed per workspace. Managed ensure rejects every HTTP/TCP upstream outside that exact alias namespace before mutation. The runtime config is computed in memory only \u2014 the committed \`.devrouter.yml\` is never rewritten.`,
7107
7169
  "- TLS: namespaced hosts (`web.<ws>.localhost`) are not covered by the `*.localhost` wildcard; devrouter auto-extends the mkcert cert SANs for active hosts when TLS is enabled.",
7108
7170
  "- Lifecycle: after one-time setup, use `devrouter ensure .` for both primary and linked checkouts; never branch on checkout kind or use live verify as startup. Managed consumer images contain no devrouter package/helper: ensure delivers its matching helper at runtime and invokes an exact captured snapshot of the repository-owned post-start adapter. Keep `.devrouter.yml` as the only consumer-side version pin. Use `devrouter stop .` for a non-destructive pause, `devrouter stop . --delete` only for explicit exact-owner cleanup without removing the checkout, and `devrouter exec . -- <command...>` for container commands. Never substitute raw DevPod/Devsy mutations; they bypass the machine-global ownership lock, which serializes provider mutations in a fair arrival-order queue and lets contenders wait up to thirty minutes with throttled stderr progress before failing with the queue position or holder PID and true durations. `workspace up` creates linked worktrees; destructive worktree removal and GC remain ledger-scoped.",
7171
+ "- After a drained interrupted ensure, manual exec can use an exactly proven retained Devsy runtime with the plain local Docker command. Revalidate identity for each tooling command until later preparation reconciles startup; preserve configuration, data and interrupted history. This does not repair absent stop baselines or provide OOM protection.",
7172
+ "- Busy exec waits up to thirty minutes for a positively identified active lifecycle worker on the same checkout, with stderr progress. Do not stop healthy work to make room. Commands stay serial; timeout or cancellation before admission leaves prior work untouched. A lifecycle fence change cancels admission, and uncertain execution is never replayed.",
7109
7173
  "- Ensure reconciles interrupted or never-dispatched ensure after positive worker drainage. Unknown arbitrary exec never replays; use explicit stop to reconcile that uncertainty. Preserve journals and generated configuration if evidence is unavailable. This does not prevent OOM or activate capacity-managed recovery.",
7110
7174
  "- Devsy agent readiness: run `devrouter setup --yes --workspace-runtime devsy` once so Devrouter acquires and verifies the pinned agent in its own machine cache. `doctor` reports `ready`, `missing`, `stale`, or `invalid` without network access, and `ensure` fails before the provider queue when readiness is not `ready`. An explicit `DEVSY_AGENT_BINARY` remains authoritative and must match a pinned official asset.",
7111
7175
  "- A managed adapter plus `postCreateCommand` requires `waitFor` exactly `postCreateCommand` or `postStartCommand`; managed selective config preserves lifecycle fields and changes only `runServices`.",
@@ -8689,6 +8753,9 @@ var init_devcontainer_diagnostics = __esm({
8689
8753
  });
8690
8754
 
8691
8755
  // src/core/route-state.ts
8756
+ function listRoutesForWorktreePath(worktreePath, routes = listHostRouteState()) {
8757
+ return routes.filter((route) => sameWorkspacePath(route.repoPath, worktreePath));
8758
+ }
8692
8759
  function listRoutesForWorktreePaths(worktreePaths) {
8693
8760
  const routes = listHostRouteState();
8694
8761
  const byWorktreePath = /* @__PURE__ */ new Map();
@@ -9548,11 +9615,13 @@ var init_status = __esm({
9548
9615
  });
9549
9616
 
9550
9617
  // src/core/devpod-registry.ts
9551
- function listDevpodWorkspacesRaw() {
9618
+ function listDevpodWorkspacesRaw(options = {}) {
9552
9619
  const result = (0, import_node_child_process12.spawnSync)("devpod", ["list", "--output", "json", "--skip-pro"], {
9553
9620
  encoding: "utf-8"
9554
9621
  });
9555
- if (result.status !== 0) {
9622
+ if (options.allowMissingExecutable && result.error?.code === "ENOENT")
9623
+ return [];
9624
+ if (result.error || result.status !== 0) {
9556
9625
  const details = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
9557
9626
  throw new Error(`devpod list failed: ${details || "devpod is not installed or unavailable"}`);
9558
9627
  }
@@ -9634,6 +9703,10 @@ function listDevsyWorkspaces() {
9634
9703
  ...typeof candidate.uid === "string" ? { uid: candidate.uid } : {},
9635
9704
  ...typeof candidate.context === "string" ? { context: candidate.context } : {}
9636
9705
  };
9706
+ const provider = candidate.provider;
9707
+ if (provider && typeof provider.name === "string") workspace.providerName = provider.name;
9708
+ if (typeof provider?.options?.DOCKER_PATH?.value === "string")
9709
+ workspace.dockerPathOption = provider.options.DOCKER_PATH.value;
9637
9710
  if ("lastUsed" in candidate) {
9638
9711
  if (typeof candidate.lastUsed === "string") {
9639
9712
  workspace.lastUsed = candidate.lastUsed;
@@ -9691,6 +9764,16 @@ function inspectDevsyWorkspaceOwnership(workspaces, devsyId, worktreePath) {
9691
9764
  }
9692
9765
  return exact[0] ? { status: "owned", workspace: exact[0] } : { status: "absent" };
9693
9766
  }
9767
+ function selectDevsyWorkspace(workspaces, repoPath) {
9768
+ const matches = workspaces.filter(
9769
+ (workspace) => sameWorkspacePath(workspace.source.localFolder, repoPath)
9770
+ );
9771
+ if (matches.length > 1) {
9772
+ const ids = matches.map((match) => match.id).join(", ");
9773
+ throw new Error(`Multiple Devsy workspaces reference '${repoPath}': ${ids}`);
9774
+ }
9775
+ return matches[0];
9776
+ }
9694
9777
  var import_node_child_process13;
9695
9778
  var init_devsy_workspaces = __esm({
9696
9779
  "src/core/devsy-workspaces.ts"() {
@@ -10904,7 +10987,56 @@ function proveRetainedManagedStop(state) {
10904
10987
  }
10905
10988
  return result;
10906
10989
  }
10990
+ function absentRegistrationIdentity(state) {
10991
+ resetWorkspaceRuntimeCaches();
10992
+ if (resolveWorkspaceRuntimeOrDefault(state.repoPath) !== "devsy")
10993
+ throw new Error("Stop provider changed.");
10994
+ if (!state.workspace || !isLinkedWorktree(state.repoPath))
10995
+ throw new Error("Absent stop requires a linked workspace.");
10996
+ const record4 = readWorkspaceOwnership(state.repoPath, state.workspace);
10997
+ if (!record4 || record4.workspace !== state.workspace || record4.devpodId !== state.devpodId || !sameWorkspacePath(record4.worktreePath, state.repoPath) || resolveWorktreeWorkspace(state.repoPath) !== state.workspace || inspectWorkspaceOwnership(record4, listGitWorktrees(state.repoPath), void 0).ownerStatus !== "present")
10998
+ throw new Error("Absent stop workspace ownership changed.");
10999
+ for (const entries of [
11000
+ listDevsyWorkspaces(),
11001
+ listDevpodWorkspacesRaw({ allowMissingExecutable: true })
11002
+ ]) {
11003
+ if (entries.some(
11004
+ (entry) => entry.id === state.devpodId || sameWorkspacePath(entry.source.localFolder, state.repoPath)
11005
+ ))
11006
+ throw new Error("Absent stop requires both provider registrations to remain absent.");
11007
+ }
11008
+ return { record: record4, gitCommonDir: resolveGitCommonDir(state.repoPath) };
11009
+ }
11010
+ function proveManagedStop(state) {
11011
+ const baseline = validateManagedStopBaseline(state.stopBaseline, state);
11012
+ const owner = inspectDevsyWorkspaceOwnership(
11013
+ listDevsyWorkspaces(),
11014
+ state.devpodId,
11015
+ state.repoPath
11016
+ );
11017
+ if (owner.status !== "absent")
11018
+ return { status: "retained", containers: proveRetainedManagedStop(state) };
11019
+ if (baseline.sourceContainer) throw new Error("Absent stop cannot prove a source container.");
11020
+ const before = absentRegistrationIdentity(state);
11021
+ if (!(0, import_node_util2.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state))
11022
+ throw new Error("Retained stop generation changed.");
11023
+ if (inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId)
11024
+ throw new Error("Stop daemon changed.");
11025
+ assertManagedStopContainersAbsent(
11026
+ baseline.endpoint,
11027
+ baseline.containers.map((container) => container.id)
11028
+ );
11029
+ const uidBytes = Buffer.byteLength(baseline.uid);
11030
+ const runnerId = uidBytes === 16 || uidBytes === 40 ? baseline.uid : baseline.providerId;
11031
+ if (inspectManagedStopContainers(baseline.project, baseline.endpoint).length !== 0 || inspectManagedStopWorkspaceIds(baseline.endpoint, baseline.composeDirectory).length !== 0 || inspectProviderRunnerContainers(baseline.endpoint, runnerId).length !== 0)
11032
+ throw new Error("Absent stop observed a remaining workspace population.");
11033
+ if (inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId || !(0, import_node_util2.isDeepStrictEqual)(absentRegistrationIdentity(state), before) || !(0, import_node_util2.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state))
11034
+ throw new Error("Absent stop authority changed during inspection.");
11035
+ return { status: "proven-absent", containers: [] };
11036
+ }
10907
11037
  function stopFromManagedBaseline(state) {
11038
+ const proof = proveManagedStop(state);
11039
+ if (proof.status === "proven-absent") return proof.status;
10908
11040
  const baseline = validateManagedStopBaseline(state.stopBaseline, state);
10909
11041
  const stopped = /* @__PURE__ */ new Set();
10910
11042
  for (const initial of proveRetainedManagedStop(state))
@@ -10927,6 +11059,12 @@ function stopFromManagedBaseline(state) {
10927
11059
  }
10928
11060
  if (inspect().some((container) => container.state.Running))
10929
11061
  throw new Error("Retained workloads remain running.");
11062
+ return "retained";
11063
+ }
11064
+ function managedStopRouteReferences(state) {
11065
+ return state.desired.apps.flatMap(
11066
+ (name) => ["http", "tcp"].map((protocol) => ({ repoPath: state.repoPath, name, protocol }))
11067
+ );
10930
11068
  }
10931
11069
  var import_node_os2, import_node_path31, import_node_util2;
10932
11070
  var init_managed_stop_recovery = __esm({
@@ -10936,6 +11074,7 @@ var init_managed_stop_recovery = __esm({
10936
11074
  import_node_path31 = __toESM(require("path"));
10937
11075
  import_node_util2 = require("util");
10938
11076
  init_devpod_environment();
11077
+ init_devpod_registry();
10939
11078
  init_devsy_workspaces();
10940
11079
  init_managed_compose_population();
10941
11080
  init_managed_runtime_state();
@@ -11045,8 +11184,7 @@ function stopRetainedManagedDevsyWorkspace(options) {
11045
11184
  throw new Error("Managed stop requires the exact retained workspace identity.");
11046
11185
  }
11047
11186
  if (state.stopBaseline) {
11048
- stopFromManagedBaseline(state);
11049
- return true;
11187
+ return stopFromManagedBaseline(state) === "proven-absent" ? "proven-absent" : true;
11050
11188
  }
11051
11189
  const workspaceEnv = workspace ? { token: workspace, gitCommonDir: resolveGitCommonDir(repoPath) } : void 0;
11052
11190
  function registration2() {
@@ -11230,12 +11368,14 @@ function inspectExactOwnership(devsyId, worktreePath) {
11230
11368
  }
11231
11369
  function mutateOwnedDevsyWorkspace(action2, devsyId, worktreePath) {
11232
11370
  return withMutationLock(`Devsy ${action2}`, worktreePath, () => {
11233
- if (action2 === "stop" && stopRetainedManagedDevsyWorkspace({
11234
- repoPath: worktreePath,
11235
- devsyId,
11236
- stopProvider: () => runDevsyAction("stop", devsyId)
11237
- }))
11238
- return { status: "changed" };
11371
+ if (action2 === "stop") {
11372
+ const stopped = stopRetainedManagedDevsyWorkspace({
11373
+ repoPath: worktreePath,
11374
+ devsyId,
11375
+ stopProvider: () => runDevsyAction("stop", devsyId)
11376
+ });
11377
+ if (stopped) return { status: stopped === "proven-absent" ? "proven-absent" : "changed" };
11378
+ }
11239
11379
  const before = inspectExactOwnership(devsyId, worktreePath);
11240
11380
  if (before.status === "absent") return { status: "absent" };
11241
11381
  runDevsyAction(action2, devsyId);
@@ -12018,7 +12158,7 @@ async function buildDoctorReport(options = {}) {
12018
12158
  const config = runtimeConfig.config;
12019
12159
  loadedConfig = config;
12020
12160
  loadedWorkspace = runtimeConfig.workspace;
12021
- const cliVersion = true ? "0.0.61" : "0.0.0-dev";
12161
+ const cliVersion = true ? "0.0.63" : "0.0.0-dev";
12022
12162
  const configVersion = config.devrouter?.version;
12023
12163
  if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
12024
12164
  addCheck(checks, {
@@ -12651,6 +12791,96 @@ var init_up = __esm({
12651
12791
  }
12652
12792
  });
12653
12793
 
12794
+ // src/core/devsy-exec-proof.ts
12795
+ function proveLocalDockerSelection(workspace) {
12796
+ const name = workspace.providerName;
12797
+ const context = workspace.context;
12798
+ if (!name || !context || !/^[a-z0-9][a-z0-9-]{0,31}$/.test(name) || !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(context))
12799
+ throw new Error("Retained exec provider configuration identity is unavailable.");
12800
+ const file = import_node_path37.default.join(
12801
+ process.env.DEVSY_HOME || import_node_path37.default.join(import_node_os4.default.homedir(), ".devsy"),
12802
+ "contexts",
12803
+ context,
12804
+ "providers",
12805
+ name,
12806
+ "provider.json"
12807
+ );
12808
+ try {
12809
+ if (import_node_fs30.default.statSync(file).size > 1024 * 1024) throw new Error();
12810
+ const config = JSON.parse(import_node_fs30.default.readFileSync(file, "utf8"));
12811
+ const driver = config.agent?.driver ?? "docker";
12812
+ const configuredPath = config.agent?.docker?.path ?? "";
12813
+ if (typeof configuredPath !== "string") throw new Error();
12814
+ const command = configuredPath === "${DOCKER_PATH}" || configuredPath === "$DOCKER_PATH" ? workspace.dockerPathOption || process.env.DOCKER_PATH || "docker" : configuredPath || "docker";
12815
+ if (config.name !== name || driver !== "docker" || command !== "docker") throw new Error();
12816
+ } catch {
12817
+ throw new Error("Retained exec requires a provider configured for the local Docker command.");
12818
+ }
12819
+ return name;
12820
+ }
12821
+ function captureDevsyExecProof(repoPath) {
12822
+ repoPath = comparableWorkspacePath(repoPath);
12823
+ const registry = listDevsyWorkspaces();
12824
+ const workspace = selectDevsyWorkspace(registry, repoPath);
12825
+ if (!workspace?.uid || !workspace.context || workspace.source.container || inspectDevsyWorkspaceOwnership(registry, workspace.id, repoPath).status !== "owned" || inspectDevsyRuntimeStatus(workspace.id) !== "running")
12826
+ throw new Error("Retained exec requires one exact running Devsy registration.");
12827
+ const providerName = proveLocalDockerSelection(workspace);
12828
+ const endpoint = resolveManagedStopEndpoint();
12829
+ if (!supportsManagedStopBaseline(endpoint))
12830
+ throw new Error("Retained exec requires an exact local Docker endpoint.");
12831
+ const daemon = inspectManagedStopDaemon(endpoint);
12832
+ const candidates = workspaceAppContainers(inspectWorkspaceContainers(), repoPath).filter(
12833
+ (container) => container.state.Running
12834
+ );
12835
+ if (candidates.length !== 1)
12836
+ throw new Error("Retained exec requires one exact running primary container.");
12837
+ const candidate = candidates[0];
12838
+ const project = candidate.labels["com.docker.compose.project"];
12839
+ if (!project) throw new Error("Retained exec primary has no Compose identity.");
12840
+ const pinned = workspaceAppContainers(
12841
+ inspectManagedStopContainers(project, endpoint),
12842
+ repoPath
12843
+ ).filter((container) => container.state.Running);
12844
+ if (pinned.length !== 1 || pinned[0].id !== candidate.id)
12845
+ throw new Error("Retained exec primary changed during inspection.");
12846
+ const mounts = pinned[0].mounts.filter(
12847
+ (mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath)
12848
+ );
12849
+ if (mounts.length !== 1 || !mounts[0].Destination.startsWith("/"))
12850
+ throw new Error("Retained exec requires one exact source mount.");
12851
+ const uidBytes = Buffer.byteLength(workspace.uid);
12852
+ const runner = uidBytes === 16 || uidBytes === 40 ? workspace.uid : workspace.id;
12853
+ const runnerContainers = inspectProviderRunnerContainers(endpoint, runner);
12854
+ if (runnerContainers.length !== 1 || runnerContainers[0] !== candidate.id)
12855
+ throw new Error("Retained exec runner does not select the exact primary container.");
12856
+ if (inspectManagedStopRunnerId(endpoint, candidate.id) !== runner || inspectManagedStopDaemon(endpoint) !== daemon)
12857
+ throw new Error("Retained exec provider or Docker identity changed.");
12858
+ return {
12859
+ repoPath,
12860
+ id: workspace.id,
12861
+ uid: workspace.uid,
12862
+ context: workspace.context,
12863
+ providerName,
12864
+ endpoint,
12865
+ daemon,
12866
+ containerId: candidate.id,
12867
+ workspacePath: mounts[0].Destination
12868
+ };
12869
+ }
12870
+ var import_node_fs30, import_node_os4, import_node_path37, import_node_util3;
12871
+ var init_devsy_exec_proof = __esm({
12872
+ "src/core/devsy-exec-proof.ts"() {
12873
+ "use strict";
12874
+ import_node_fs30 = __toESM(require("fs"));
12875
+ import_node_os4 = __toESM(require("os"));
12876
+ import_node_path37 = __toESM(require("path"));
12877
+ import_node_util3 = require("util");
12878
+ init_devpod_environment();
12879
+ init_devsy_workspaces();
12880
+ init_workspace();
12881
+ }
12882
+ });
12883
+
12654
12884
  // src/core/reliability-model.ts
12655
12885
  function record3(value) {
12656
12886
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -12680,7 +12910,7 @@ function assertReliabilityEvent(value) {
12680
12910
  valid = oneOf2(value.mode, ["start", "attach"]) && isReliabilityId(value.key) && isReliabilityId(value.operationId) && isReliabilityProfile(value.profile) && isConsumer(value.consumer);
12681
12911
  break;
12682
12912
  case "operation-request":
12683
- valid = oneOf2(value.kind, ["ensure", "exec"]) && isReliabilityId(value.key) && isReliabilityId(value.operationId) && isReliabilityProfile(value.profile) && isConsumer(value.consumer) && typeof value.runtimeRunning === "boolean";
12913
+ valid = oneOf2(value.kind, ["ensure", "exec"]) && isReliabilityId(value.key) && isReliabilityId(value.operationId) && isReliabilityProfile(value.profile) && isConsumer(value.consumer) && typeof value.runtimeRunning === "boolean" && (value.recoverInterruptedEnsure === void 0 || typeof value.recoverInterruptedEnsure === "boolean");
12684
12914
  break;
12685
12915
  case "drained":
12686
12916
  valid = isReliabilityId(value.operationId);
@@ -12863,6 +13093,10 @@ function handleRequest(state, event) {
12863
13093
  });
12864
13094
  return transition(state, "accepted");
12865
13095
  }
13096
+ function canExecAfterInterruptedEnsure(state) {
13097
+ const latestEnsure = [...state.operationHistory].reverse().find((operation) => operation.kind === "ensure");
13098
+ return state.executionPolicy === "manual" && state.desired === "running" && latestEnsure?.status === "INTERRUPTED" && latestEnsure.drained && state.operation?.drained === true && (state.operation.kind === "ensure" && state.phase === "recovering" || state.operation.kind === "exec" && ["COMPLETED", "NOT_LAUNCHED", "NOT_STARTED"].includes(state.operation.status));
13099
+ }
12866
13100
  function handleOperationRequest(state, event) {
12867
13101
  if (state.executionPolicy !== "manual") return unchanged(state, "blocked");
12868
13102
  const previous = state.operationHistory.find((entry) => entry.key === event.key);
@@ -12873,9 +13107,11 @@ function handleOperationRequest(state, event) {
12873
13107
  );
12874
13108
  if (state.operationHistory.some((entry) => entry.id === event.operationId))
12875
13109
  return unchanged(state, "conflict");
13110
+ if (event.kind === "exec" && canExecAfterInterruptedEnsure(state) && event.recoverInterruptedEnsure !== true)
13111
+ return unchanged(state, "blocked");
12876
13112
  const fullyStopped = state.stopProof.workloadsStopped && state.stopProof.routesRemoved;
12877
13113
  const reconcileEnsure = event.kind === "ensure" && state.operation?.kind === "ensure" && ["NOT_STARTED", "INTERRUPTED"].includes(state.operation.status);
12878
- if (state.operation && (!state.operation.drained || !["COMPLETED", "NOT_LAUNCHED", "NOT_STARTED"].includes(state.operation.status) && !fullyStopped && !reconcileEnsure))
13114
+ if (state.operation && (!state.operation.drained || !["COMPLETED", "NOT_LAUNCHED", "NOT_STARTED"].includes(state.operation.status) && !fullyStopped && !reconcileEnsure && !(event.kind === "exec" && event.recoverInterruptedEnsure === true && canExecAfterInterruptedEnsure(state))))
12879
13115
  return unchanged(state, "blocked");
12880
13116
  if (state.phase === "stopping" || state.desired === "parked-for-capacity")
12881
13117
  return unchanged(state, "blocked");
@@ -12886,8 +13122,9 @@ function handleOperationRequest(state, event) {
12886
13122
  if (rollover) {
12887
13123
  if (state.operation && (!state.operation.drained || !["COMPLETED", "NOT_LAUNCHED"].includes(state.operation.status)))
12888
13124
  return unchanged(state, "blocked");
13125
+ const latestEnsureId = [...state.operationHistory].reverse().find((entry) => entry.kind === "ensure")?.id;
12889
13126
  retired = state.operationHistory.findIndex(
12890
- (entry) => entry.id !== state.operation?.id && entry.drained && ["COMPLETED", "NOT_LAUNCHED"].includes(entry.status)
13127
+ (entry) => entry.id !== state.operation?.id && entry.id !== latestEnsureId && entry.drained && ["COMPLETED", "NOT_LAUNCHED"].includes(entry.status)
12891
13128
  );
12892
13129
  if (retired === -1) return unchanged(state, "blocked");
12893
13130
  }
@@ -13215,7 +13452,7 @@ function applyReliabilityEvent(state, event, nowMs) {
13215
13452
  case "not-started":
13216
13453
  if (next.executionPolicy !== "manual" || next.operation?.kind !== "exec")
13217
13454
  return unchanged(next, "blocked");
13218
- if (next.operation.id !== event.operationId || next.operation.status !== "RUNNING")
13455
+ if (next.operation.id !== event.operationId || !["RUNNING", "DISPATCH_RECORDED"].includes(next.operation.status))
13219
13456
  return unchanged(next, "stale");
13220
13457
  next.operation = { ...next.operation, status: "NOT_LAUNCHED", exitCode: null };
13221
13458
  return transition(next, "accepted");
@@ -13270,6 +13507,11 @@ function sameFence2(record4, fence) {
13270
13507
  ([key, value]) => value === fence[key]
13271
13508
  );
13272
13509
  }
13510
+ function hasDuplicateOperation(record4, requestId, operationId) {
13511
+ return record4.state.operationHistory.some(
13512
+ (operation) => operation.key === requestId || operation.id === operationId
13513
+ );
13514
+ }
13273
13515
  function workerGroupAbsent(pid) {
13274
13516
  try {
13275
13517
  process.kill(-pid, 0);
@@ -13286,11 +13528,11 @@ function signalOwnedGroup(child, signal) {
13286
13528
  if (error.code !== "ESRCH") throw error;
13287
13529
  }
13288
13530
  }
13289
- async function runLifecycleWorker(request) {
13531
+ async function runLifecycleWorker(request, beforeAdmission) {
13290
13532
  if (process.platform === "win32")
13291
13533
  throw new Error("Lifecycle workers require POSIX process-group ownership.");
13292
- const workerPath = import_node_path37.default.join(__dirname, "devrouter-lifecycle-worker.js");
13293
- if (!import_node_fs30.default.existsSync(workerPath))
13534
+ const workerPath = import_node_path38.default.join(__dirname, "devrouter-lifecycle-worker.js");
13535
+ if (!import_node_fs31.default.existsSync(workerPath))
13294
13536
  throw new Error("The packaged lifecycle worker is missing; rebuild the CLI.");
13295
13537
  const child = (0, import_node_child_process21.fork)(workerPath, [], {
13296
13538
  detached: true,
@@ -13298,13 +13540,18 @@ async function runLifecycleWorker(request) {
13298
13540
  execArgv: []
13299
13541
  });
13300
13542
  let ready = false;
13543
+ let closed = false;
13544
+ let registered = false;
13545
+ let dispatchAcknowledged = false;
13546
+ let sendAttempted = false;
13547
+ let readinessTimer;
13301
13548
  let result;
13302
13549
  let cancelled = false;
13303
13550
  let forceTimer;
13304
13551
  let monitor;
13305
13552
  let failure;
13306
13553
  const cancel = () => {
13307
- if (cancelled) return;
13554
+ if (cancelled || closed) return;
13308
13555
  cancelled = true;
13309
13556
  try {
13310
13557
  signalOwnedGroup(child, "SIGTERM");
@@ -13327,20 +13574,56 @@ async function runLifecycleWorker(request) {
13327
13574
  child.once("error", (error) => {
13328
13575
  failure = error;
13329
13576
  });
13330
- child.on("message", (message) => {
13577
+ readinessTimer = setTimeout(() => {
13578
+ failure = new Error("Lifecycle worker readiness timed out before dispatch.");
13579
+ cancel();
13580
+ }, 3e4);
13581
+ child.on("message", async (message) => {
13331
13582
  if (!message || typeof message !== "object") return;
13332
13583
  if ("ready" in message && message.ready === true && !ready) {
13333
13584
  ready = true;
13585
+ if (readinessTimer) clearTimeout(readinessTimer);
13334
13586
  try {
13587
+ await new Promise((resolve2) => setImmediate(resolve2));
13588
+ if (closed) return;
13335
13589
  if (cancelled || !child.pid)
13336
13590
  throw new Error("Lifecycle invocation was cancelled before dispatch.");
13337
13591
  if (request.kind !== "stop") {
13338
13592
  const birth = processBirthIdentity(child.pid);
13339
13593
  if (!birth) throw new Error("Could not prove lifecycle worker incarnation.");
13340
13594
  updateReliabilityOperation(request.identity, (record4) => {
13341
- if (!sameFence2(record4, request.fence) || record4.worker || record4.state.operation?.id !== request.operationId) {
13595
+ beforeAdmission?.();
13596
+ if (!request.admission)
13597
+ throw new Error("Lifecycle operation admission is required.");
13598
+ if (hasDuplicateOperation(record4, request.requestId, request.operationId))
13599
+ throw new Error(
13600
+ "Lifecycle operation request conflicts with an existing operation."
13601
+ );
13602
+ if (!sameFence2(record4, request.fence))
13342
13603
  throw new Error("Lifecycle intent changed before dispatch.");
13604
+ if (record4.worker || record4.revision !== request.admission.expectedRevision) {
13605
+ throw new LifecycleWorkerAdmissionBusyError();
13343
13606
  }
13607
+ const admitted = stepReliability(
13608
+ record4.state,
13609
+ {
13610
+ ...request.fence,
13611
+ type: "operation-request",
13612
+ kind: request.kind === "exec" ? "exec" : "ensure",
13613
+ key: request.requestId,
13614
+ operationId: request.operationId,
13615
+ profile: request.admission.profile,
13616
+ consumer: request.admission.consumer,
13617
+ runtimeRunning: request.admission.runtimeRunning,
13618
+ ...request.admission.recoverInterruptedEnsure ? { recoverInterruptedEnsure: true } : {}
13619
+ },
13620
+ Date.now()
13621
+ );
13622
+ if (admitted.outcome !== "accepted")
13623
+ throw new Error(`Lifecycle admission is ${admitted.outcome}.`);
13624
+ record4.state = admitted.state;
13625
+ record4.outcome = null;
13626
+ request.fence = reliabilityFence(record4.state);
13344
13627
  record4.worker = {
13345
13628
  id: request.workerId,
13346
13629
  operationId: request.operationId,
@@ -13356,6 +13639,7 @@ async function runLifecycleWorker(request) {
13356
13639
  throw new Error("Lifecycle dispatch is blocked.");
13357
13640
  record4.state = transition2.state;
13358
13641
  });
13642
+ registered = true;
13359
13643
  updateReliabilityOperation(request.identity, (record4) => {
13360
13644
  const transition2 = stepReliability(
13361
13645
  record4.state,
@@ -13366,12 +13650,18 @@ async function runLifecycleWorker(request) {
13366
13650
  },
13367
13651
  Date.now()
13368
13652
  );
13369
- record4.state = transition2.state;
13370
13653
  if (!transition2.effects.some((effect2) => effect2.kind === "launch")) {
13371
13654
  throw new Error("Lifecycle dispatch persistence was not acknowledged.");
13372
13655
  }
13656
+ record4.state = transition2.state;
13373
13657
  });
13658
+ dispatchAcknowledged = true;
13374
13659
  }
13660
+ await new Promise((resolve2) => setImmediate(resolve2));
13661
+ if (closed) return;
13662
+ beforeAdmission?.();
13663
+ if (cancelled) throw new Error("Lifecycle invocation was cancelled before dispatch.");
13664
+ sendAttempted = true;
13375
13665
  child.send({ request }, (error) => {
13376
13666
  if (error) {
13377
13667
  failure = error;
@@ -13396,8 +13686,9 @@ async function runLifecycleWorker(request) {
13396
13686
  }
13397
13687
  });
13398
13688
  child.once("close", () => {
13689
+ closed = true;
13399
13690
  try {
13400
- if (request.kind !== "stop") {
13691
+ if (request.kind !== "stop" && registered) {
13401
13692
  updateReliabilityOperation(request.identity, (record4) => {
13402
13693
  if (record4.worker?.id !== request.workerId || record4.worker.pid !== child.pid) return;
13403
13694
  if (!result) {
@@ -13405,7 +13696,7 @@ async function runLifecycleWorker(request) {
13405
13696
  record4.state,
13406
13697
  {
13407
13698
  ...reliabilityFence(record4.state),
13408
- type: "interrupted",
13699
+ type: request.kind === "exec" && dispatchAcknowledged && !sendAttempted ? "not-started" : "interrupted",
13409
13700
  operationId: request.operationId
13410
13701
  },
13411
13702
  Date.now()
@@ -13439,6 +13730,7 @@ async function runLifecycleWorker(request) {
13439
13730
  });
13440
13731
  });
13441
13732
  } finally {
13733
+ if (readinessTimer) clearTimeout(readinessTimer);
13442
13734
  if (monitor) clearInterval(monitor);
13443
13735
  if (forceTimer) clearTimeout(forceTimer);
13444
13736
  process.removeListener("SIGINT", onSignal);
@@ -13448,29 +13740,24 @@ async function runLifecycleWorker(request) {
13448
13740
  function newLifecycleIds() {
13449
13741
  return { requestId: (0, import_node_crypto16.randomUUID)(), operationId: (0, import_node_crypto16.randomUUID)(), workerId: (0, import_node_crypto16.randomUUID)() };
13450
13742
  }
13451
- var import_node_child_process21, import_node_crypto16, import_node_fs30, import_node_path37;
13743
+ var import_node_child_process21, import_node_crypto16, import_node_fs31, import_node_path38, LifecycleWorkerAdmissionBusyError;
13452
13744
  var init_reliability_worker = __esm({
13453
13745
  "src/core/reliability-worker.ts"() {
13454
13746
  "use strict";
13455
13747
  import_node_child_process21 = require("child_process");
13456
13748
  import_node_crypto16 = require("crypto");
13457
- import_node_fs30 = __toESM(require("fs"));
13458
- import_node_path37 = __toESM(require("path"));
13749
+ import_node_fs31 = __toESM(require("fs"));
13750
+ import_node_path38 = __toESM(require("path"));
13459
13751
  init_file_lock();
13460
13752
  init_reliability_contract();
13461
13753
  init_reliability_model();
13462
13754
  init_reliability_operation_store();
13463
- }
13464
- });
13465
-
13466
- // src/core/managed-host-preparation.ts
13467
- var import_node_child_process22, import_node_fs31;
13468
- var init_managed_host_preparation = __esm({
13469
- "src/core/managed-host-preparation.ts"() {
13470
- "use strict";
13471
- import_node_child_process22 = require("child_process");
13472
- import_node_fs31 = __toESM(require("fs"));
13473
- init_repo_config();
13755
+ LifecycleWorkerAdmissionBusyError = class extends Error {
13756
+ constructor() {
13757
+ super("Lifecycle admission lost to another operation.");
13758
+ this.name = "LifecycleWorkerAdmissionBusyError";
13759
+ }
13760
+ };
13474
13761
  }
13475
13762
  });
13476
13763
 
@@ -13479,7 +13766,7 @@ function sleep(ms) {
13479
13766
  return new Promise((resolve) => setTimeout(resolve, ms));
13480
13767
  }
13481
13768
  function inspectRouterApi(protocol) {
13482
- const result = (0, import_node_child_process23.spawnSync)(
13769
+ const result = (0, import_node_child_process22.spawnSync)(
13483
13770
  "curl",
13484
13771
  [
13485
13772
  "--silent",
@@ -13506,6 +13793,11 @@ function inspectRouterApi(protocol) {
13506
13793
  if (!Array.isArray(value)) {
13507
13794
  return { ok: false, details: `${protocol.toUpperCase()} router API returned a non-array` };
13508
13795
  }
13796
+ if (value.some((item) => !isRecord4(item) || typeof item.name !== "string" || !item.name))
13797
+ return {
13798
+ ok: false,
13799
+ details: `${protocol.toUpperCase()} router API returned malformed entries`
13800
+ };
13509
13801
  return {
13510
13802
  ok: true,
13511
13803
  reachedPageLimit: value.length >= ROUTER_API_PAGE_SIZE,
@@ -13518,6 +13810,9 @@ function inspectRouterApi(protocol) {
13518
13810
  )
13519
13811
  };
13520
13812
  }
13813
+ function isRecord4(value) {
13814
+ return value !== null && typeof value === "object" && !Array.isArray(value);
13815
+ }
13521
13816
  function inspectExpectedRoutes(routes, expectation) {
13522
13817
  const mismatched = [];
13523
13818
  const failures = [];
@@ -13609,19 +13904,19 @@ async function ensureTraefikRouteExpectation(routes, expectation, options) {
13609
13904
  async function ensureTraefikRoutesRemoved(routes, options = {}) {
13610
13905
  return ensureTraefikRouteExpectation(routes, "removed", options);
13611
13906
  }
13612
- var import_node_child_process23, import_node_fs32, import_node_path38, ROUTER_API_BASE, ROUTER_RELOAD_LOCK_FILE, ROUTER_API_REQUEST_TIMEOUT_SECONDS, DEFAULT_INITIAL_TIMEOUT_MS, DEFAULT_RECOVERY_TIMEOUT_MS, DEFAULT_POLL_INTERVAL_MS, ROUTER_API_PAGE_SIZE;
13907
+ var import_node_child_process22, import_node_fs32, import_node_path39, ROUTER_API_BASE, ROUTER_RELOAD_LOCK_FILE, ROUTER_API_REQUEST_TIMEOUT_SECONDS, DEFAULT_INITIAL_TIMEOUT_MS, DEFAULT_RECOVERY_TIMEOUT_MS, DEFAULT_POLL_INTERVAL_MS, ROUTER_API_PAGE_SIZE;
13613
13908
  var init_traefik_route_health = __esm({
13614
13909
  "src/core/traefik-route-health.ts"() {
13615
13910
  "use strict";
13616
- import_node_child_process23 = require("child_process");
13911
+ import_node_child_process22 = require("child_process");
13617
13912
  import_node_fs32 = __toESM(require("fs"));
13618
- import_node_path38 = __toESM(require("path"));
13913
+ import_node_path39 = __toESM(require("path"));
13619
13914
  init_file_lock();
13620
13915
  init_host_routes();
13621
13916
  init_reliability_context();
13622
13917
  init_router();
13623
13918
  ROUTER_API_BASE = "http://127.0.0.1:8080/api";
13624
- ROUTER_RELOAD_LOCK_FILE = import_node_path38.default.join(DEVROUTER_HOME, "router-reload.lock");
13919
+ ROUTER_RELOAD_LOCK_FILE = import_node_path39.default.join(DEVROUTER_HOME, "router-reload.lock");
13625
13920
  ROUTER_API_REQUEST_TIMEOUT_SECONDS = "2";
13626
13921
  DEFAULT_INITIAL_TIMEOUT_MS = 3e3;
13627
13922
  DEFAULT_RECOVERY_TIMEOUT_MS = 1e4;
@@ -13630,6 +13925,17 @@ var init_traefik_route_health = __esm({
13630
13925
  }
13631
13926
  });
13632
13927
 
13928
+ // src/core/managed-host-preparation.ts
13929
+ var import_node_child_process23, import_node_fs33;
13930
+ var init_managed_host_preparation = __esm({
13931
+ "src/core/managed-host-preparation.ts"() {
13932
+ "use strict";
13933
+ import_node_child_process23 = require("child_process");
13934
+ import_node_fs33 = __toESM(require("fs"));
13935
+ init_repo_config();
13936
+ }
13937
+ });
13938
+
13633
13939
  // src/core/workspace-ensure.ts
13634
13940
  function resolveLinkedTarget(repoPath) {
13635
13941
  const snapshots = getWorkspaceRegistrySnapshots();
@@ -13660,13 +13966,13 @@ function resolveLinkedTarget(repoPath) {
13660
13966
  gitCommonDir: resolveGitCommonDir(repoPath)
13661
13967
  };
13662
13968
  }
13663
- var import_node_child_process24, import_node_fs33, import_node_path39;
13969
+ var import_node_child_process24, import_node_fs34, import_node_path40;
13664
13970
  var init_workspace_ensure = __esm({
13665
13971
  "src/core/workspace-ensure.ts"() {
13666
13972
  "use strict";
13667
13973
  import_node_child_process24 = require("child_process");
13668
- import_node_fs33 = __toESM(require("fs"));
13669
- import_node_path39 = __toESM(require("path"));
13974
+ import_node_fs34 = __toESM(require("fs"));
13975
+ import_node_path40 = __toESM(require("path"));
13670
13976
  init_devcontainer_profile();
13671
13977
  init_devpod_environment();
13672
13978
  init_devpod_mutation();
@@ -13692,6 +13998,11 @@ var init_workspace_ensure = __esm({
13692
13998
  });
13693
13999
 
13694
14000
  // src/core/reliability-lifecycle.ts
14001
+ function matchesFence(record4, fence) {
14002
+ return Object.entries(reliabilityFence(record4.state)).every(
14003
+ ([key, value]) => value === fence[key]
14004
+ );
14005
+ }
13695
14006
  async function withLifecycleOperationLock(repoPath, operation) {
13696
14007
  if (activeWorker && lockHeld) {
13697
14008
  if (!sameWorkspacePath(repoPath, activeWorker.repoPath))
@@ -13739,6 +14050,23 @@ function reconcileDrained(record4) {
13739
14050
  record4.worker = null;
13740
14051
  }
13741
14052
  }
14053
+ function reportBusyExecWait(operationId, waitedMs) {
14054
+ process.stderr.write(
14055
+ `Lifecycle exec ${operationId} is waiting for the existing worker (${Math.ceil(waitedMs / 1e3)}s).
14056
+ `
14057
+ );
14058
+ }
14059
+ function installLifecycleWaitSignals(cancellation) {
14060
+ const onSignal = () => {
14061
+ cancellation.requested = true;
14062
+ };
14063
+ process.once("SIGINT", onSignal);
14064
+ process.once("SIGTERM", onSignal);
14065
+ return () => {
14066
+ process.removeListener("SIGINT", onSignal);
14067
+ process.removeListener("SIGTERM", onSignal);
14068
+ };
14069
+ }
13742
14070
  async function superviseLifecycle(kind, repoPath, options = {}, command) {
13743
14071
  repoPath = comparableWorkspacePath(repoPath);
13744
14072
  if (isLinkedWorktree(repoPath) && !readPersistedWorkspace(repoPath)) {
@@ -13751,47 +14079,119 @@ async function superviseLifecycle(kind, repoPath, options = {}, command) {
13751
14079
  provider: resolveWorkspaceRuntimeOrDefault(repoPath)
13752
14080
  };
13753
14081
  const ids = newLifecycleIds();
13754
- let fence;
13755
- const runtimeRunning = kind === "exec" ? Boolean(resolveRunningWorkspaceContainer(repoPath)) : false;
13756
- updateReliabilityOperation(identity2, (record4) => {
13757
- reconcileDrained(record4);
13758
- if (kind === "stop") {
14082
+ const copiedOptions = { ...options };
14083
+ const copiedCommand = command ? [...command] : void 0;
14084
+ if (kind === "stop") {
14085
+ const fence = updateReliabilityOperation(identity2, (record4) => {
14086
+ reconcileDrained(record4);
13759
14087
  stepRecord(record4, { ...reliabilityFence(record4.state), type: "stop" });
13760
- } else {
13761
- if (record4.worker)
14088
+ return reliabilityFence(record4.state);
14089
+ });
14090
+ return runLifecycleWorker({ kind, repoPath, identity: identity2, ...ids, fence, options: copiedOptions });
14091
+ }
14092
+ const initial = readReliabilityOperation(identity2) ?? updateReliabilityOperation(identity2, (record4) => record4);
14093
+ const expectedFence = reliabilityFence(initial.state);
14094
+ const deadline = Date.now() + BUSY_EXEC_WAIT_MS;
14095
+ const cancellation = { requested: false };
14096
+ const removeSignals = installLifecycleWaitSignals(cancellation);
14097
+ let lastProgress = -Infinity;
14098
+ const assertWaiting = () => {
14099
+ if (cancellation.requested)
14100
+ throw new Error(`Lifecycle exec ${ids.operationId} cancelled before dispatch.`);
14101
+ if (Date.now() >= deadline)
14102
+ throw new Error(
14103
+ `Lifecycle exec ${ids.operationId} admission timed out; command was not launched.`
14104
+ );
14105
+ };
14106
+ try {
14107
+ for (; ; ) {
14108
+ assertWaiting();
14109
+ let previous = readReliabilityOperation(identity2);
14110
+ if (!previous || !matchesFence(previous, expectedFence))
13762
14111
  throw new Error(
13763
- "An earlier lifecycle worker may still be active; use explicit stop to reconcile it."
14112
+ "Lifecycle intent changed while awaiting admission; command was not launched."
13764
14113
  );
13765
- record4.outcome = null;
13766
- stepRecord(record4, {
13767
- ...reliabilityFence(record4.state),
13768
- type: "operation-request",
13769
- kind,
13770
- key: ids.requestId,
13771
- operationId: ids.operationId,
13772
- profile: options.profile ?? record4.state.profile ?? "full",
13773
- consumer: { id: "manual-cli", requiredCapabilities: [], pinned: false },
13774
- runtimeRunning
13775
- });
14114
+ if (hasDuplicateOperation(previous, ids.requestId, ids.operationId))
14115
+ throw new Error("Lifecycle request identity already exists; command was not replayed.");
14116
+ if (previous.worker) {
14117
+ const worker = previous.worker;
14118
+ const birth = processBirthIdentity(worker.pid);
14119
+ if (birth !== worker.birth && workerGroupAbsent(worker.pid)) {
14120
+ previous = updateReliabilityOperation(identity2, (record4) => {
14121
+ reconcileDrained(record4);
14122
+ return record4;
14123
+ });
14124
+ } else {
14125
+ if (!birth || birth !== worker.birth || previous.state.operation?.status === "COMPLETION_UNKNOWN")
14126
+ throw new Error(
14127
+ "Existing lifecycle worker identity or completion is uncertain; preserve its evidence."
14128
+ );
14129
+ if (kind !== "exec")
14130
+ throw new Error("A lifecycle worker is active; wait for its completion before ensure.");
14131
+ const now = Date.now();
14132
+ if (now - lastProgress >= BUSY_EXEC_PROGRESS_MS) {
14133
+ reportBusyExecWait(ids.operationId, now - (deadline - BUSY_EXEC_WAIT_MS));
14134
+ lastProgress = now;
14135
+ }
14136
+ await new Promise(
14137
+ (resolve) => setTimeout(resolve, Math.min(BUSY_EXEC_POLL_MS, deadline - now))
14138
+ );
14139
+ continue;
14140
+ }
14141
+ }
14142
+ if (!previous.worker && previous.state.operation?.status === "NOT_STARTED" && !previous.state.operation.drained)
14143
+ previous = updateReliabilityOperation(identity2, (record4) => {
14144
+ reconcileDrained(record4);
14145
+ return record4;
14146
+ });
14147
+ assertWaiting();
14148
+ const runtimeRunning = kind === "exec" && Boolean(resolveRunningWorkspaceContainer(repoPath));
14149
+ let retainedExecProof;
14150
+ if (kind === "exec" && identity2.provider === "devsy" && canExecAfterInterruptedEnsure(previous.state))
14151
+ retainedExecProof = withMutationLock(
14152
+ "Prove retained exec",
14153
+ repoPath,
14154
+ () => captureDevsyExecProof(repoPath)
14155
+ );
14156
+ assertWaiting();
14157
+ try {
14158
+ return await runLifecycleWorker(
14159
+ {
14160
+ kind,
14161
+ repoPath,
14162
+ identity: identity2,
14163
+ ...ids,
14164
+ fence: { ...expectedFence },
14165
+ options: copiedOptions,
14166
+ ...copiedCommand ? { command: copiedCommand } : {},
14167
+ ...retainedExecProof ? { retainedExecProof } : {},
14168
+ admission: {
14169
+ expectedRevision: previous.revision,
14170
+ profile: copiedOptions.profile ?? previous.state.profile ?? "full",
14171
+ consumer: { id: "manual-cli", requiredCapabilities: [], pinned: false },
14172
+ runtimeRunning,
14173
+ ...retainedExecProof ? { recoverInterruptedEnsure: true } : {}
14174
+ }
14175
+ },
14176
+ assertWaiting
14177
+ );
14178
+ } catch (error) {
14179
+ if (!(error instanceof LifecycleWorkerAdmissionBusyError) || kind !== "exec") throw error;
14180
+ assertWaiting();
14181
+ await new Promise((resolve) => setTimeout(resolve, BUSY_EXEC_POLL_MS));
14182
+ }
13776
14183
  }
13777
- fence = reliabilityFence(record4.state);
13778
- });
13779
- return runLifecycleWorker({
13780
- kind,
13781
- repoPath,
13782
- identity: identity2,
13783
- ...ids,
13784
- fence,
13785
- options,
13786
- ...command ? { command } : {}
13787
- });
14184
+ } finally {
14185
+ removeSignals();
14186
+ }
13788
14187
  }
13789
- var import_node_path40, activeWorker, lockHeld;
14188
+ var import_node_path41, activeWorker, lockHeld, BUSY_EXEC_WAIT_MS, BUSY_EXEC_POLL_MS, BUSY_EXEC_PROGRESS_MS;
13790
14189
  var init_reliability_lifecycle = __esm({
13791
14190
  "src/core/reliability-lifecycle.ts"() {
13792
14191
  "use strict";
13793
- import_node_path40 = __toESM(require("path"));
14192
+ import_node_path41 = __toESM(require("path"));
13794
14193
  init_devpod_environment();
14194
+ init_devsy_exec_proof();
13795
14195
  init_devsy_mutation();
13796
14196
  init_file_lock();
13797
14197
  init_host_routes();
@@ -13802,11 +14202,15 @@ var init_reliability_lifecycle = __esm({
13802
14202
  init_reliability_model();
13803
14203
  init_reliability_operation_store();
13804
14204
  init_reliability_worker();
14205
+ init_traefik_route_health();
13805
14206
  init_workspace();
13806
14207
  init_workspace_ensure();
13807
14208
  init_workspace_runtime();
13808
14209
  init_reliability_context();
13809
14210
  lockHeld = false;
14211
+ BUSY_EXEC_WAIT_MS = 30 * 60 * 1e3;
14212
+ BUSY_EXEC_POLL_MS = 250;
14213
+ BUSY_EXEC_PROGRESS_MS = 1e4;
13810
14214
  }
13811
14215
  });
13812
14216
 
@@ -14200,17 +14604,17 @@ function asRecord3(value) {
14200
14604
  return value;
14201
14605
  }
14202
14606
  function readJson(filePath) {
14203
- if (!import_node_fs34.default.existsSync(filePath)) {
14607
+ if (!import_node_fs35.default.existsSync(filePath)) {
14204
14608
  return void 0;
14205
14609
  }
14206
14610
  try {
14207
- return JSON.parse(import_node_fs34.default.readFileSync(filePath, "utf-8"));
14611
+ return JSON.parse(import_node_fs35.default.readFileSync(filePath, "utf-8"));
14208
14612
  } catch {
14209
14613
  return void 0;
14210
14614
  }
14211
14615
  }
14212
14616
  function relative(repoPath, filePath) {
14213
- return import_node_path41.default.relative(repoPath, filePath) || ".";
14617
+ return import_node_path42.default.relative(repoPath, filePath) || ".";
14214
14618
  }
14215
14619
  function redactEnvAssignments(value) {
14216
14620
  return value.replace(
@@ -14251,7 +14655,7 @@ function inspectPackageManager(repoPath, pkg) {
14251
14655
  ["bun.lock", "bun"]
14252
14656
  ];
14253
14657
  for (const [fileName, name] of lockfiles) {
14254
- if (import_node_fs34.default.existsSync(import_node_path41.default.join(repoPath, fileName))) {
14658
+ if (import_node_fs35.default.existsSync(import_node_path42.default.join(repoPath, fileName))) {
14255
14659
  return { name, source: fileName };
14256
14660
  }
14257
14661
  }
@@ -14266,9 +14670,9 @@ function inspectNode(repoPath, pkg) {
14266
14670
  if (typeof engines?.node === "string") {
14267
14671
  return { version: engines.node, source: "package.json:engines.node" };
14268
14672
  }
14269
- const nvmrc = import_node_path41.default.join(repoPath, ".nvmrc");
14270
- if (import_node_fs34.default.existsSync(nvmrc)) {
14271
- const version = import_node_fs34.default.readFileSync(nvmrc, "utf-8").trim();
14673
+ const nvmrc = import_node_path42.default.join(repoPath, ".nvmrc");
14674
+ if (import_node_fs35.default.existsSync(nvmrc)) {
14675
+ const version = import_node_fs35.default.readFileSync(nvmrc, "utf-8").trim();
14272
14676
  return { version, source: ".nvmrc" };
14273
14677
  }
14274
14678
  return void 0;
@@ -14329,7 +14733,7 @@ function configuredComposeFiles(repoPath) {
14329
14733
  const files = config.apps.filter(
14330
14734
  (app) => app.runtime === "docker"
14331
14735
  ).flatMap((app) => app.docker.composeFiles).filter(
14332
- (fileName) => !import_node_path41.default.isAbsolute(fileName) && !import_node_path41.default.normalize(fileName).startsWith("..")
14736
+ (fileName) => !import_node_path42.default.isAbsolute(fileName) && !import_node_path42.default.normalize(fileName).startsWith("..")
14333
14737
  );
14334
14738
  return Array.from(new Set(files));
14335
14739
  } catch {
@@ -14347,7 +14751,7 @@ function composeFiles(repoPath) {
14347
14751
  ...configuredComposeFiles(repoPath)
14348
14752
  ];
14349
14753
  return Array.from(new Set(candidates)).filter(
14350
- (fileName) => import_node_fs34.default.existsSync(import_node_path41.default.join(repoPath, fileName))
14754
+ (fileName) => import_node_fs35.default.existsSync(import_node_path42.default.join(repoPath, fileName))
14351
14755
  );
14352
14756
  }
14353
14757
  function stringArray2(value) {
@@ -14385,7 +14789,7 @@ function inspectServices(repoPath) {
14385
14789
  const services = [];
14386
14790
  for (const fileName of composeFiles(repoPath)) {
14387
14791
  try {
14388
- const parsed = import_yaml6.default.parse(import_node_fs34.default.readFileSync(import_node_path41.default.join(repoPath, fileName), "utf-8"));
14792
+ const parsed = import_yaml6.default.parse(import_node_fs35.default.readFileSync(import_node_path42.default.join(repoPath, fileName), "utf-8"));
14389
14793
  const serviceMap = asRecord3(asRecord3(parsed)?.services);
14390
14794
  for (const [name, value] of Object.entries(serviceMap ?? {})) {
14391
14795
  const service = asRecord3(value);
@@ -14420,8 +14824,8 @@ function inspectServices(repoPath) {
14420
14824
  return services;
14421
14825
  }
14422
14826
  function inspectEnvFiles(repoPath) {
14423
- const files = import_node_fs34.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
14424
- const content = import_node_fs34.default.readFileSync(import_node_path41.default.join(repoPath, fileName), "utf-8");
14827
+ const files = import_node_fs35.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
14828
+ const content = import_node_fs35.default.readFileSync(import_node_path42.default.join(repoPath, fileName), "utf-8");
14425
14829
  const names = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#") && line.includes("=")).map((line) => line.split("=")[0]?.trim()).filter((name) => Boolean(name)).sort();
14426
14830
  return { path: fileName, names };
14427
14831
  });
@@ -14435,18 +14839,18 @@ function inspectEnvFiles(repoPath) {
14435
14839
  };
14436
14840
  }
14437
14841
  function inspectDevcontainer(repoPath) {
14438
- const dir = import_node_path41.default.join(repoPath, ".devcontainer");
14439
- if (!import_node_fs34.default.existsSync(dir)) {
14842
+ const dir = import_node_path42.default.join(repoPath, ".devcontainer");
14843
+ if (!import_node_fs35.default.existsSync(dir)) {
14440
14844
  return { exists: false, files: [] };
14441
14845
  }
14442
14846
  return {
14443
14847
  exists: true,
14444
- files: import_node_fs34.default.readdirSync(dir).filter((fileName) => import_node_fs34.default.statSync(import_node_path41.default.join(dir, fileName)).isFile()).sort().map((fileName) => `.devcontainer/${fileName}`)
14848
+ files: import_node_fs35.default.readdirSync(dir).filter((fileName) => import_node_fs35.default.statSync(import_node_path42.default.join(dir, fileName)).isFile()).sort().map((fileName) => `.devcontainer/${fileName}`)
14445
14849
  };
14446
14850
  }
14447
14851
  function inspectDevrouter(repoPath) {
14448
14852
  const configPath = getRepoConfigPath(repoPath);
14449
- if (!import_node_fs34.default.existsSync(configPath)) {
14853
+ if (!import_node_fs35.default.existsSync(configPath)) {
14450
14854
  return {
14451
14855
  exists: false,
14452
14856
  configPath,
@@ -14490,15 +14894,15 @@ function inspectAgentGuidance(repoPath) {
14490
14894
  ["AGENTS.md", "agents"],
14491
14895
  ["CLAUDE.md", "claude"]
14492
14896
  ]) {
14493
- if (import_node_fs34.default.existsSync(import_node_path41.default.join(repoPath, fileName))) {
14897
+ if (import_node_fs35.default.existsSync(import_node_path42.default.join(repoPath, fileName))) {
14494
14898
  results.push({ path: fileName, kind });
14495
14899
  }
14496
14900
  }
14497
- const skillsDir = import_node_path41.default.join(repoPath, ".agents", "skills");
14498
- if (import_node_fs34.default.existsSync(skillsDir)) {
14499
- for (const name of import_node_fs34.default.readdirSync(skillsDir).sort()) {
14500
- const skillPath = import_node_path41.default.join(skillsDir, name, "SKILL.md");
14501
- if (import_node_fs34.default.existsSync(skillPath)) {
14901
+ const skillsDir = import_node_path42.default.join(repoPath, ".agents", "skills");
14902
+ if (import_node_fs35.default.existsSync(skillsDir)) {
14903
+ for (const name of import_node_fs35.default.readdirSync(skillsDir).sort()) {
14904
+ const skillPath = import_node_path42.default.join(skillsDir, name, "SKILL.md");
14905
+ if (import_node_fs35.default.existsSync(skillPath)) {
14502
14906
  results.push({ path: relative(repoPath, skillPath), kind: "skill" });
14503
14907
  }
14504
14908
  }
@@ -14541,7 +14945,7 @@ function buildIssues(report) {
14541
14945
  }
14542
14946
  function inspectRepo(options = {}) {
14543
14947
  const repoPath = resolveRepoPath(options.repo);
14544
- const pkg = readJson(import_node_path41.default.join(repoPath, "package.json"));
14948
+ const pkg = readJson(import_node_path42.default.join(repoPath, "package.json"));
14545
14949
  const scripts = inspectScripts(pkg);
14546
14950
  const reportWithoutIssues = {
14547
14951
  repoPath,
@@ -14560,12 +14964,12 @@ function inspectRepo(options = {}) {
14560
14964
  issues: buildIssues(reportWithoutIssues)
14561
14965
  };
14562
14966
  }
14563
- var import_node_fs34, import_node_path41, import_yaml6;
14967
+ var import_node_fs35, import_node_path42, import_yaml6;
14564
14968
  var init_repo_inspect = __esm({
14565
14969
  "src/core/repo-inspect.ts"() {
14566
14970
  "use strict";
14567
- import_node_fs34 = __toESM(require("fs"));
14568
- import_node_path41 = __toESM(require("path"));
14971
+ import_node_fs35 = __toESM(require("fs"));
14972
+ import_node_path42 = __toESM(require("path"));
14569
14973
  import_yaml6 = __toESM(require("yaml"));
14570
14974
  init_repo_config();
14571
14975
  }
@@ -14670,7 +15074,7 @@ function requiredFileChecks(repoPath) {
14670
15074
  ".devcontainer/docker-compose.yml",
14671
15075
  ".devrouter.yml"
14672
15076
  ];
14673
- const missing = required.filter((fileName) => !import_node_fs35.default.existsSync(import_node_path42.default.join(repoPath, fileName)));
15077
+ const missing = required.filter((fileName) => !import_node_fs36.default.existsSync(import_node_path43.default.join(repoPath, fileName)));
14674
15078
  return {
14675
15079
  id: "repo.devcontainer.verify-files",
14676
15080
  level: missing.length === 0 ? "ok" : "error",
@@ -14895,12 +15299,12 @@ async function verifyDevcontainer(options = {}) {
14895
15299
  nextSteps: collectNextSteps3(checks)
14896
15300
  };
14897
15301
  }
14898
- var import_node_fs35, import_node_path42;
15302
+ var import_node_fs36, import_node_path43;
14899
15303
  var init_devcontainer_verify = __esm({
14900
15304
  "src/core/devcontainer-verify.ts"() {
14901
15305
  "use strict";
14902
- import_node_fs35 = __toESM(require("fs"));
14903
- import_node_path42 = __toESM(require("path"));
15306
+ import_node_fs36 = __toESM(require("fs"));
15307
+ import_node_path43 = __toESM(require("path"));
14904
15308
  init_capabilities();
14905
15309
  init_doctor();
14906
15310
  init_host_routes();
@@ -15197,7 +15601,7 @@ function packageManagerIssues(repo) {
15197
15601
  }
15198
15602
  function plannedFiles(repoPath, version) {
15199
15603
  const repo = inspectRepo({ repo: repoPath });
15200
- const projectName = sanitizeProjectName(import_node_path43.default.basename(repo.repoPath));
15604
+ const projectName = sanitizeProjectName(import_node_path44.default.basename(repo.repoPath));
15201
15605
  const nodeMajor = majorVersion(repo.node?.version, "24");
15202
15606
  const pnpmVersion = repo.packageManager?.name === "pnpm" && repo.packageManager.version ? repo.packageManager.version : DEFAULT_PNPM_VERSION;
15203
15607
  const port = inferPort2(repo);
@@ -15242,8 +15646,8 @@ function plannedFiles(repoPath, version) {
15242
15646
  };
15243
15647
  }
15244
15648
  function classifyFile(repoPath, file) {
15245
- const absolutePath = import_node_path43.default.join(repoPath, file.relativePath);
15246
- if (!import_node_fs36.default.existsSync(absolutePath)) {
15649
+ const absolutePath = import_node_path44.default.join(repoPath, file.relativePath);
15650
+ if (!import_node_fs37.default.existsSync(absolutePath)) {
15247
15651
  return {
15248
15652
  path: file.relativePath,
15249
15653
  action: "create",
@@ -15251,7 +15655,7 @@ function classifyFile(repoPath, file) {
15251
15655
  bytes: Buffer.byteLength(file.content)
15252
15656
  };
15253
15657
  }
15254
- const current = import_node_fs36.default.readFileSync(absolutePath, "utf-8");
15658
+ const current = import_node_fs37.default.readFileSync(absolutePath, "utf-8");
15255
15659
  if (!current.includes(MANAGED_MARKER2)) {
15256
15660
  return {
15257
15661
  path: file.relativePath,
@@ -15308,11 +15712,11 @@ function buildPlan(repoPath, dryRun, version) {
15308
15712
  };
15309
15713
  }
15310
15714
  function writeFile(repoPath, file) {
15311
- const absolutePath = import_node_path43.default.join(repoPath, file.relativePath);
15312
- import_node_fs36.default.mkdirSync(import_node_path43.default.dirname(absolutePath), { recursive: true });
15313
- import_node_fs36.default.writeFileSync(absolutePath, file.content, "utf-8");
15715
+ const absolutePath = import_node_path44.default.join(repoPath, file.relativePath);
15716
+ import_node_fs37.default.mkdirSync(import_node_path44.default.dirname(absolutePath), { recursive: true });
15717
+ import_node_fs37.default.writeFileSync(absolutePath, file.content, "utf-8");
15314
15718
  if (file.executable) {
15315
- import_node_fs36.default.chmodSync(absolutePath, 493);
15719
+ import_node_fs37.default.chmodSync(absolutePath, 493);
15316
15720
  }
15317
15721
  }
15318
15722
  function writeDevcontainer(options = {}) {
@@ -15350,12 +15754,12 @@ function writeDevcontainer(options = {}) {
15350
15754
  nextSteps: postWriteNextSteps(repoPath)
15351
15755
  };
15352
15756
  }
15353
- var import_node_fs36, import_node_path43, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
15757
+ var import_node_fs37, import_node_path44, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
15354
15758
  var init_devcontainer_write = __esm({
15355
15759
  "src/core/devcontainer-write.ts"() {
15356
15760
  "use strict";
15357
- import_node_fs36 = __toESM(require("fs"));
15358
- import_node_path43 = __toESM(require("path"));
15761
+ import_node_fs37 = __toESM(require("fs"));
15762
+ import_node_path44 = __toESM(require("path"));
15359
15763
  init_repo_config();
15360
15764
  init_repo_inspect();
15361
15765
  MANAGED_MARKER2 = "devrouter:managed devcontainer";
@@ -15468,11 +15872,11 @@ var init_repo_devcontainer = __esm({
15468
15872
  function fail3(message) {
15469
15873
  throw new Error(message);
15470
15874
  }
15471
- function isRecord4(value) {
15875
+ function isRecord5(value) {
15472
15876
  return typeof value === "object" && value !== null && !Array.isArray(value);
15473
15877
  }
15474
15878
  function requireRecord(value, label) {
15475
- if (!isRecord4(value)) fail3(`${label} must be a mapping.`);
15879
+ if (!isRecord5(value)) fail3(`${label} must be a mapping.`);
15476
15880
  return value;
15477
15881
  }
15478
15882
  function requireExactKeys(value, allowed, label) {
@@ -15689,13 +16093,13 @@ function buildProfilePlanReport(options) {
15689
16093
  return { ...report, contractPath, bindings };
15690
16094
  }
15691
16095
  function loadContract(repoPath, contractPath) {
15692
- if (import_node_path44.default.isAbsolute(contractPath)) {
16096
+ if (import_node_path45.default.isAbsolute(contractPath)) {
15693
16097
  fail3("profile plan contract path must be relative to the repository.");
15694
16098
  }
15695
16099
  const absolutePath = assertPathWithinRepo(contractPath, repoPath, "Profile plan contract");
15696
16100
  let metadata;
15697
16101
  try {
15698
- metadata = import_node_fs37.default.lstatSync(absolutePath);
16102
+ metadata = import_node_fs38.default.lstatSync(absolutePath);
15699
16103
  } catch (error) {
15700
16104
  fail3(`could not inspect profile plan contract '${contractPath}': ${error.message}`);
15701
16105
  }
@@ -15705,16 +16109,16 @@ function loadContract(repoPath, contractPath) {
15705
16109
  if (metadata.size > MAX_CONTRACT_BYTES) {
15706
16110
  fail3(`profile plan contract exceeds the ${MAX_CONTRACT_BYTES}-byte limit.`);
15707
16111
  }
15708
- const realRepo = import_node_fs37.default.realpathSync(repoPath);
15709
- const realContract = import_node_fs37.default.realpathSync(absolutePath);
16112
+ const realRepo = import_node_fs38.default.realpathSync(repoPath);
16113
+ const realContract = import_node_fs38.default.realpathSync(absolutePath);
15710
16114
  assertPathWithinRepo(realContract, realRepo, "Profile plan contract");
15711
- const source = import_node_fs37.default.readFileSync(absolutePath, "utf-8");
16115
+ const source = import_node_fs38.default.readFileSync(absolutePath, "utf-8");
15712
16116
  if (Buffer.byteLength(source, "utf-8") > MAX_CONTRACT_BYTES) {
15713
16117
  fail3(`profile plan contract exceeds the ${MAX_CONTRACT_BYTES}-byte limit.`);
15714
16118
  }
15715
16119
  return {
15716
16120
  contract: parseProfilePlanContract(source),
15717
- contractPath: import_node_path44.default.relative(repoPath, absolutePath).split(import_node_path44.default.sep).join("/")
16121
+ contractPath: import_node_path45.default.relative(repoPath, absolutePath).split(import_node_path45.default.sep).join("/")
15718
16122
  };
15719
16123
  }
15720
16124
  function resolveProfilePlan(options) {
@@ -15728,12 +16132,12 @@ function resolveProfilePlan(options) {
15728
16132
  contractPath: loaded.contractPath
15729
16133
  });
15730
16134
  }
15731
- var import_node_fs37, import_node_path44, import_yaml7, CONTRACT_VERSION, MAX_CONTRACT_BYTES, MAX_APP_MAPPINGS, MAX_BINDING_KEYS, MAX_BINDING_VALUES, MAX_LITERAL_LENGTH, BINDING_KEY;
16135
+ var import_node_fs38, import_node_path45, import_yaml7, CONTRACT_VERSION, MAX_CONTRACT_BYTES, MAX_APP_MAPPINGS, MAX_BINDING_KEYS, MAX_BINDING_VALUES, MAX_LITERAL_LENGTH, BINDING_KEY;
15732
16136
  var init_profile_plan = __esm({
15733
16137
  "src/core/profile-plan.ts"() {
15734
16138
  "use strict";
15735
- import_node_fs37 = __toESM(require("fs"));
15736
- import_node_path44 = __toESM(require("path"));
16139
+ import_node_fs38 = __toESM(require("fs"));
16140
+ import_node_path45 = __toESM(require("path"));
15737
16141
  import_yaml7 = require("yaml");
15738
16142
  init_paths();
15739
16143
  init_profile_resolution();
@@ -15765,7 +16169,7 @@ async function runProfileResolveCommand(options) {
15765
16169
  async function runProfilePlanCommand(options) {
15766
16170
  const report = resolveProfilePlan(options);
15767
16171
  if (options.output) {
15768
- writeFileAtomically(import_node_path45.default.resolve(options.output), `${JSON.stringify(report, null, 2)}
16172
+ writeFileAtomically(import_node_path46.default.resolve(options.output), `${JSON.stringify(report, null, 2)}
15769
16173
  `);
15770
16174
  }
15771
16175
  if (options.json) {
@@ -15801,14 +16205,14 @@ function printProfilePlanSummary(report, output2) {
15801
16205
  process.stdout.write(`Binding ${key}: ${renderValues(report.bindings[key] ?? [])}
15802
16206
  `);
15803
16207
  }
15804
- if (output2) process.stdout.write(`Output: ${import_node_path45.default.resolve(output2)}
16208
+ if (output2) process.stdout.write(`Output: ${import_node_path46.default.resolve(output2)}
15805
16209
  `);
15806
16210
  }
15807
- var import_node_path45;
16211
+ var import_node_path46;
15808
16212
  var init_profile = __esm({
15809
16213
  "src/commands/profile.ts"() {
15810
16214
  "use strict";
15811
- import_node_path45 = __toESM(require("path"));
16215
+ import_node_path46 = __toESM(require("path"));
15812
16216
  init_atomic_file();
15813
16217
  init_output();
15814
16218
  init_profile_plan();
@@ -16098,7 +16502,7 @@ function sanitizeRouterId(value) {
16098
16502
  return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
16099
16503
  }
16100
16504
  function repoHash(repoPath) {
16101
- return (0, import_node_crypto17.createHash)("sha1").update(import_node_path46.default.resolve(repoPath)).digest("hex").slice(0, 12);
16505
+ return (0, import_node_crypto17.createHash)("sha1").update(import_node_path47.default.resolve(repoPath)).digest("hex").slice(0, 12);
16102
16506
  }
16103
16507
  function asDockerApp(app) {
16104
16508
  return app.runtime === "docker";
@@ -16177,11 +16581,11 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
16177
16581
  if (dockerApps.length === 0) {
16178
16582
  throw new Error("No docker apps selected to prepare compose overlay.");
16179
16583
  }
16180
- const cachePath = import_node_path46.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
16181
- import_node_fs38.default.mkdirSync(cachePath, { recursive: true });
16182
- const overlayPath = import_node_path46.default.join(cachePath, "compose.devrouter.yml");
16584
+ const cachePath = import_node_path47.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
16585
+ import_node_fs39.default.mkdirSync(cachePath, { recursive: true });
16586
+ const overlayPath = import_node_path47.default.join(cachePath, "compose.devrouter.yml");
16183
16587
  const overlayDocument = buildOverlayDocument(dockerApps, publishTcpPorts);
16184
- import_node_fs38.default.writeFileSync(overlayPath, import_yaml8.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
16588
+ import_node_fs39.default.writeFileSync(overlayPath, import_yaml8.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
16185
16589
  return {
16186
16590
  overlayPath,
16187
16591
  composeFiles: ensureComposeFiles(dockerApps),
@@ -16301,14 +16705,14 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
16301
16705
  const port = Number(match[1]);
16302
16706
  return Number.isInteger(port) && port > 0 ? port : void 0;
16303
16707
  }
16304
- var import_node_child_process27, import_node_crypto17, import_node_fs38, import_node_path46, import_yaml8;
16708
+ var import_node_child_process27, import_node_crypto17, import_node_fs39, import_node_path47, import_yaml8;
16305
16709
  var init_docker_run = __esm({
16306
16710
  "src/core/docker-run.ts"() {
16307
16711
  "use strict";
16308
16712
  import_node_child_process27 = require("child_process");
16309
16713
  import_node_crypto17 = require("crypto");
16310
- import_node_fs38 = __toESM(require("fs"));
16311
- import_node_path46 = __toESM(require("path"));
16714
+ import_node_fs39 = __toESM(require("fs"));
16715
+ import_node_path47 = __toESM(require("path"));
16312
16716
  import_yaml8 = __toESM(require("yaml"));
16313
16717
  init_docker_error_guidance();
16314
16718
  init_paths();
@@ -17130,13 +17534,13 @@ function measureWorktreeConsumption(worktreePath, options) {
17130
17534
  const startedAt = Date.now();
17131
17535
  let rootStat;
17132
17536
  try {
17133
- rootStat = import_node_fs39.default.lstatSync(worktreePath);
17537
+ rootStat = import_node_fs40.default.lstatSync(worktreePath);
17134
17538
  } catch (error) {
17135
17539
  return { status: "unknown", reason: describeError(error, worktreePath) };
17136
17540
  }
17137
17541
  let rootEntries;
17138
17542
  try {
17139
- rootEntries = import_node_fs39.default.readdirSync(worktreePath, { withFileTypes: true });
17543
+ rootEntries = import_node_fs40.default.readdirSync(worktreePath, { withFileTypes: true });
17140
17544
  } catch (error) {
17141
17545
  return { status: "unknown", reason: describeError(error, worktreePath) };
17142
17546
  }
@@ -17162,10 +17566,10 @@ function measureWorktreeConsumption(worktreePath, options) {
17162
17566
  timedOut = true;
17163
17567
  break;
17164
17568
  }
17165
- const entryPath = import_node_path47.default.join(dirPath, entry.name);
17569
+ const entryPath = import_node_path48.default.join(dirPath, entry.name);
17166
17570
  let entryStat;
17167
17571
  try {
17168
- entryStat = import_node_fs39.default.lstatSync(entryPath);
17572
+ entryStat = import_node_fs40.default.lstatSync(entryPath);
17169
17573
  } catch (error) {
17170
17574
  if (error?.code === "ENOENT") continue;
17171
17575
  unreadableReason = describeIncompleteWalk(error);
@@ -17175,7 +17579,7 @@ function measureWorktreeConsumption(worktreePath, options) {
17175
17579
  if (!entryStat.isDirectory()) continue;
17176
17580
  let childEntries;
17177
17581
  try {
17178
- childEntries = import_node_fs39.default.readdirSync(entryPath, { withFileTypes: true });
17582
+ childEntries = import_node_fs40.default.readdirSync(entryPath, { withFileTypes: true });
17179
17583
  } catch (error) {
17180
17584
  unreadableReason = describeIncompleteWalk(error);
17181
17585
  break;
@@ -17239,12 +17643,12 @@ function describeError(error, worktreePath) {
17239
17643
  }
17240
17644
  return `could not stat worktree path '${worktreePath}': ${error?.message ?? String(error)}`;
17241
17645
  }
17242
- var import_node_fs39, import_node_path47, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
17646
+ var import_node_fs40, import_node_path48, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
17243
17647
  var init_workspace_consumption = __esm({
17244
17648
  "src/core/workspace-consumption.ts"() {
17245
17649
  "use strict";
17246
- import_node_fs39 = __toESM(require("fs"));
17247
- import_node_path47 = __toESM(require("path"));
17650
+ import_node_fs40 = __toESM(require("fs"));
17651
+ import_node_path48 = __toESM(require("path"));
17248
17652
  init_devpod_environment();
17249
17653
  DEFAULT_DEADLINE_MS = 1e4;
17250
17654
  BLOCK_SIZE_BYTES = 512;
@@ -17252,7 +17656,7 @@ var init_workspace_consumption = __esm({
17252
17656
  });
17253
17657
 
17254
17658
  // src/core/workspace-cleanup.ts
17255
- function isRecord5(value) {
17659
+ function isRecord6(value) {
17256
17660
  return value !== null && typeof value === "object" && !Array.isArray(value);
17257
17661
  }
17258
17662
  function isSha(value) {
@@ -17345,7 +17749,7 @@ function evaluateWorkspaceActivity(evidence, cutoff) {
17345
17749
  }
17346
17750
  function readGitSnapshot(worktree, commandRunner) {
17347
17751
  const comparablePath = comparableWorkspacePath(worktree.path);
17348
- if (worktree.prunable || !import_node_fs40.default.existsSync(comparablePath)) {
17752
+ if (worktree.prunable || !import_node_fs41.default.existsSync(comparablePath)) {
17349
17753
  return { worktree, checkout: "missing", head: null, committerDate: null };
17350
17754
  }
17351
17755
  const head = gitOutput(comparablePath, ["rev-parse", "--verify", "HEAD"], commandRunner);
@@ -17396,14 +17800,14 @@ function parseGitHubChanges(value, project, branch) {
17396
17800
  if (!Array.isArray(value)) return void 0;
17397
17801
  const changes = [];
17398
17802
  for (const item of value) {
17399
- if (!isRecord5(item)) return void 0;
17400
- const repository = isRecord5(item.repository) ? item.repository.nameWithOwner : void 0;
17401
- const headRepository = isRecord5(item.headRepository) ? item.headRepository.nameWithOwner : void 0;
17803
+ if (!isRecord6(item)) return void 0;
17804
+ const repository = isRecord6(item.repository) ? item.repository.nameWithOwner : void 0;
17805
+ const headRepository = isRecord6(item.headRepository) ? item.headRepository.nameWithOwner : void 0;
17402
17806
  if (repository !== project || headRepository !== project || item.headRefName !== branch || !isSha(item.headRefOid) || typeof item.baseRefName !== "string") {
17403
17807
  continue;
17404
17808
  }
17405
17809
  const merged = item.state === "MERGED" && validTimestamp(item.mergedAt);
17406
- const mergeCommit = isRecord5(item.mergeCommit) && isSha(item.mergeCommit.oid) ? item.mergeCommit.oid : void 0;
17810
+ const mergeCommit = isRecord6(item.mergeCommit) && isSha(item.mergeCommit.oid) ? item.mergeCommit.oid : void 0;
17407
17811
  const baseSha = isSha(item.baseRefOid) ? item.baseRefOid : void 0;
17408
17812
  changes.push({
17409
17813
  sourceBranch: branch,
@@ -17420,14 +17824,14 @@ function parseGitLabChanges(value, _project, branch) {
17420
17824
  if (!Array.isArray(value)) return void 0;
17421
17825
  const changes = [];
17422
17826
  for (const item of value) {
17423
- if (!isRecord5(item)) return void 0;
17827
+ if (!isRecord6(item)) return void 0;
17424
17828
  const sourceProjectId = item.source_project_id;
17425
17829
  const targetProjectId = item.target_project_id;
17426
17830
  const sameProject = typeof sourceProjectId === "number" && typeof targetProjectId === "number" && sourceProjectId === targetProjectId;
17427
17831
  if (!sameProject || item.source_branch !== branch || !isSha(item.sha) || typeof item.target_branch !== "string") {
17428
17832
  continue;
17429
17833
  }
17430
- const diffRefs = isRecord5(item.diff_refs) ? item.diff_refs : void 0;
17834
+ const diffRefs = isRecord6(item.diff_refs) ? item.diff_refs : void 0;
17431
17835
  const mergeCommitSha = isSha(item.merge_commit_sha) ? item.merge_commit_sha : void 0;
17432
17836
  const baseSha = diffRefs && isSha(diffRefs.base_sha) ? diffRefs.base_sha : void 0;
17433
17837
  changes.push({
@@ -17870,7 +18274,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
17870
18274
  containers2 = measureContainersFn(worktreePaths);
17871
18275
  } catch (error) {
17872
18276
  const reason = `container measurement failed: ${describeCause(error)}`;
17873
- containers2 = new Map(worktreePaths.map((path48) => [path48, unknownContainers(reason)]));
18277
+ containers2 = new Map(worktreePaths.map((path49) => [path49, unknownContainers(reason)]));
17874
18278
  }
17875
18279
  }
17876
18280
  const rows = records.map((record4) => {
@@ -17919,12 +18323,12 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
17919
18323
  workspaces: rows
17920
18324
  };
17921
18325
  }
17922
- var import_node_child_process29, import_node_fs40, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
18326
+ var import_node_child_process29, import_node_fs41, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
17923
18327
  var init_workspace_cleanup = __esm({
17924
18328
  "src/core/workspace-cleanup.ts"() {
17925
18329
  "use strict";
17926
18330
  import_node_child_process29 = require("child_process");
17927
- import_node_fs40 = __toESM(require("fs"));
18331
+ import_node_fs41 = __toESM(require("fs"));
17928
18332
  init_devpod_workspaces();
17929
18333
  init_host_routes();
17930
18334
  init_repo_config();
@@ -17971,7 +18375,7 @@ function warnMissingWorkspaceOwnership(repoPath) {
17971
18375
  );
17972
18376
  }
17973
18377
  function defaultWorktreePath(mainRepo, ws) {
17974
- return import_node_path48.default.join(mainRepo, "trees", ws);
18378
+ return import_node_path49.default.join(mainRepo, "trees", ws);
17975
18379
  }
17976
18380
  function assertDefaultWorktreeRootIgnored(mainRepo) {
17977
18381
  const ignored = (0, import_node_child_process30.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
@@ -17979,12 +18383,12 @@ function assertDefaultWorktreeRootIgnored(mainRepo) {
17979
18383
  });
17980
18384
  if (ignored.status !== 0) {
17981
18385
  throw new Error(
17982
- `Default worktree root '${import_node_path48.default.join(mainRepo, "trees")}' is not ignored. Add 'trees/' to '${import_node_path48.default.join(mainRepo, ".gitignore")}' or use --path.`
18386
+ `Default worktree root '${import_node_path49.default.join(mainRepo, "trees")}' is not ignored. Add 'trees/' to '${import_node_path49.default.join(mainRepo, ".gitignore")}' or use --path.`
17983
18387
  );
17984
18388
  }
17985
18389
  }
17986
18390
  function legacyDefaultWorktreePath(mainRepo, ws) {
17987
- return import_node_path48.default.join(import_node_path48.default.dirname(mainRepo), `${import_node_path48.default.basename(mainRepo)}-${ws}`);
18391
+ return import_node_path49.default.join(import_node_path49.default.dirname(mainRepo), `${import_node_path49.default.basename(mainRepo)}-${ws}`);
17988
18392
  }
17989
18393
  function teardownFallbackPath(mainRepo, workspace) {
17990
18394
  const candidates = [
@@ -18096,7 +18500,7 @@ function assertFullDownPreflight(mainRepo, target) {
18096
18500
  if (sameWorkspacePath(target.worktreePath, mainRepo)) {
18097
18501
  throw new Error("Refusing to remove the primary Git checkout.");
18098
18502
  }
18099
- if (!target.worktree || target.worktree.prunable || !import_node_fs41.default.existsSync(target.worktreePath)) return;
18503
+ if (!target.worktree || target.worktree.prunable || !import_node_fs42.default.existsSync(target.worktreePath)) return;
18100
18504
  if (target.worktree.locked) {
18101
18505
  throw new Error(
18102
18506
  `Worktree '${target.worktreePath}' is locked; unlock it before workspace down.`
@@ -18136,9 +18540,9 @@ async function workspaceUp(branch, opts = {}) {
18136
18540
  (candidate) => defaultWorktreePath(mainRepo, candidate)
18137
18541
  );
18138
18542
  const generatedPath = candidatePaths.find(
18139
- (candidate) => !import_node_fs41.default.existsSync(candidate) && !worktrees.some((worktree) => sameWorkspacePath(worktree.path, candidate))
18543
+ (candidate) => !import_node_fs42.default.existsSync(candidate) && !worktrees.some((worktree) => sameWorkspacePath(worktree.path, candidate))
18140
18544
  );
18141
- const selectedPath = opts.path ? import_node_path48.default.resolve(opts.path) : existingBranch?.path ?? generatedPath;
18545
+ const selectedPath = opts.path ? import_node_path49.default.resolve(opts.path) : existingBranch?.path ?? generatedPath;
18142
18546
  if (!selectedPath) {
18143
18547
  throw new Error(`Could not allocate a collision-safe worktree path for branch '${branch}'.`);
18144
18548
  }
@@ -18147,7 +18551,7 @@ async function workspaceUp(branch, opts = {}) {
18147
18551
  `Branch '${branch}' already uses worktree '${existingBranch.path}', not '${selectedPath}'.`
18148
18552
  );
18149
18553
  }
18150
- if (import_node_fs41.default.existsSync(selectedPath)) {
18554
+ if (import_node_fs42.default.existsSync(selectedPath)) {
18151
18555
  const registered = worktrees.find(
18152
18556
  (worktree) => sameWorkspacePath(worktree.path, selectedPath)
18153
18557
  );
@@ -18249,8 +18653,24 @@ async function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = fals
18249
18653
  "Retained managed runtime has no exact provider registration; routes were preserved."
18250
18654
  );
18251
18655
  }
18252
- const routes = removeWorkspaceRoutesForWorktree(resolved.workspace, resolved.worktreePath);
18253
- await ensureTraefikRoutesRemoved(routes);
18656
+ const retained = action2 === "stop" ? readManagedRuntimeState(resolved.worktreePath, resolved.workspace) : void 0;
18657
+ let absent = false;
18658
+ const remove = () => {
18659
+ if (retained?.stopBaseline) {
18660
+ absent = proveManagedStop(retained).status === "proven-absent";
18661
+ if (absent && listRoutesForWorktreePath(resolved.worktreePath).some(
18662
+ (route) => route.workspace !== resolved.workspace || !retained.desired.apps.includes(route.name)
18663
+ ))
18664
+ throw new Error("Absent stop contains routes outside the retained desired set.");
18665
+ }
18666
+ claimLifecycleEffect();
18667
+ return removeWorkspaceRoutesForWorktree(resolved.workspace, resolved.worktreePath);
18668
+ };
18669
+ const routes = retained?.stopBaseline ? withMutationLock("Revalidate stop routes", resolved.worktreePath, remove) : remove();
18670
+ await ensureTraefikRoutesRemoved([
18671
+ ...routes,
18672
+ ...absent && retained ? managedStopRouteReferences(retained) : []
18673
+ ]);
18254
18674
  if (!quiet) {
18255
18675
  process.stdout.write(
18256
18676
  `Freed ${routes.length} route(s) for workspace '${resolved.workspace}'.
@@ -18261,6 +18681,7 @@ async function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = fals
18261
18681
  ...mutation.status === "changed" ? { devpodId } : {},
18262
18682
  freedRoutes: routes.length,
18263
18683
  providerChanged: mutation.status === "changed",
18684
+ ...absent ? { runtimeAbsent: true } : {},
18264
18685
  workspace: resolved.workspace
18265
18686
  };
18266
18687
  }
@@ -18287,7 +18708,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
18287
18708
  }
18288
18709
  const result = await mutateWorkspaceRuntime("delete", resolved, worktrees, opts.quiet);
18289
18710
  if (removeWorktree) {
18290
- if (resolved.worktree && !resolved.worktree.prunable && import_node_fs41.default.existsSync(resolved.worktreePath)) {
18711
+ if (resolved.worktree && !resolved.worktree.prunable && import_node_fs42.default.existsSync(resolved.worktreePath)) {
18291
18712
  const rm = (0, import_node_child_process30.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
18292
18713
  encoding: "utf-8"
18293
18714
  });
@@ -18306,7 +18727,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
18306
18727
  }
18307
18728
  return result;
18308
18729
  };
18309
- return resolved.worktree && !resolved.worktree.prunable && import_node_fs41.default.existsSync(resolved.worktreePath) ? withLifecycleOperationLock(resolved.worktreePath, operation) : operation();
18730
+ return resolved.worktree && !resolved.worktree.prunable && import_node_fs42.default.existsSync(resolved.worktreePath) ? withLifecycleOperationLock(resolved.worktreePath, operation) : operation();
18310
18731
  }
18311
18732
  async function workspaceStop(target, opts = {}) {
18312
18733
  return runWorkspaceLifecycle("stop", target, opts);
@@ -18314,16 +18735,18 @@ async function workspaceStop(target, opts = {}) {
18314
18735
  async function workspaceDown(target, opts = {}) {
18315
18736
  return runWorkspaceLifecycle("down", target, opts);
18316
18737
  }
18317
- var import_node_child_process30, import_node_fs41, import_node_path48, WORKSPACE_ALLOCATION_LOCK_WAIT_MS;
18738
+ var import_node_child_process30, import_node_fs42, import_node_path49, WORKSPACE_ALLOCATION_LOCK_WAIT_MS;
18318
18739
  var init_workspace_lifecycle = __esm({
18319
18740
  "src/core/workspace-lifecycle.ts"() {
18320
18741
  "use strict";
18321
18742
  import_node_child_process30 = require("child_process");
18322
- import_node_fs41 = __toESM(require("fs"));
18323
- import_node_path48 = __toESM(require("path"));
18743
+ import_node_fs42 = __toESM(require("fs"));
18744
+ import_node_path49 = __toESM(require("path"));
18324
18745
  init_devpod_mutation();
18325
18746
  init_devpod_workspaces();
18747
+ init_devsy_mutation();
18326
18748
  init_managed_runtime_state();
18749
+ init_managed_stop_recovery();
18327
18750
  init_reliability_lifecycle();
18328
18751
  init_repo_config();
18329
18752
  init_route_state();
@@ -18479,7 +18902,7 @@ var init_version = __esm({
18479
18902
 
18480
18903
  // src/cli.ts
18481
18904
  var import_commander = require("commander");
18482
- var CLI_VERSION = true ? "0.0.61" : "0.0.0-dev";
18905
+ var CLI_VERSION = true ? "0.0.63" : "0.0.0-dev";
18483
18906
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
18484
18907
  function withErrorHandling(action2) {
18485
18908
  return async (...args) => {