@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.
@@ -936,6 +936,28 @@ function inspectManagedStopRunnerId(endpoint, containerId) {
936
936
  throw new Error("Provider container binding is unavailable.");
937
937
  return value;
938
938
  }
939
+ function inspectProviderRunnerContainers(endpoint, runnerId) {
940
+ if (!runnerId || runnerId.length > 4096 || /[\r\n\0]/.test(runnerId))
941
+ throw new Error("Provider runner identity is malformed.");
942
+ const ids = parseDockerLines(
943
+ runManagedStopDocker(
944
+ [
945
+ "ps",
946
+ "-a",
947
+ "--no-trunc",
948
+ "--filter",
949
+ `label=dev.containers.id=${runnerId}`,
950
+ "--format",
951
+ "{{.ID}}"
952
+ ],
953
+ endpoint
954
+ )
955
+ );
956
+ if (ids.length > 256) throw new Error("Provider runner population exceeds its bound.");
957
+ ids.forEach(assertFullContainerId);
958
+ assertUniqueContainerIds(ids);
959
+ return ids;
960
+ }
939
961
  function inspectManagedStopWorkspaceIds(endpoint, composeDirectory) {
940
962
  const ids = parseDockerLines(
941
963
  runManagedStopDocker(
@@ -956,6 +978,33 @@ function inspectManagedStopWorkspaceIds(endpoint, composeDirectory) {
956
978
  assertUniqueContainerIds(ids);
957
979
  return ids;
958
980
  }
981
+ function hasExactManagedStopMissingObjectError(stderr, id2) {
982
+ if (typeof stderr !== "string") return false;
983
+ const line = stderr.endsWith("\r\n") ? stderr.slice(0, -2) : stderr.endsWith("\n") ? stderr.slice(0, -1) : stderr;
984
+ return line.toLowerCase() === `error: no such object: ${id2}`;
985
+ }
986
+ function assertManagedStopContainersAbsent(endpoint, ids) {
987
+ assertManagedStopEndpoint(endpoint);
988
+ if (!Array.isArray(ids) || ids.length === 0 || ids.length > 256) {
989
+ throw new Error("Managed stop absence proof requires between 1 and 256 container ids.");
990
+ }
991
+ ids.forEach(assertFullContainerId);
992
+ assertUniqueContainerIds(ids);
993
+ const env = { ...process.env };
994
+ delete env.DOCKER_CONTEXT;
995
+ delete env.DOCKER_HOST;
996
+ for (const id2 of ids) {
997
+ const result = (0, import_node_child_process3.spawnSync)("docker", ["--host", endpoint, "inspect", "--format", "{{.Id}}", id2], {
998
+ env,
999
+ encoding: "utf-8",
1000
+ timeout: MANAGED_STOP_DOCKER_TIMEOUT_MS,
1001
+ maxBuffer: MANAGED_STOP_DOCKER_MAX_BUFFER
1002
+ });
1003
+ if (result.status !== 1 || result.error || result.signal || result.stdout !== "" && result.stdout !== "\n" && result.stdout !== "\r\n" || !hasExactManagedStopMissingObjectError(result.stderr, id2)) {
1004
+ throw new Error("Managed stop Docker inspection did not prove container absence.");
1005
+ }
1006
+ }
1007
+ }
959
1008
  function stopPinnedManagedContainer(endpoint, containerId) {
960
1009
  assertFullContainerId(containerId);
961
1010
  runManagedStopDocker(["stop", containerId], endpoint, 3e4);
@@ -966,11 +1015,13 @@ var import_node_child_process8 = require("child_process");
966
1015
 
967
1016
  // src/core/devpod-registry.ts
968
1017
  var import_node_child_process4 = require("child_process");
969
- function listDevpodWorkspacesRaw() {
1018
+ function listDevpodWorkspacesRaw(options = {}) {
970
1019
  const result = (0, import_node_child_process4.spawnSync)("devpod", ["list", "--output", "json", "--skip-pro"], {
971
1020
  encoding: "utf-8"
972
1021
  });
973
- if (result.status !== 0) {
1022
+ if (options.allowMissingExecutable && result.error?.code === "ENOENT")
1023
+ return [];
1024
+ if (result.error || result.status !== 0) {
974
1025
  const details = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
975
1026
  throw new Error(`devpod list failed: ${details || "devpod is not installed or unavailable"}`);
976
1027
  }
@@ -1046,6 +1097,10 @@ function listDevsyWorkspaces() {
1046
1097
  ...typeof candidate.uid === "string" ? { uid: candidate.uid } : {},
1047
1098
  ...typeof candidate.context === "string" ? { context: candidate.context } : {}
1048
1099
  };
1100
+ const provider = candidate.provider;
1101
+ if (provider && typeof provider.name === "string") workspace.providerName = provider.name;
1102
+ if (typeof provider?.options?.DOCKER_PATH?.value === "string")
1103
+ workspace.dockerPathOption = provider.options.DOCKER_PATH.value;
1049
1104
  if ("lastUsed" in candidate) {
1050
1105
  if (typeof candidate.lastUsed === "string") {
1051
1106
  workspace.lastUsed = candidate.lastUsed;
@@ -1627,29 +1682,101 @@ function selectDevpodWorkspace(workspaces, repoPath) {
1627
1682
  // src/core/devsy-exec.ts
1628
1683
  var import_node_child_process22 = require("child_process");
1629
1684
 
1630
- // src/core/execution-outcome.ts
1631
- var ExecutionOutcomeError = class extends Error {
1632
- outcome;
1633
- constructor(message, outcome) {
1634
- super(message);
1635
- this.name = "ExecutionOutcomeError";
1636
- this.outcome = outcome;
1685
+ // src/core/devsy-exec-proof.ts
1686
+ var import_node_fs6 = __toESM(require("fs"));
1687
+ var import_node_os2 = __toESM(require("os"));
1688
+ var import_node_path7 = __toESM(require("path"));
1689
+ var import_node_util = require("util");
1690
+ function proveLocalDockerSelection(workspace) {
1691
+ const name = workspace.providerName;
1692
+ const context = workspace.context;
1693
+ if (!name || !context || !/^[a-z0-9][a-z0-9-]{0,31}$/.test(name) || !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(context))
1694
+ throw new Error("Retained exec provider configuration identity is unavailable.");
1695
+ const file = import_node_path7.default.join(
1696
+ process.env.DEVSY_HOME || import_node_path7.default.join(import_node_os2.default.homedir(), ".devsy"),
1697
+ "contexts",
1698
+ context,
1699
+ "providers",
1700
+ name,
1701
+ "provider.json"
1702
+ );
1703
+ try {
1704
+ if (import_node_fs6.default.statSync(file).size > 1024 * 1024) throw new Error();
1705
+ const config = JSON.parse(import_node_fs6.default.readFileSync(file, "utf8"));
1706
+ const driver = config.agent?.driver ?? "docker";
1707
+ const configuredPath = config.agent?.docker?.path ?? "";
1708
+ if (typeof configuredPath !== "string") throw new Error();
1709
+ const command = configuredPath === "${DOCKER_PATH}" || configuredPath === "$DOCKER_PATH" ? workspace.dockerPathOption || process.env.DOCKER_PATH || "docker" : configuredPath || "docker";
1710
+ if (config.name !== name || driver !== "docker" || command !== "docker") throw new Error();
1711
+ } catch {
1712
+ throw new Error("Retained exec requires a provider configured for the local Docker command.");
1637
1713
  }
1638
- };
1639
-
1640
- // src/core/reliability-lifecycle.ts
1641
- var import_node_path27 = __toESM(require("path"));
1714
+ return name;
1715
+ }
1716
+ function captureDevsyExecProof(repoPath) {
1717
+ repoPath = comparableWorkspacePath(repoPath);
1718
+ const registry = listDevsyWorkspaces();
1719
+ const workspace = selectDevsyWorkspace(registry, repoPath);
1720
+ if (!workspace?.uid || !workspace.context || workspace.source.container || inspectDevsyWorkspaceOwnership(registry, workspace.id, repoPath).status !== "owned" || inspectDevsyRuntimeStatus(workspace.id) !== "running")
1721
+ throw new Error("Retained exec requires one exact running Devsy registration.");
1722
+ const providerName = proveLocalDockerSelection(workspace);
1723
+ const endpoint = resolveManagedStopEndpoint();
1724
+ if (!supportsManagedStopBaseline(endpoint))
1725
+ throw new Error("Retained exec requires an exact local Docker endpoint.");
1726
+ const daemon = inspectManagedStopDaemon(endpoint);
1727
+ const candidates = workspaceAppContainers(inspectWorkspaceContainers(), repoPath).filter(
1728
+ (container) => container.state.Running
1729
+ );
1730
+ if (candidates.length !== 1)
1731
+ throw new Error("Retained exec requires one exact running primary container.");
1732
+ const candidate = candidates[0];
1733
+ const project = candidate.labels["com.docker.compose.project"];
1734
+ if (!project) throw new Error("Retained exec primary has no Compose identity.");
1735
+ const pinned = workspaceAppContainers(
1736
+ inspectManagedStopContainers(project, endpoint),
1737
+ repoPath
1738
+ ).filter((container) => container.state.Running);
1739
+ if (pinned.length !== 1 || pinned[0].id !== candidate.id)
1740
+ throw new Error("Retained exec primary changed during inspection.");
1741
+ const mounts = pinned[0].mounts.filter(
1742
+ (mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath)
1743
+ );
1744
+ if (mounts.length !== 1 || !mounts[0].Destination.startsWith("/"))
1745
+ throw new Error("Retained exec requires one exact source mount.");
1746
+ const uidBytes = Buffer.byteLength(workspace.uid);
1747
+ const runner = uidBytes === 16 || uidBytes === 40 ? workspace.uid : workspace.id;
1748
+ const runnerContainers = inspectProviderRunnerContainers(endpoint, runner);
1749
+ if (runnerContainers.length !== 1 || runnerContainers[0] !== candidate.id)
1750
+ throw new Error("Retained exec runner does not select the exact primary container.");
1751
+ if (inspectManagedStopRunnerId(endpoint, candidate.id) !== runner || inspectManagedStopDaemon(endpoint) !== daemon)
1752
+ throw new Error("Retained exec provider or Docker identity changed.");
1753
+ return {
1754
+ repoPath,
1755
+ id: workspace.id,
1756
+ uid: workspace.uid,
1757
+ context: workspace.context,
1758
+ providerName,
1759
+ endpoint,
1760
+ daemon,
1761
+ containerId: candidate.id,
1762
+ workspacePath: mounts[0].Destination
1763
+ };
1764
+ }
1765
+ function revalidateDevsyExecProof(repoPath, proof) {
1766
+ if (!(0, import_node_util.isDeepStrictEqual)(captureDevsyExecProof(repoPath), proof))
1767
+ throw new Error("Retained exec identity changed before command launch.");
1768
+ }
1642
1769
 
1643
1770
  // src/core/devsy-mutation.ts
1644
1771
  var import_node_child_process14 = require("child_process");
1645
- var import_node_fs15 = __toESM(require("fs"));
1646
- var import_node_path20 = __toESM(require("path"));
1772
+ var import_node_fs16 = __toESM(require("fs"));
1773
+ var import_node_path21 = __toESM(require("path"));
1647
1774
 
1648
1775
  // src/core/devsy-agent.ts
1649
1776
  var import_node_child_process9 = require("child_process");
1650
1777
  var import_node_crypto4 = require("crypto");
1651
- var import_node_fs6 = __toESM(require("fs"));
1652
- var import_node_path7 = __toESM(require("path"));
1778
+ var import_node_fs7 = __toESM(require("fs"));
1779
+ var import_node_path8 = __toESM(require("path"));
1653
1780
  var SUPPORTED_DEVSY_VERSION = "1.16.2";
1654
1781
  var DEVSY_AGENT_SETUP_COMMAND = "devrouter setup --yes --workspace-runtime devsy";
1655
1782
  var DEVSY_AGENT_ASSETS = [
@@ -1675,7 +1802,7 @@ var DevsyAgentReadinessError = class extends Error {
1675
1802
  this.name = "DevsyAgentReadinessError";
1676
1803
  }
1677
1804
  };
1678
- var DEVSY_AGENT_CACHE_LOCK = import_node_path7.default.join(DEVROUTER_HOME, "devsy-agent-cache.lock");
1805
+ var DEVSY_AGENT_CACHE_LOCK = import_node_path8.default.join(DEVROUTER_HOME, "devsy-agent-cache.lock");
1679
1806
  function parseVersion(output) {
1680
1807
  return output?.match(
1681
1808
  /\bv?(\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?)\b/
@@ -1699,14 +1826,14 @@ function nativeAssetName(platform, arch) {
1699
1826
  return void 0;
1700
1827
  }
1701
1828
  function managedBinaryPath(cacheRoot, asset) {
1702
- return import_node_path7.default.join(cacheRoot, `v${SUPPORTED_DEVSY_VERSION}`, asset.name);
1829
+ return import_node_path8.default.join(cacheRoot, `v${SUPPORTED_DEVSY_VERSION}`, asset.name);
1703
1830
  }
1704
1831
  function hashOpenFile(file) {
1705
1832
  const digest = (0, import_node_crypto4.createHash)("sha256");
1706
1833
  const buffer = Buffer.allocUnsafe(1024 * 1024);
1707
1834
  let position = 0;
1708
1835
  for (; ; ) {
1709
- const bytesRead = import_node_fs6.default.readSync(file, buffer, 0, buffer.length, position);
1836
+ const bytesRead = import_node_fs7.default.readSync(file, buffer, 0, buffer.length, position);
1710
1837
  if (bytesRead === 0) break;
1711
1838
  digest.update(buffer.subarray(0, bytesRead));
1712
1839
  position += bytesRead;
@@ -1716,8 +1843,8 @@ function hashOpenFile(file) {
1716
1843
  function inspectBinary(binaryPath, assets, missingState) {
1717
1844
  let file;
1718
1845
  try {
1719
- file = import_node_fs6.default.openSync(binaryPath, import_node_fs6.default.constants.O_RDONLY | import_node_fs6.default.constants.O_NOFOLLOW);
1720
- const stat = import_node_fs6.default.fstatSync(file);
1846
+ file = import_node_fs7.default.openSync(binaryPath, import_node_fs7.default.constants.O_RDONLY | import_node_fs7.default.constants.O_NOFOLLOW);
1847
+ const stat = import_node_fs7.default.fstatSync(file);
1721
1848
  if (!stat.isFile()) {
1722
1849
  return { state: "invalid", reason: "the selected source is not a regular file" };
1723
1850
  }
@@ -1732,7 +1859,7 @@ function inspectBinary(binaryPath, assets, missingState) {
1732
1859
  const code = error.code;
1733
1860
  return code === "ENOENT" ? { state: missingState, reason: "the selected source is missing" } : { state: "invalid", reason: "the selected source is not a readable regular file" };
1734
1861
  } finally {
1735
- if (file !== void 0) import_node_fs6.default.closeSync(file);
1862
+ if (file !== void 0) import_node_fs7.default.closeSync(file);
1736
1863
  }
1737
1864
  }
1738
1865
  function resolvedOptions(options) {
@@ -1741,7 +1868,7 @@ function resolvedOptions(options) {
1741
1868
  platform: options.platform ?? process.platform,
1742
1869
  arch: options.arch ?? process.arch,
1743
1870
  versionOutput: options.versionOutput ?? installedVersionOutput(),
1744
- cacheRoot: options.cacheRoot ?? import_node_path7.default.join(CACHE_DIR, "devsy", "agents"),
1871
+ cacheRoot: options.cacheRoot ?? import_node_path8.default.join(CACHE_DIR, "devsy", "agents"),
1745
1872
  lockPath: options.lockPath ?? DEVSY_AGENT_CACHE_LOCK,
1746
1873
  assets: options.assets ?? DEVSY_AGENT_ASSETS,
1747
1874
  nativeAssetName: options.nativeAssetName
@@ -1802,19 +1929,19 @@ function requireReadyDevsyAgent(options = {}) {
1802
1929
  }
1803
1930
 
1804
1931
  // src/core/managed-devsy-stop.ts
1805
- var import_node_os3 = __toESM(require("os"));
1806
- var import_node_path19 = __toESM(require("path"));
1932
+ var import_node_os4 = __toESM(require("os"));
1933
+ var import_node_path20 = __toESM(require("path"));
1807
1934
 
1808
1935
  // src/core/devcontainer-profile.ts
1809
1936
  var import_node_child_process10 = require("child_process");
1810
1937
  var import_node_crypto5 = require("crypto");
1811
- var import_node_fs8 = __toESM(require("fs"));
1812
- var import_node_path10 = __toESM(require("path"));
1938
+ var import_node_fs9 = __toESM(require("fs"));
1939
+ var import_node_path11 = __toESM(require("path"));
1813
1940
  var import_yaml = __toESM(require("yaml"));
1814
1941
 
1815
1942
  // src/core/devcontainer-config.ts
1816
- var import_node_fs7 = __toESM(require("fs"));
1817
- var import_node_path8 = __toESM(require("path"));
1943
+ var import_node_fs8 = __toESM(require("fs"));
1944
+ var import_node_path9 = __toESM(require("path"));
1818
1945
  var import_jsonc_parser = require("jsonc-parser");
1819
1946
  function isObject(value) {
1820
1947
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -1837,11 +1964,11 @@ function parseDevcontainerConfig(contents, sourcePath) {
1837
1964
  return parsed;
1838
1965
  }
1839
1966
  function readDevcontainerConfig(repoPath) {
1840
- const sourcePath = import_node_path8.default.join(repoPath, ".devcontainer", "devcontainer.json");
1841
- if (!import_node_fs7.default.existsSync(sourcePath) || !import_node_fs7.default.lstatSync(sourcePath).isFile()) {
1967
+ const sourcePath = import_node_path9.default.join(repoPath, ".devcontainer", "devcontainer.json");
1968
+ if (!import_node_fs8.default.existsSync(sourcePath) || !import_node_fs8.default.lstatSync(sourcePath).isFile()) {
1842
1969
  throw new Error(`Managed Dev Container source config does not exist: ${sourcePath}`);
1843
1970
  }
1844
- const sourceContents = import_node_fs7.default.readFileSync(sourcePath, "utf-8");
1971
+ const sourceContents = import_node_fs8.default.readFileSync(sourcePath, "utf-8");
1845
1972
  return {
1846
1973
  sourcePath,
1847
1974
  sourceContents,
@@ -1859,11 +1986,11 @@ function assertManagedDevcontainerLifecycle(repoPath) {
1859
1986
  }
1860
1987
 
1861
1988
  // src/core/paths.ts
1862
- var import_node_path9 = __toESM(require("path"));
1989
+ var import_node_path10 = __toESM(require("path"));
1863
1990
  function assertPathWithinRepo(filePath, repoRoot, label) {
1864
- const resolvedRoot = import_node_path9.default.resolve(repoRoot);
1865
- const resolved = import_node_path9.default.resolve(repoRoot, filePath);
1866
- if (resolved !== resolvedRoot && !resolved.startsWith(resolvedRoot + import_node_path9.default.sep)) {
1991
+ const resolvedRoot = import_node_path10.default.resolve(repoRoot);
1992
+ const resolved = import_node_path10.default.resolve(repoRoot, filePath);
1993
+ if (resolved !== resolvedRoot && !resolved.startsWith(resolvedRoot + import_node_path10.default.sep)) {
1867
1994
  throw new Error(`${label} path '${filePath}' escapes the repository root.`);
1868
1995
  }
1869
1996
  return resolved;
@@ -1917,14 +2044,14 @@ function resolveComposeFiles(source, repoPath, linked) {
1917
2044
  "managedRuntime requires .devcontainer/devcontainer.json to define dockerComposeFile."
1918
2045
  );
1919
2046
  }
1920
- const directory = import_node_path10.default.join(repoPath, ".devcontainer");
2047
+ const directory = import_node_path11.default.join(repoPath, ".devcontainer");
1921
2048
  const files = references.map((reference) => {
1922
2049
  const resolved2 = assertPathWithinRepo(
1923
2050
  resolveComposeReference(reference, linked),
1924
2051
  directory,
1925
2052
  "dockerComposeFile"
1926
2053
  );
1927
- if (!import_node_fs8.default.existsSync(resolved2) || !import_node_fs8.default.lstatSync(resolved2).isFile()) {
2054
+ if (!import_node_fs9.default.existsSync(resolved2) || !import_node_fs9.default.lstatSync(resolved2).isFile()) {
1928
2055
  throw new Error(`Managed Dev Container compose file does not exist: ${resolved2}`);
1929
2056
  }
1930
2057
  return resolved2;
@@ -1932,7 +2059,7 @@ function resolveComposeFiles(source, repoPath, linked) {
1932
2059
  for (const file of files) {
1933
2060
  let parsed;
1934
2061
  try {
1935
- parsed = import_yaml.default.parse(import_node_fs8.default.readFileSync(file, "utf-8"));
2062
+ parsed = import_yaml.default.parse(import_node_fs9.default.readFileSync(file, "utf-8"));
1936
2063
  } catch (error) {
1937
2064
  throw new Error(
1938
2065
  `Could not parse managed Dev Container compose file '${file}': ${String(error)}`
@@ -1981,12 +2108,12 @@ function assertIgnoredGeneratedPath(repoPath, generatedPath) {
1981
2108
  }
1982
2109
  }
1983
2110
  function assertGeneratedPathOwnership(generatedPath) {
1984
- if (!import_node_fs8.default.existsSync(generatedPath)) return;
1985
- const stat = import_node_fs8.default.lstatSync(generatedPath);
2111
+ if (!import_node_fs9.default.existsSync(generatedPath)) return;
2112
+ const stat = import_node_fs9.default.lstatSync(generatedPath);
1986
2113
  if (!stat.isFile() || stat.isSymbolicLink()) {
1987
2114
  throw new Error(`Managed Dev Container path '${generatedPath}' is not a regular file.`);
1988
2115
  }
1989
- const firstLine = import_node_fs8.default.readFileSync(generatedPath, "utf-8").split(/\r?\n/, 1)[0];
2116
+ const firstLine = import_node_fs9.default.readFileSync(generatedPath, "utf-8").split(/\r?\n/, 1)[0];
1990
2117
  if (firstLine !== MANAGED_DEVCONTAINER_MARKER) {
1991
2118
  throw new Error(
1992
2119
  `Managed Dev Container path '${generatedPath}' exists without the devrouter ownership marker.`
@@ -2062,7 +2189,7 @@ function inspectManagedDevcontainerConfig(options) {
2062
2189
  const contents = `${MANAGED_DEVCONTAINER_MARKER}
2063
2190
  ${JSON.stringify(effective, null, 2)}
2064
2191
  `;
2065
- const generatedPath = import_node_path10.default.join(options.repoPath, MANAGED_DEVCONTAINER_PATH);
2192
+ const generatedPath = import_node_path11.default.join(options.repoPath, MANAGED_DEVCONTAINER_PATH);
2066
2193
  assertIgnoredGeneratedPath(options.repoPath, generatedPath);
2067
2194
  assertGeneratedPathOwnership(generatedPath);
2068
2195
  return {
@@ -2089,13 +2216,13 @@ function writeManagedDevcontainerConfig(plan) {
2089
2216
  function inspectManagedDevcontainerGeneratedConfig(plan) {
2090
2217
  let stat;
2091
2218
  try {
2092
- stat = import_node_fs8.default.lstatSync(plan.generatedPath);
2219
+ stat = import_node_fs9.default.lstatSync(plan.generatedPath);
2093
2220
  } catch (error) {
2094
2221
  if (error.code === "ENOENT") return { status: "missing" };
2095
2222
  throw error;
2096
2223
  }
2097
2224
  if (!stat.isFile() || stat.isSymbolicLink()) return { status: "foreign" };
2098
- const contents = import_node_fs8.default.readFileSync(plan.generatedPath, "utf-8");
2225
+ const contents = import_node_fs9.default.readFileSync(plan.generatedPath, "utf-8");
2099
2226
  if (!contents.startsWith(`${MANAGED_DEVCONTAINER_MARKER}
2100
2227
  `)) {
2101
2228
  return { status: "foreign" };
@@ -2107,17 +2234,17 @@ function inspectManagedDevcontainerGeneratedConfig(plan) {
2107
2234
  };
2108
2235
  }
2109
2236
  function removeManagedDevcontainerConfig(plan) {
2110
- if (!import_node_fs8.default.existsSync(plan.generatedPath)) return;
2111
- const stat = import_node_fs8.default.lstatSync(plan.generatedPath);
2237
+ if (!import_node_fs9.default.existsSync(plan.generatedPath)) return;
2238
+ const stat = import_node_fs9.default.lstatSync(plan.generatedPath);
2112
2239
  if (!stat.isFile()) {
2113
2240
  throw new Error(`Managed Dev Container path '${plan.generatedPath}' is not a regular file.`);
2114
2241
  }
2115
- const contents = import_node_fs8.default.readFileSync(plan.generatedPath, "utf-8");
2242
+ const contents = import_node_fs9.default.readFileSync(plan.generatedPath, "utf-8");
2116
2243
  if (!contents.startsWith(`${MANAGED_DEVCONTAINER_MARKER}
2117
2244
  `)) {
2118
2245
  throw new Error(`Managed Dev Container path '${plan.generatedPath}' is not devrouter-owned.`);
2119
2246
  }
2120
- import_node_fs8.default.unlinkSync(plan.generatedPath);
2247
+ import_node_fs9.default.unlinkSync(plan.generatedPath);
2121
2248
  }
2122
2249
  function assertSafeContainerId(containerId) {
2123
2250
  if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(containerId)) {
@@ -2141,8 +2268,8 @@ function managedComposeEnvironment(workspace) {
2141
2268
  }
2142
2269
  function assertReadableComposeFile(file) {
2143
2270
  try {
2144
- if (!import_node_fs8.default.statSync(file).isFile()) throw new Error("not a regular file");
2145
- import_node_fs8.default.accessSync(file, import_node_fs8.default.constants.R_OK);
2271
+ if (!import_node_fs9.default.statSync(file).isFile()) throw new Error("not a regular file");
2272
+ import_node_fs9.default.accessSync(file, import_node_fs9.default.constants.R_OK);
2146
2273
  } catch {
2147
2274
  throw new Error("Managed container Compose configuration contains an unreadable file.");
2148
2275
  }
@@ -2274,8 +2401,8 @@ function stopExactManagedService(containerId, service, options) {
2274
2401
  }
2275
2402
 
2276
2403
  // src/core/managed-compose-population.ts
2277
- var import_node_fs9 = __toESM(require("fs"));
2278
- var import_node_path11 = __toESM(require("path"));
2404
+ var import_node_fs10 = __toESM(require("fs"));
2405
+ var import_node_path12 = __toESM(require("path"));
2279
2406
  function proveManagedComposePopulation(options) {
2280
2407
  const { plan, repoPath, composeProject, featureDirectory, devsyRoot, containers } = options;
2281
2408
  const services = /* @__PURE__ */ new Set();
@@ -2289,13 +2416,13 @@ function proveManagedComposePopulation(options) {
2289
2416
  }
2290
2417
  services.add(service);
2291
2418
  const files = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").map((file) => file.trim());
2292
- if (files.some((file) => !file || !import_node_path11.default.isAbsolute(file)) || new Set(files.map((file) => import_node_path11.default.resolve(file))).size !== files.length || files.length < plan.composeFiles.length || plan.composeFiles.some((file, index) => !sameWorkspacePath(file, files[index]))) {
2419
+ if (files.some((file) => !file || !import_node_path12.default.isAbsolute(file)) || new Set(files.map((file) => import_node_path12.default.resolve(file))).size !== files.length || files.length < plan.composeFiles.length || plan.composeFiles.some((file, index) => !sameWorkspacePath(file, files[index]))) {
2293
2420
  throw new Error("Managed stop Compose file identity changed.");
2294
2421
  }
2295
2422
  for (const file of files.slice(plan.composeFiles.length)) {
2296
- if (import_node_path11.default.dirname(file) !== featureDirectory || !/^docker-compose\.devcontainer\.containerFeatures-[a-zA-Z0-9_-]+\.yml$/.test(
2297
- import_node_path11.default.basename(file)
2298
- ) || import_node_fs9.default.realpathSync(file) !== import_node_path11.default.join(import_node_fs9.default.realpathSync(devsyRoot), import_node_path11.default.relative(devsyRoot, file))) {
2423
+ if (import_node_path12.default.dirname(file) !== featureDirectory || !/^docker-compose\.devcontainer\.containerFeatures-[a-zA-Z0-9_-]+\.yml$/.test(
2424
+ import_node_path12.default.basename(file)
2425
+ ) || import_node_fs10.default.realpathSync(file) !== import_node_path12.default.join(import_node_fs10.default.realpathSync(devsyRoot), import_node_path12.default.relative(devsyRoot, file))) {
2299
2426
  throw new Error("Managed stop refuses a foreign or escaped provider Compose file.");
2300
2427
  }
2301
2428
  }
@@ -2316,11 +2443,11 @@ function proveManagedComposePopulation(options) {
2316
2443
 
2317
2444
  // src/core/managed-runtime-state.ts
2318
2445
  var import_node_crypto6 = require("crypto");
2319
- var import_node_fs10 = __toESM(require("fs"));
2320
- var import_node_path13 = __toESM(require("path"));
2446
+ var import_node_fs11 = __toESM(require("fs"));
2447
+ var import_node_path14 = __toESM(require("path"));
2321
2448
 
2322
2449
  // src/core/managed-stop-baseline.ts
2323
- var import_node_path12 = __toESM(require("path"));
2450
+ var import_node_path13 = __toESM(require("path"));
2324
2451
  function record(value, keys2) {
2325
2452
  if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).length !== keys2.length || Object.keys(value).some((key) => !keys2.includes(key))) {
2326
2453
  throw new Error("Invalid managed stop baseline fields.");
@@ -2335,7 +2462,7 @@ function text(value, empty = false) {
2335
2462
  }
2336
2463
  function absolute(value) {
2337
2464
  const result = text(value);
2338
- if (!import_node_path12.default.isAbsolute(result) || import_node_path12.default.normalize(result) !== result) {
2465
+ if (!import_node_path13.default.isAbsolute(result) || import_node_path13.default.normalize(result) !== result) {
2339
2466
  throw new Error("Invalid managed stop baseline path.");
2340
2467
  }
2341
2468
  return result;
@@ -2413,8 +2540,8 @@ function validateManagedStopBaseline(value, state) {
2413
2540
  const c = record(entry, ["id", "service", "configFiles", "mounts"]);
2414
2541
  const configFiles = list(c.configFiles, absolute);
2415
2542
  if (configFiles.length < result.composeFiles.length || result.composeFiles.some((file, index) => configFiles[index] !== file) || configFiles.slice(result.composeFiles.length).some(
2416
- (file) => import_node_path12.default.dirname(file) !== result.featureDirectory || !/^docker-compose\.devcontainer\.containerFeatures-[a-zA-Z0-9_-]+\.yml$/.test(
2417
- import_node_path12.default.basename(file)
2543
+ (file) => import_node_path13.default.dirname(file) !== result.featureDirectory || !/^docker-compose\.devcontainer\.containerFeatures-[a-zA-Z0-9_-]+\.yml$/.test(
2544
+ import_node_path13.default.basename(file)
2418
2545
  )
2419
2546
  )) {
2420
2547
  throw new Error("Invalid stop baseline Compose provenance.");
@@ -2453,7 +2580,7 @@ function stateKey(repoPath, workspace) {
2453
2580
  return (0, import_node_crypto6.createHash)("sha256").update(`${repoPath}\0${workspace ?? ""}`, "utf-8").digest("hex");
2454
2581
  }
2455
2582
  function managedRuntimeStatePath(repoPath, workspace) {
2456
- return import_node_path13.default.join(DEVROUTER_HOME, "managed-runtime", `${stateKey(repoPath, workspace)}.json`);
2583
+ return import_node_path14.default.join(DEVROUTER_HOME, "managed-runtime", `${stateKey(repoPath, workspace)}.json`);
2457
2584
  }
2458
2585
  function isStringArray(value) {
2459
2586
  return Array.isArray(value) && value.every((item) => typeof item === "string") && new Set(value).size === value.length;
@@ -2518,9 +2645,9 @@ function validateState(value, repoPath, workspace) {
2518
2645
  }
2519
2646
  return result;
2520
2647
  }
2521
- function readManagedRuntimeState(repoPath, workspace, read = (file) => import_node_fs10.default.readFileSync(file, "utf-8")) {
2648
+ function readManagedRuntimeState(repoPath, workspace, read = (file) => import_node_fs11.default.readFileSync(file, "utf-8")) {
2522
2649
  const statePath = managedRuntimeStatePath(repoPath, workspace);
2523
- if (!import_node_fs10.default.existsSync(statePath)) return void 0;
2650
+ if (!import_node_fs11.default.existsSync(statePath)) return void 0;
2524
2651
  let parsed;
2525
2652
  try {
2526
2653
  parsed = JSON.parse(read(statePath));
@@ -2547,9 +2674,9 @@ function markManagedRuntimeDegraded(state, transitionPhase) {
2547
2674
  }
2548
2675
 
2549
2676
  // src/core/managed-stop-recovery.ts
2550
- var import_node_os2 = __toESM(require("os"));
2551
- var import_node_path15 = __toESM(require("path"));
2552
- var import_node_util = require("util");
2677
+ var import_node_os3 = __toESM(require("os"));
2678
+ var import_node_path16 = __toESM(require("path"));
2679
+ var import_node_util2 = require("util");
2553
2680
 
2554
2681
  // src/core/reliability-context.ts
2555
2682
  var claim;
@@ -2563,11 +2690,11 @@ function claimLifecycleEffect() {
2563
2690
 
2564
2691
  // src/core/workspace-ownership.ts
2565
2692
  var import_node_child_process11 = require("child_process");
2566
- var import_node_fs11 = __toESM(require("fs"));
2567
- var import_node_path14 = __toESM(require("path"));
2693
+ var import_node_fs12 = __toESM(require("fs"));
2694
+ var import_node_path15 = __toESM(require("path"));
2568
2695
  var READ_ONLY_GIT_ENV = { ...process.env, GIT_OPTIONAL_LOCKS: "0" };
2569
2696
  var OWNERSHIP_VERSION = 1;
2570
- var OWNERSHIP_DIR = import_node_path14.default.join("devrouter", "workspaces");
2697
+ var OWNERSHIP_DIR = import_node_path15.default.join("devrouter", "workspaces");
2571
2698
  function commandError(command, repoPath, stderr) {
2572
2699
  return new Error(
2573
2700
  `${command} failed for '${repoPath}': ${stderr?.trim() || "not a Git repository"}`
@@ -2582,7 +2709,7 @@ function resolveGitCommonDir(repoPath) {
2582
2709
  if (result.status !== 0 || !output) {
2583
2710
  throw commandError("Could not resolve the Git common directory", repoPath, result.stderr);
2584
2711
  }
2585
- return comparableWorkspacePath(import_node_path14.default.isAbsolute(output) ? output : import_node_path14.default.resolve(repoPath, output));
2712
+ return comparableWorkspacePath(import_node_path15.default.isAbsolute(output) ? output : import_node_path15.default.resolve(repoPath, output));
2586
2713
  }
2587
2714
  function listGitWorktrees(repoPath) {
2588
2715
  const result = (0, import_node_child_process11.spawnSync)("git", ["-C", repoPath, "worktree", "list", "--porcelain"], {
@@ -2622,7 +2749,7 @@ function listGitWorktrees(repoPath) {
2622
2749
  return worktrees;
2623
2750
  }
2624
2751
  function ownershipDirectory(repoPath) {
2625
- return import_node_path14.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
2752
+ return import_node_path15.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
2626
2753
  }
2627
2754
  function validateWorkspace(value, label) {
2628
2755
  if (typeof value !== "string" || wsFromBranch(value) !== value) {
@@ -2650,7 +2777,7 @@ function validateRecord(value, expectedWorkspace) {
2650
2777
  `workspace ownership file '${expectedWorkspace}' contains identity '${workspace}'`
2651
2778
  );
2652
2779
  }
2653
- if (typeof candidate.worktreePath !== "string" || !import_node_path14.default.isAbsolute(candidate.worktreePath)) {
2780
+ if (typeof candidate.worktreePath !== "string" || !import_node_path15.default.isAbsolute(candidate.worktreePath)) {
2654
2781
  throw new Error("invalid workspace ownership worktreePath");
2655
2782
  }
2656
2783
  if (candidate.branch !== null && typeof candidate.branch !== "string") {
@@ -2668,7 +2795,7 @@ function validateRecord(value, expectedWorkspace) {
2668
2795
  };
2669
2796
  }
2670
2797
  function recordPath(repoPath, workspace) {
2671
- return import_node_path14.default.join(
2798
+ return import_node_path15.default.join(
2672
2799
  ownershipDirectory(repoPath),
2673
2800
  `${validateWorkspace(workspace, "workspace")}.json`
2674
2801
  );
@@ -2676,7 +2803,7 @@ function recordPath(repoPath, workspace) {
2676
2803
  function readRecordFile(filePath, expectedWorkspace) {
2677
2804
  let parsed;
2678
2805
  try {
2679
- parsed = JSON.parse(import_node_fs11.default.readFileSync(filePath, "utf-8"));
2806
+ parsed = JSON.parse(import_node_fs12.default.readFileSync(filePath, "utf-8"));
2680
2807
  } catch (error) {
2681
2808
  if (error instanceof SyntaxError) {
2682
2809
  throw new Error(`invalid workspace ownership JSON at '${filePath}'`);
@@ -2701,7 +2828,7 @@ function listWorkspaceOwnership(repoPath) {
2701
2828
  function listWorkspaceOwnershipInDirectory(directory) {
2702
2829
  let entries;
2703
2830
  try {
2704
- entries = import_node_fs11.default.readdirSync(directory, { withFileTypes: true });
2831
+ entries = import_node_fs12.default.readdirSync(directory, { withFileTypes: true });
2705
2832
  } catch (error) {
2706
2833
  if (error.code === "ENOENT") return [];
2707
2834
  throw error;
@@ -2709,14 +2836,14 @@ function listWorkspaceOwnershipInDirectory(directory) {
2709
2836
  return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
2710
2837
  const workspace = entry.name.slice(0, -".json".length);
2711
2838
  validateWorkspace(workspace, "filename");
2712
- return readRecordFile(import_node_path14.default.join(directory, entry.name), workspace);
2839
+ return readRecordFile(import_node_path15.default.join(directory, entry.name), workspace);
2713
2840
  });
2714
2841
  }
2715
2842
  function writeWorkspaceOwnershipInDirectory(directory, input) {
2716
2843
  const workspace = validateWorkspace(input.workspace, "workspace");
2717
2844
  const devpodId = validateWorkspace(input.devpodId, "devpodId");
2718
2845
  const worktreePath = comparableWorkspacePath(input.worktreePath);
2719
- const filePath = import_node_path14.default.join(directory, `${workspace}.json`);
2846
+ const filePath = import_node_path15.default.join(directory, `${workspace}.json`);
2720
2847
  const now = (/* @__PURE__ */ new Date()).toISOString();
2721
2848
  const records = listWorkspaceOwnershipInDirectory(directory);
2722
2849
  const existing = records.find((record5) => record5.workspace === workspace);
@@ -2752,9 +2879,9 @@ function writeWorkspaceOwnershipInDirectory(directory, input) {
2752
2879
  return record4;
2753
2880
  }
2754
2881
  function removeWorkspaceOwnershipInDirectory(directory, workspace) {
2755
- const filePath = import_node_path14.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
2882
+ const filePath = import_node_path15.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
2756
2883
  try {
2757
- import_node_fs11.default.rmSync(filePath);
2884
+ import_node_fs12.default.rmSync(filePath);
2758
2885
  return true;
2759
2886
  } catch (error) {
2760
2887
  if (error.code === "ENOENT") return false;
@@ -2765,7 +2892,7 @@ function sameOwnershipRecord(left, right) {
2765
2892
  return left.version === right.version && left.workspace === right.workspace && sameWorkspacePath(left.worktreePath, right.worktreePath) && left.branch === right.branch && left.devpodId === right.devpodId && left.createdAt === right.createdAt && left.updatedAt === right.updatedAt;
2766
2893
  }
2767
2894
  function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
2768
- const filePath = import_node_path14.default.join(
2895
+ const filePath = import_node_path15.default.join(
2769
2896
  directory,
2770
2897
  `${validateWorkspace(expected.workspace, "workspace")}.json`
2771
2898
  );
@@ -2777,14 +2904,14 @@ function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
2777
2904
  throw error;
2778
2905
  }
2779
2906
  if (!sameOwnershipRecord(current, expected)) return "changed";
2780
- import_node_fs11.default.rmSync(filePath);
2907
+ import_node_fs12.default.rmSync(filePath);
2781
2908
  return "removed";
2782
2909
  }
2783
2910
  function withWorkspaceOwnershipTransaction(repoPath, operation, options = {}) {
2784
2911
  const directory = ownershipDirectory(repoPath);
2785
- import_node_fs11.default.mkdirSync(directory, { recursive: true });
2912
+ import_node_fs12.default.mkdirSync(directory, { recursive: true });
2786
2913
  return withFileLockSync(
2787
- import_node_path14.default.join(directory, ".lock"),
2914
+ import_node_path15.default.join(directory, ".lock"),
2788
2915
  {
2789
2916
  activity: "workspace ownership transaction",
2790
2917
  target: `'${repoPath}'`,
@@ -2817,7 +2944,7 @@ function persistedWorkspaceOwners(repoPath, worktreePath) {
2817
2944
  try {
2818
2945
  workspace = readPersistedWorkspace(worktree.path);
2819
2946
  } catch (error) {
2820
- if (!import_node_fs11.default.existsSync(worktree.path)) continue;
2947
+ if (!import_node_fs12.default.existsSync(worktree.path)) continue;
2821
2948
  throw error;
2822
2949
  }
2823
2950
  if (!workspace) continue;
@@ -2974,7 +3101,7 @@ function inspectWorkspaceOwnership(record4, worktrees, devpods) {
2974
3101
  if (worktree?.locked) {
2975
3102
  return { ownerStatus: "locked", devpodStatus, worktree };
2976
3103
  }
2977
- if ((!worktree || worktree.prunable) && import_node_fs11.default.existsSync(record4.worktreePath)) {
3104
+ if ((!worktree || worktree.prunable) && import_node_fs12.default.existsSync(record4.worktreePath)) {
2978
3105
  return { ownerStatus: "conflict", devpodStatus, worktree };
2979
3106
  }
2980
3107
  if (!worktree || worktree.prunable) {
@@ -3052,7 +3179,7 @@ function prove(state, baseline) {
3052
3179
  const containers = inspectManagedStopContainers(baseline.project, baseline.endpoint);
3053
3180
  const expected = baseline.containers.map((container) => ({ ...container, mounts: orderedMounts(container.mounts) })).sort((a, b) => compareText(a.id, b.id));
3054
3181
  const observed = containers.map(identity).sort((a, b) => compareText(a.id, b.id));
3055
- if (!(0, import_node_util.isDeepStrictEqual)(expected, observed) || containers.some(
3182
+ if (!(0, import_node_util2.isDeepStrictEqual)(expected, observed) || containers.some(
3056
3183
  (container) => container.labels["com.docker.compose.project.working_dir"] !== baseline.composeDirectory
3057
3184
  )) {
3058
3185
  throw new Error("Retained container population or immutable identity changed.");
@@ -3061,7 +3188,7 @@ function prove(state, baseline) {
3061
3188
  baseline.endpoint,
3062
3189
  baseline.composeDirectory
3063
3190
  ).sort();
3064
- if (!(0, import_node_util.isDeepStrictEqual)(workspaceIds, expected.map((container) => container.id).sort())) {
3191
+ if (!(0, import_node_util2.isDeepStrictEqual)(workspaceIds, expected.map((container) => container.id).sort())) {
3065
3192
  throw new Error("Workspace contains an unrecorded container population.");
3066
3193
  }
3067
3194
  const primary = baseline.containers.find(
@@ -3073,7 +3200,7 @@ function prove(state, baseline) {
3073
3200
  if (!baseline.sourceContainer && inspectManagedStopRunnerId(baseline.endpoint, primary.id) !== runnerId) {
3074
3201
  throw new Error("Provider-to-container binding changed.");
3075
3202
  }
3076
- if (!(0, import_node_util.isDeepStrictEqual)(registration(state), owner) || inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId) {
3203
+ if (!(0, import_node_util2.isDeepStrictEqual)(registration(state), owner) || inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId) {
3077
3204
  throw new Error("Stop authority changed during inspection.");
3078
3205
  }
3079
3206
  return containers;
@@ -3082,8 +3209,8 @@ function captureManagedStopBaseline(state, plan, primaryId, expectedEndpoint) {
3082
3209
  const owner = registration(state);
3083
3210
  if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(owner.context) || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(owner.providerId))
3084
3211
  throw new Error("Invalid capture provider identity.");
3085
- const devsyRoot = import_node_path15.default.resolve(process.env.DEVSY_HOME || import_node_path15.default.join(import_node_os2.default.homedir(), ".devsy"));
3086
- const featureDirectory = import_node_path15.default.join(
3212
+ const devsyRoot = import_node_path16.default.resolve(process.env.DEVSY_HOME || import_node_path16.default.join(import_node_os3.default.homedir(), ".devsy"));
3213
+ const featureDirectory = import_node_path16.default.join(
3087
3214
  devsyRoot,
3088
3215
  "contexts",
3089
3216
  owner.context,
@@ -3133,16 +3260,65 @@ function captureManagedStopBaseline(state, plan, primaryId, expectedEndpoint) {
3133
3260
  }
3134
3261
  function proveRetainedManagedStop(state) {
3135
3262
  const baseline = validateManagedStopBaseline(state.stopBaseline, state);
3136
- if (!(0, import_node_util.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state)) {
3263
+ if (!(0, import_node_util2.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state)) {
3137
3264
  throw new Error("Retained stop generation changed.");
3138
3265
  }
3139
3266
  const result = prove(state, baseline);
3140
- if (!(0, import_node_util.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state)) {
3267
+ if (!(0, import_node_util2.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state)) {
3141
3268
  throw new Error("Retained stop generation changed during inspection.");
3142
3269
  }
3143
3270
  return result;
3144
3271
  }
3272
+ function absentRegistrationIdentity(state) {
3273
+ resetWorkspaceRuntimeCaches();
3274
+ if (resolveWorkspaceRuntimeOrDefault(state.repoPath) !== "devsy")
3275
+ throw new Error("Stop provider changed.");
3276
+ if (!state.workspace || !isLinkedWorktree(state.repoPath))
3277
+ throw new Error("Absent stop requires a linked workspace.");
3278
+ const record4 = readWorkspaceOwnership(state.repoPath, state.workspace);
3279
+ 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")
3280
+ throw new Error("Absent stop workspace ownership changed.");
3281
+ for (const entries of [
3282
+ listDevsyWorkspaces(),
3283
+ listDevpodWorkspacesRaw({ allowMissingExecutable: true })
3284
+ ]) {
3285
+ if (entries.some(
3286
+ (entry) => entry.id === state.devpodId || sameWorkspacePath(entry.source.localFolder, state.repoPath)
3287
+ ))
3288
+ throw new Error("Absent stop requires both provider registrations to remain absent.");
3289
+ }
3290
+ return { record: record4, gitCommonDir: resolveGitCommonDir(state.repoPath) };
3291
+ }
3292
+ function proveManagedStop(state) {
3293
+ const baseline = validateManagedStopBaseline(state.stopBaseline, state);
3294
+ const owner = inspectDevsyWorkspaceOwnership(
3295
+ listDevsyWorkspaces(),
3296
+ state.devpodId,
3297
+ state.repoPath
3298
+ );
3299
+ if (owner.status !== "absent")
3300
+ return { status: "retained", containers: proveRetainedManagedStop(state) };
3301
+ if (baseline.sourceContainer) throw new Error("Absent stop cannot prove a source container.");
3302
+ const before = absentRegistrationIdentity(state);
3303
+ if (!(0, import_node_util2.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state))
3304
+ throw new Error("Retained stop generation changed.");
3305
+ if (inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId)
3306
+ throw new Error("Stop daemon changed.");
3307
+ assertManagedStopContainersAbsent(
3308
+ baseline.endpoint,
3309
+ baseline.containers.map((container) => container.id)
3310
+ );
3311
+ const uidBytes = Buffer.byteLength(baseline.uid);
3312
+ const runnerId = uidBytes === 16 || uidBytes === 40 ? baseline.uid : baseline.providerId;
3313
+ if (inspectManagedStopContainers(baseline.project, baseline.endpoint).length !== 0 || inspectManagedStopWorkspaceIds(baseline.endpoint, baseline.composeDirectory).length !== 0 || inspectProviderRunnerContainers(baseline.endpoint, runnerId).length !== 0)
3314
+ throw new Error("Absent stop observed a remaining workspace population.");
3315
+ 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))
3316
+ throw new Error("Absent stop authority changed during inspection.");
3317
+ return { status: "proven-absent", containers: [] };
3318
+ }
3145
3319
  function stopFromManagedBaseline(state) {
3320
+ const proof = proveManagedStop(state);
3321
+ if (proof.status === "proven-absent") return proof.status;
3146
3322
  const baseline = validateManagedStopBaseline(state.stopBaseline, state);
3147
3323
  const stopped = /* @__PURE__ */ new Set();
3148
3324
  for (const initial of proveRetainedManagedStop(state))
@@ -3165,11 +3341,17 @@ function stopFromManagedBaseline(state) {
3165
3341
  }
3166
3342
  if (inspect().some((container) => container.state.Running))
3167
3343
  throw new Error("Retained workloads remain running.");
3344
+ return "retained";
3345
+ }
3346
+ function managedStopRouteReferences(state) {
3347
+ return state.desired.apps.flatMap(
3348
+ (name) => ["http", "tcp"].map((protocol) => ({ repoPath: state.repoPath, name, protocol }))
3349
+ );
3168
3350
  }
3169
3351
 
3170
3352
  // src/core/repo-config.ts
3171
- var import_node_fs13 = __toESM(require("fs"));
3172
- var import_node_path17 = __toESM(require("path"));
3353
+ var import_node_fs14 = __toESM(require("fs"));
3354
+ var import_node_path18 = __toESM(require("path"));
3173
3355
  var import_yaml3 = __toESM(require("yaml"));
3174
3356
 
3175
3357
  // src/core/capabilities.ts
@@ -3195,9 +3377,9 @@ function formatSupportedTcpProtocols() {
3195
3377
  }
3196
3378
 
3197
3379
  // src/core/host-routes.ts
3198
- var import_node_fs12 = __toESM(require("fs"));
3199
- var import_node_path16 = __toESM(require("path"));
3200
- var import_node_util2 = require("util");
3380
+ var import_node_fs13 = __toESM(require("fs"));
3381
+ var import_node_path17 = __toESM(require("path"));
3382
+ var import_node_util3 = require("util");
3201
3383
  var import_yaml2 = __toESM(require("yaml"));
3202
3384
  var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "0.0.0.0"]);
3203
3385
  var UPSTREAM_RE = /^([a-zA-Z0-9._-]+):(\d{1,5})$/;
@@ -3380,7 +3562,7 @@ function writeRouteGeneration(routes, tlsEnabled) {
3380
3562
  writeFileAtomically(HOST_ROUTES_STATE_FILE, renderCompatibilityState(routes));
3381
3563
  writeFileAtomically(TRAEFIK_HOST_ROUTES_FILE, renderCanonicalState(routes, tlsEnabled));
3382
3564
  }
3383
- function readCompatibilityState(read = (file) => import_node_fs12.default.readFileSync(file, "utf-8")) {
3565
+ function readCompatibilityState(read = (file) => import_node_fs13.default.readFileSync(file, "utf-8")) {
3384
3566
  let raw;
3385
3567
  try {
3386
3568
  raw = read(HOST_ROUTES_STATE_FILE);
@@ -3437,14 +3619,14 @@ function parseCanonicalState(raw) {
3437
3619
  throw new Error(`Canonical host-route document is invalid: ${error.message}`);
3438
3620
  }
3439
3621
  const expectedDocument = buildHostRoutesDocument(routes, metadata.tlsEnabled);
3440
- if (!(0, import_node_util2.isDeepStrictEqual)(document, expectedDocument)) {
3622
+ if (!(0, import_node_util3.isDeepStrictEqual)(document, expectedDocument)) {
3441
3623
  throw new Error("Canonical host-route document does not match its metadata.");
3442
3624
  }
3443
3625
  return { kind: "canonical", metadata };
3444
3626
  }
3445
3627
  function ensureHostRouteStorage() {
3446
- import_node_fs12.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
3447
- import_node_fs12.default.mkdirSync(TRAEFIK_DYNAMIC_DIR, { recursive: true });
3628
+ import_node_fs13.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
3629
+ import_node_fs13.default.mkdirSync(TRAEFIK_DYNAMIC_DIR, { recursive: true });
3448
3630
  }
3449
3631
  function writeState(routes) {
3450
3632
  ensureHostRouteStorage();
@@ -3463,8 +3645,8 @@ function refreshHostRoutesDynamicFile() {
3463
3645
  }
3464
3646
  function readHostRouteStateLocked() {
3465
3647
  ensureHostRouteStorage();
3466
- if (!import_node_fs12.default.existsSync(TRAEFIK_HOST_ROUTES_FILE)) {
3467
- if (import_node_fs12.default.existsSync(HOST_ROUTES_STATE_FILE)) {
3648
+ if (!import_node_fs13.default.existsSync(TRAEFIK_HOST_ROUTES_FILE)) {
3649
+ if (import_node_fs13.default.existsSync(HOST_ROUTES_STATE_FILE)) {
3468
3650
  const routes = readCompatibilityState();
3469
3651
  writeRouteGeneration(routes, isTLSEnabled());
3470
3652
  return routes;
@@ -3472,10 +3654,10 @@ function readHostRouteStateLocked() {
3472
3654
  writeRouteGeneration([], isTLSEnabled());
3473
3655
  return [];
3474
3656
  }
3475
- const raw = import_node_fs12.default.readFileSync(TRAEFIK_HOST_ROUTES_FILE, "utf-8");
3657
+ const raw = import_node_fs13.default.readFileSync(TRAEFIK_HOST_ROUTES_FILE, "utf-8");
3476
3658
  const canonical = parseCanonicalState(raw);
3477
3659
  if (canonical.kind === "legacy") {
3478
- if (!import_node_fs12.default.existsSync(HOST_ROUTES_STATE_FILE)) {
3660
+ if (!import_node_fs13.default.existsSync(HOST_ROUTES_STATE_FILE)) {
3479
3661
  throw new Error(
3480
3662
  "Headerless host-route document requires a valid compatibility state file for migration."
3481
3663
  );
@@ -3485,9 +3667,9 @@ function readHostRouteStateLocked() {
3485
3667
  return routes;
3486
3668
  }
3487
3669
  let mirrorMatches = false;
3488
- if (import_node_fs12.default.existsSync(HOST_ROUTES_STATE_FILE)) {
3670
+ if (import_node_fs13.default.existsSync(HOST_ROUTES_STATE_FILE)) {
3489
3671
  try {
3490
- mirrorMatches = (0, import_node_util2.isDeepStrictEqual)(readCompatibilityState(), canonical.metadata.routes);
3672
+ mirrorMatches = (0, import_node_util3.isDeepStrictEqual)(readCompatibilityState(), canonical.metadata.routes);
3491
3673
  } catch {
3492
3674
  mirrorMatches = false;
3493
3675
  }
@@ -4505,15 +4687,15 @@ function applyProfile(config, profile) {
4505
4687
  return next;
4506
4688
  }
4507
4689
  function resolveRepoPath(repoPath) {
4508
- return import_node_path17.default.resolve(repoPath ?? process.cwd());
4690
+ return import_node_path18.default.resolve(repoPath ?? process.cwd());
4509
4691
  }
4510
4692
  function getRepoConfigPath(repoPath) {
4511
- return import_node_path17.default.join(resolveRepoPath(repoPath), CONFIG_FILE_NAME);
4693
+ return import_node_path18.default.join(resolveRepoPath(repoPath), CONFIG_FILE_NAME);
4512
4694
  }
4513
- function loadRepoConfig(repoPath, read = (file) => import_node_fs13.default.readFileSync(file, "utf-8")) {
4695
+ function loadRepoConfig(repoPath, read = (file) => import_node_fs14.default.readFileSync(file, "utf-8")) {
4514
4696
  const resolvedRepoPath = resolveRepoPath(repoPath);
4515
4697
  const configPath = getRepoConfigPath(resolvedRepoPath);
4516
- if (!import_node_fs13.default.existsSync(configPath)) {
4698
+ if (!import_node_fs14.default.existsSync(configPath)) {
4517
4699
  throw new Error(
4518
4700
  `Missing ${CONFIG_FILE_NAME} in ${resolvedRepoPath}. Run 'dev repo init --repo ${resolvedRepoPath}' first.`
4519
4701
  );
@@ -4523,7 +4705,7 @@ function loadRepoConfig(repoPath, read = (file) => import_node_fs13.default.read
4523
4705
  const config = parseConfig(parsed ?? {}, configPath);
4524
4706
  const requiredVersion = config.devrouter?.version;
4525
4707
  if (requiredVersion && !hasWarnedVersionMismatch) {
4526
- const cliVersion = true ? "0.0.61" : "0.0.0-dev";
4708
+ const cliVersion = true ? "0.0.63" : "0.0.0-dev";
4527
4709
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
4528
4710
  hasWarnedVersionMismatch = true;
4529
4711
  process.stderr.write(
@@ -4543,7 +4725,7 @@ function namespaceHost(host, workspace) {
4543
4725
  return `${base}.${workspace}${suffix}`;
4544
4726
  }
4545
4727
  function applyWorkspace(config, workspace, repoPath) {
4546
- const defaultToken = wsFromBranch(config.project?.name ?? import_node_path17.default.basename(import_node_path17.default.resolve(repoPath))) ?? "app";
4728
+ const defaultToken = wsFromBranch(config.project?.name ?? import_node_path18.default.basename(import_node_path18.default.resolve(repoPath))) ?? "app";
4547
4729
  const substitutionToken = workspace ?? defaultToken;
4548
4730
  const next = structuredClone(config);
4549
4731
  for (const app of next.apps) {
@@ -4704,10 +4886,10 @@ async function ensureNetwork(name) {
4704
4886
  // src/core/tls.ts
4705
4887
  var import_node_child_process13 = require("child_process");
4706
4888
  var import_node_crypto7 = require("crypto");
4707
- var import_node_fs14 = __toESM(require("fs"));
4708
- var import_node_path18 = __toESM(require("path"));
4889
+ var import_node_fs15 = __toESM(require("fs"));
4890
+ var import_node_path19 = __toESM(require("path"));
4709
4891
  var DEFAULT_TLS_CERT_HOSTS = ["localhost", "*.localhost"];
4710
- var TLS_CERTIFICATE_LOCK_FILE = import_node_path18.default.join(DEVROUTER_HOME, "tls-certificate.lock");
4892
+ var TLS_CERTIFICATE_LOCK_FILE = import_node_path19.default.join(DEVROUTER_HOME, "tls-certificate.lock");
4711
4893
  var TLS_CERTIFICATE_LOCK_WAIT_MS = 6e4;
4712
4894
  var TLS_CERTIFICATE_WRITE_ATTEMPTS = 2;
4713
4895
  function runOrThrow(command, args) {
@@ -4739,8 +4921,8 @@ function tlsSetupCommand(repoPath) {
4739
4921
  }
4740
4922
  function getMkcertRootCAPath(options = {}) {
4741
4923
  ensureMkcert();
4742
- const rootCAPath = import_node_path18.default.join(runOrThrow("mkcert", ["-CAROOT"]), "rootCA.pem");
4743
- if (!import_node_fs14.default.existsSync(rootCAPath)) {
4924
+ const rootCAPath = import_node_path19.default.join(runOrThrow("mkcert", ["-CAROOT"]), "rootCA.pem");
4925
+ if (!import_node_fs15.default.existsSync(rootCAPath)) {
4744
4926
  throw new Error(
4745
4927
  `mkcert root CA was not found at '${rootCAPath}'. Run: ${tlsSetupCommand(options.repoPath)}`
4746
4928
  );
@@ -4841,11 +5023,11 @@ function compactTLSCertificateHosts(hosts) {
4841
5023
  );
4842
5024
  }
4843
5025
  function readCurrentCertificateHosts(options = {}) {
4844
- if (!import_node_fs14.default.existsSync(CERT_FILE)) {
5026
+ if (!import_node_fs15.default.existsSync(CERT_FILE)) {
4845
5027
  return [];
4846
5028
  }
4847
5029
  try {
4848
- const pem = import_node_fs14.default.readFileSync(CERT_FILE, "utf-8");
5030
+ const pem = import_node_fs15.default.readFileSync(CERT_FILE, "utf-8");
4849
5031
  return parseCertificateDnsHosts(pem);
4850
5032
  } catch (error) {
4851
5033
  if (options.replaceMalformed) {
@@ -5059,8 +5241,7 @@ function stopRetainedManagedDevsyWorkspace(options) {
5059
5241
  throw new Error("Managed stop requires the exact retained workspace identity.");
5060
5242
  }
5061
5243
  if (state.stopBaseline) {
5062
- stopFromManagedBaseline(state);
5063
- return true;
5244
+ return stopFromManagedBaseline(state) === "proven-absent" ? "proven-absent" : true;
5064
5245
  }
5065
5246
  const workspaceEnv = workspace ? { token: workspace, gitCommonDir: resolveGitCommonDir(repoPath) } : void 0;
5066
5247
  function registration2() {
@@ -5084,8 +5265,8 @@ function stopRetainedManagedDevsyWorkspace(options) {
5084
5265
  if (!context || !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(context) || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(devsyId)) {
5085
5266
  throw new Error("Managed stop requires a valid exact Devsy context and workspace ID.");
5086
5267
  }
5087
- const devsyRoot = import_node_path19.default.resolve(process.env.DEVSY_HOME || import_node_path19.default.join(import_node_os3.default.homedir(), ".devsy"));
5088
- const featureDirectory = import_node_path19.default.join(
5268
+ const devsyRoot = import_node_path20.default.resolve(process.env.DEVSY_HOME || import_node_path20.default.join(import_node_os4.default.homedir(), ".devsy"));
5269
+ const featureDirectory = import_node_path20.default.join(
5089
5270
  devsyRoot,
5090
5271
  "contexts",
5091
5272
  context,
@@ -5191,7 +5372,7 @@ function stopRetainedManagedDevsyWorkspace(options) {
5191
5372
  }
5192
5373
 
5193
5374
  // src/core/devsy-mutation.ts
5194
- var DEVSY_MUTATION_LOCK_FILE = import_node_path20.default.join(DEVROUTER_HOME, "devsy-mutation.lock");
5375
+ var DEVSY_MUTATION_LOCK_FILE = import_node_path21.default.join(DEVROUTER_HOME, "devsy-mutation.lock");
5195
5376
  var DEVSY_MUTATION_WAIT_MS = 18e5;
5196
5377
  var DevsyStartPostconditionError = class extends Error {
5197
5378
  };
@@ -5206,7 +5387,7 @@ function failedStartMayHaveAttached(devsyId, repoPath) {
5206
5387
  }
5207
5388
  }
5208
5389
  function withMutationLock(activity, target, operation) {
5209
- import_node_fs15.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
5390
+ import_node_fs16.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
5210
5391
  return withFileLockSync(
5211
5392
  DEVSY_MUTATION_LOCK_FILE,
5212
5393
  {
@@ -5220,7 +5401,7 @@ function withMutationLock(activity, target, operation) {
5220
5401
  );
5221
5402
  }
5222
5403
  function withMutationLockAsync(activity, target, operation) {
5223
- import_node_fs15.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
5404
+ import_node_fs16.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
5224
5405
  return withFileLock(
5225
5406
  DEVSY_MUTATION_LOCK_FILE,
5226
5407
  {
@@ -5288,12 +5469,14 @@ function inspectExactOwnership(devsyId, worktreePath) {
5288
5469
  }
5289
5470
  function mutateOwnedDevsyWorkspace(action, devsyId, worktreePath) {
5290
5471
  return withMutationLock(`Devsy ${action}`, worktreePath, () => {
5291
- if (action === "stop" && stopRetainedManagedDevsyWorkspace({
5292
- repoPath: worktreePath,
5293
- devsyId,
5294
- stopProvider: () => runDevsyAction("stop", devsyId)
5295
- }))
5296
- return { status: "changed" };
5472
+ if (action === "stop") {
5473
+ const stopped = stopRetainedManagedDevsyWorkspace({
5474
+ repoPath: worktreePath,
5475
+ devsyId,
5476
+ stopProvider: () => runDevsyAction("stop", devsyId)
5477
+ });
5478
+ if (stopped) return { status: stopped === "proven-absent" ? "proven-absent" : "changed" };
5479
+ }
5297
5480
  const before = inspectExactOwnership(devsyId, worktreePath);
5298
5481
  if (before.status === "absent") return { status: "absent" };
5299
5482
  runDevsyAction(action, devsyId);
@@ -5424,6 +5607,19 @@ async function startDevsyWorkspace(options) {
5424
5607
  });
5425
5608
  }
5426
5609
 
5610
+ // src/core/execution-outcome.ts
5611
+ var ExecutionOutcomeError = class extends Error {
5612
+ outcome;
5613
+ constructor(message, outcome) {
5614
+ super(message);
5615
+ this.name = "ExecutionOutcomeError";
5616
+ this.outcome = outcome;
5617
+ }
5618
+ };
5619
+
5620
+ // src/core/reliability-lifecycle.ts
5621
+ var import_node_path28 = __toESM(require("path"));
5622
+
5427
5623
  // src/core/reliability-contract.ts
5428
5624
  var RELIABILITY_CONTRACT_VERSION = 2;
5429
5625
  var RELIABILITY_MAX_ITEMS = 128;
@@ -5553,7 +5749,7 @@ function assertReliabilityEvent(value) {
5553
5749
  valid = oneOf2(value.mode, ["start", "attach"]) && isReliabilityId(value.key) && isReliabilityId(value.operationId) && isReliabilityProfile(value.profile) && isConsumer(value.consumer);
5554
5750
  break;
5555
5751
  case "operation-request":
5556
- valid = oneOf2(value.kind, ["ensure", "exec"]) && isReliabilityId(value.key) && isReliabilityId(value.operationId) && isReliabilityProfile(value.profile) && isConsumer(value.consumer) && typeof value.runtimeRunning === "boolean";
5752
+ 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");
5557
5753
  break;
5558
5754
  case "drained":
5559
5755
  valid = isReliabilityId(value.operationId);
@@ -5736,6 +5932,10 @@ function handleRequest(state, event) {
5736
5932
  });
5737
5933
  return transition(state, "accepted");
5738
5934
  }
5935
+ function canExecAfterInterruptedEnsure(state) {
5936
+ const latestEnsure = [...state.operationHistory].reverse().find((operation) => operation.kind === "ensure");
5937
+ 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));
5938
+ }
5739
5939
  function handleOperationRequest(state, event) {
5740
5940
  if (state.executionPolicy !== "manual") return unchanged(state, "blocked");
5741
5941
  const previous = state.operationHistory.find((entry) => entry.key === event.key);
@@ -5746,9 +5946,11 @@ function handleOperationRequest(state, event) {
5746
5946
  );
5747
5947
  if (state.operationHistory.some((entry) => entry.id === event.operationId))
5748
5948
  return unchanged(state, "conflict");
5949
+ if (event.kind === "exec" && canExecAfterInterruptedEnsure(state) && event.recoverInterruptedEnsure !== true)
5950
+ return unchanged(state, "blocked");
5749
5951
  const fullyStopped = state.stopProof.workloadsStopped && state.stopProof.routesRemoved;
5750
5952
  const reconcileEnsure = event.kind === "ensure" && state.operation?.kind === "ensure" && ["NOT_STARTED", "INTERRUPTED"].includes(state.operation.status);
5751
- if (state.operation && (!state.operation.drained || !["COMPLETED", "NOT_LAUNCHED", "NOT_STARTED"].includes(state.operation.status) && !fullyStopped && !reconcileEnsure))
5953
+ 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))))
5752
5954
  return unchanged(state, "blocked");
5753
5955
  if (state.phase === "stopping" || state.desired === "parked-for-capacity")
5754
5956
  return unchanged(state, "blocked");
@@ -5759,8 +5961,9 @@ function handleOperationRequest(state, event) {
5759
5961
  if (rollover) {
5760
5962
  if (state.operation && (!state.operation.drained || !["COMPLETED", "NOT_LAUNCHED"].includes(state.operation.status)))
5761
5963
  return unchanged(state, "blocked");
5964
+ const latestEnsureId = [...state.operationHistory].reverse().find((entry) => entry.kind === "ensure")?.id;
5762
5965
  retired = state.operationHistory.findIndex(
5763
- (entry) => entry.id !== state.operation?.id && entry.drained && ["COMPLETED", "NOT_LAUNCHED"].includes(entry.status)
5966
+ (entry) => entry.id !== state.operation?.id && entry.id !== latestEnsureId && entry.drained && ["COMPLETED", "NOT_LAUNCHED"].includes(entry.status)
5764
5967
  );
5765
5968
  if (retired === -1) return unchanged(state, "blocked");
5766
5969
  }
@@ -6088,7 +6291,7 @@ function applyReliabilityEvent(state, event, nowMs) {
6088
6291
  case "not-started":
6089
6292
  if (next.executionPolicy !== "manual" || next.operation?.kind !== "exec")
6090
6293
  return unchanged(next, "blocked");
6091
- if (next.operation.id !== event.operationId || next.operation.status !== "RUNNING")
6294
+ if (next.operation.id !== event.operationId || !["RUNNING", "DISPATCH_RECORDED"].includes(next.operation.status))
6092
6295
  return unchanged(next, "stale");
6093
6296
  next.operation = { ...next.operation, status: "NOT_LAUNCHED", exitCode: null };
6094
6297
  return transition(next, "accepted");
@@ -6129,17 +6332,17 @@ function stepReliability(state, event, nowMs) {
6129
6332
 
6130
6333
  // src/core/reliability-operation-store.ts
6131
6334
  var import_node_crypto8 = require("crypto");
6132
- var import_node_fs16 = __toESM(require("fs"));
6133
- var import_node_path21 = __toESM(require("path"));
6335
+ var import_node_fs17 = __toESM(require("fs"));
6336
+ var import_node_path22 = __toESM(require("path"));
6134
6337
  var MAX_RECORD_BYTES = 1048576;
6135
6338
  function identityKey(identity2) {
6136
- if (!import_node_path21.default.isAbsolute(identity2.repoPath) || import_node_path21.default.resolve(identity2.repoPath) !== identity2.repoPath || identity2.repoPath.includes("\0") || identity2.workspace !== null && !isReliabilityId(identity2.workspace) || !["devpod", "devsy"].includes(identity2.provider)) {
6339
+ if (!import_node_path22.default.isAbsolute(identity2.repoPath) || import_node_path22.default.resolve(identity2.repoPath) !== identity2.repoPath || identity2.repoPath.includes("\0") || identity2.workspace !== null && !isReliabilityId(identity2.workspace) || !["devpod", "devsy"].includes(identity2.provider)) {
6137
6340
  throw new Error("Invalid reliability workspace identity.");
6138
6341
  }
6139
6342
  return (0, import_node_crypto8.createHash)("sha256").update(identity2.repoPath).digest("hex");
6140
6343
  }
6141
6344
  function reliabilityOperationPath(identity2) {
6142
- return import_node_path21.default.join(DEVROUTER_HOME, "reliability", `${identityKey(identity2)}.json`);
6345
+ return import_node_path22.default.join(DEVROUTER_HOME, "reliability", `${identityKey(identity2)}.json`);
6143
6346
  }
6144
6347
  function keys(value, expected) {
6145
6348
  if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !expected.includes(key))) {
@@ -6218,21 +6421,21 @@ function readReliabilityOperation(identity2) {
6218
6421
  const file = reliabilityOperationPath(identity2);
6219
6422
  let descriptor;
6220
6423
  try {
6221
- descriptor = import_node_fs16.default.openSync(
6424
+ descriptor = import_node_fs17.default.openSync(
6222
6425
  file,
6223
- import_node_fs16.default.constants.O_RDONLY | import_node_fs16.default.constants.O_NOFOLLOW | import_node_fs16.default.constants.O_NONBLOCK
6426
+ import_node_fs17.default.constants.O_RDONLY | import_node_fs17.default.constants.O_NOFOLLOW | import_node_fs17.default.constants.O_NONBLOCK
6224
6427
  );
6225
6428
  } catch (error) {
6226
6429
  if (error.code === "ENOENT") return void 0;
6227
6430
  throw error;
6228
6431
  }
6229
6432
  try {
6230
- const stat = import_node_fs16.default.fstatSync(descriptor);
6433
+ const stat = import_node_fs17.default.fstatSync(descriptor);
6231
6434
  if (!stat.isFile() || stat.size > MAX_RECORD_BYTES || (stat.mode & 63) !== 0) {
6232
6435
  throw new Error("Reliability record is not a bounded private file.");
6233
6436
  }
6234
6437
  const bytes = Buffer.alloc(MAX_RECORD_BYTES + 1);
6235
- const count = import_node_fs16.default.readSync(descriptor, bytes, 0, bytes.length, 0);
6438
+ const count = import_node_fs17.default.readSync(descriptor, bytes, 0, bytes.length, 0);
6236
6439
  if (count > MAX_RECORD_BYTES) throw new Error("Reliability record exceeds its byte limit.");
6237
6440
  const record4 = JSON.parse(
6238
6441
  bytes.subarray(0, count).toString("utf8")
@@ -6240,7 +6443,7 @@ function readReliabilityOperation(identity2) {
6240
6443
  validate(record4, identity2);
6241
6444
  return record4;
6242
6445
  } finally {
6243
- import_node_fs16.default.closeSync(descriptor);
6446
+ import_node_fs17.default.closeSync(descriptor);
6244
6447
  }
6245
6448
  }
6246
6449
  function persist(record4) {
@@ -6255,23 +6458,23 @@ function persist(record4) {
6255
6458
  } catch (error) {
6256
6459
  const reread = readReliabilityOperation(record4.identity);
6257
6460
  if (!reread || JSON.stringify(reread) !== JSON.stringify(record4)) throw error;
6258
- const descriptor = import_node_fs16.default.openSync(file, import_node_fs16.default.constants.O_RDONLY | import_node_fs16.default.constants.O_NOFOLLOW);
6461
+ const descriptor = import_node_fs17.default.openSync(file, import_node_fs17.default.constants.O_RDONLY | import_node_fs17.default.constants.O_NOFOLLOW);
6259
6462
  try {
6260
- import_node_fs16.default.fsyncSync(descriptor);
6463
+ import_node_fs17.default.fsyncSync(descriptor);
6261
6464
  } finally {
6262
- import_node_fs16.default.closeSync(descriptor);
6465
+ import_node_fs17.default.closeSync(descriptor);
6263
6466
  }
6264
- const directory = import_node_fs16.default.openSync(import_node_path21.default.dirname(file), "r");
6467
+ const directory = import_node_fs17.default.openSync(import_node_path22.default.dirname(file), "r");
6265
6468
  try {
6266
- import_node_fs16.default.fsyncSync(directory);
6469
+ import_node_fs17.default.fsyncSync(directory);
6267
6470
  } finally {
6268
- import_node_fs16.default.closeSync(directory);
6471
+ import_node_fs17.default.closeSync(directory);
6269
6472
  }
6270
6473
  }
6271
6474
  }
6272
6475
  function updateReliabilityOperation(identity2, operation) {
6273
6476
  const file = reliabilityOperationPath(identity2);
6274
- import_node_fs16.default.mkdirSync(import_node_path21.default.dirname(file), { recursive: true, mode: 448 });
6477
+ import_node_fs17.default.mkdirSync(import_node_path22.default.dirname(file), { recursive: true, mode: 448 });
6275
6478
  return withFileLockSync(`${file}.lock`, { activity: "reliability intent", waitMs: 2e3 }, () => {
6276
6479
  const record4 = readReliabilityOperation(identity2) ?? {
6277
6480
  version: 1,
@@ -6298,8 +6501,8 @@ function updateReliabilityOperation(identity2, operation) {
6298
6501
  // src/core/reliability-worker.ts
6299
6502
  var import_node_child_process15 = require("child_process");
6300
6503
  var import_node_crypto9 = require("crypto");
6301
- var import_node_fs17 = __toESM(require("fs"));
6302
- var import_node_path22 = __toESM(require("path"));
6504
+ var import_node_fs18 = __toESM(require("fs"));
6505
+ var import_node_path23 = __toESM(require("path"));
6303
6506
  function workerGroupAbsent(pid) {
6304
6507
  try {
6305
6508
  process.kill(-pid, 0);
@@ -6309,53 +6512,377 @@ function workerGroupAbsent(pid) {
6309
6512
  }
6310
6513
  }
6311
6514
 
6312
- // src/core/workspace-ensure.ts
6313
- var import_node_child_process21 = require("child_process");
6314
- var import_node_fs22 = __toESM(require("fs"));
6315
- var import_node_path26 = __toESM(require("path"));
6316
-
6317
- // src/core/devpod-mutation.ts
6515
+ // src/core/traefik-route-health.ts
6318
6516
  var import_node_child_process16 = require("child_process");
6319
- var import_node_fs18 = __toESM(require("fs"));
6320
- var import_node_path23 = __toESM(require("path"));
6321
- var DEVPOD_MUTATION_LOCK_FILE = import_node_path23.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
6322
- var DEVPOD_MUTATION_WAIT_MS = 18e5;
6323
- var DevpodStartPostconditionError = class extends Error {
6324
- };
6325
- function withMutationLock2(activity, target, operation) {
6326
- import_node_fs18.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
6327
- return withFileLockSync(
6328
- DEVPOD_MUTATION_LOCK_FILE,
6329
- {
6330
- activity,
6331
- target: `'${target}'`,
6332
- waitMs: DEVPOD_MUTATION_WAIT_MS,
6333
- fair: true,
6334
- onWait: createStderrWaitReporter(activity, `'${target}'`)
6335
- },
6336
- operation
6337
- );
6338
- }
6339
- function commandFailure2(result) {
6340
- return [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
6517
+ var import_node_fs19 = __toESM(require("fs"));
6518
+ var import_node_path24 = __toESM(require("path"));
6519
+ var ROUTER_API_BASE = "http://127.0.0.1:8080/api";
6520
+ var ROUTER_RELOAD_LOCK_FILE = import_node_path24.default.join(DEVROUTER_HOME, "router-reload.lock");
6521
+ var ROUTER_API_REQUEST_TIMEOUT_SECONDS = "2";
6522
+ var DEFAULT_INITIAL_TIMEOUT_MS = 3e3;
6523
+ var DEFAULT_RECOVERY_TIMEOUT_MS = 1e4;
6524
+ var DEFAULT_POLL_INTERVAL_MS = 250;
6525
+ var ROUTER_API_PAGE_SIZE = 1e3;
6526
+ function sleep(ms) {
6527
+ return new Promise((resolve) => setTimeout(resolve, ms));
6341
6528
  }
6342
- function runDevpodAction(action, devpodId, force = false) {
6343
- const args = action === "delete" ? [action, devpodId, ...force ? ["--force"] : [], "--ignore-not-found"] : [action, devpodId];
6344
- claimLifecycleEffect();
6345
- const result = (0, import_node_child_process16.spawnSync)("devpod", args, { encoding: "utf-8" });
6529
+ function inspectRouterApi(protocol) {
6530
+ const result = (0, import_node_child_process16.spawnSync)(
6531
+ "curl",
6532
+ [
6533
+ "--silent",
6534
+ "--show-error",
6535
+ "--fail",
6536
+ "--max-time",
6537
+ ROUTER_API_REQUEST_TIMEOUT_SECONDS,
6538
+ `${ROUTER_API_BASE}/${protocol}/routers?per_page=${ROUTER_API_PAGE_SIZE}`
6539
+ ],
6540
+ { encoding: "utf-8" }
6541
+ );
6346
6542
  if (result.status !== 0) {
6347
- throw new Error(
6348
- `devpod ${action}${force ? " --force" : ""} failed for '${devpodId}': ${commandFailure2(result) || "unknown error"}`
6349
- );
6543
+ return {
6544
+ ok: false,
6545
+ details: result.stderr?.trim() || `curl exited with status ${result.status ?? "unknown"}`
6546
+ };
6547
+ }
6548
+ let value;
6549
+ try {
6550
+ value = JSON.parse(result.stdout);
6551
+ } catch {
6552
+ return { ok: false, details: `${protocol.toUpperCase()} router API returned invalid JSON` };
6350
6553
  }
6554
+ if (!Array.isArray(value)) {
6555
+ return { ok: false, details: `${protocol.toUpperCase()} router API returned a non-array` };
6556
+ }
6557
+ if (value.some((item) => !isRecord3(item) || typeof item.name !== "string" || !item.name))
6558
+ return {
6559
+ ok: false,
6560
+ details: `${protocol.toUpperCase()} router API returned malformed entries`
6561
+ };
6562
+ return {
6563
+ ok: true,
6564
+ reachedPageLimit: value.length >= ROUTER_API_PAGE_SIZE,
6565
+ names: new Set(
6566
+ value.flatMap((item) => {
6567
+ if (!item || typeof item !== "object") return [];
6568
+ const name = item.name;
6569
+ return typeof name === "string" ? [name] : [];
6570
+ })
6571
+ )
6572
+ };
6351
6573
  }
6352
- function inspectExactOwnership2(devpodId, worktreePath) {
6353
- const ownership = inspectDevpodWorkspaceOwnership(
6354
- listDevpodWorkspaces(worktreePath),
6355
- devpodId,
6356
- worktreePath
6357
- );
6358
- if (ownership.status === "conflict") throw new Error(ownership.reason);
6574
+ function isRecord3(value) {
6575
+ return value !== null && typeof value === "object" && !Array.isArray(value);
6576
+ }
6577
+ function inspectNamedRouterApi(protocol, resource, name) {
6578
+ const result = (0, import_node_child_process16.spawnSync)(
6579
+ "curl",
6580
+ [
6581
+ "--silent",
6582
+ "--show-error",
6583
+ "--fail",
6584
+ "--max-time",
6585
+ ROUTER_API_REQUEST_TIMEOUT_SECONDS,
6586
+ `${ROUTER_API_BASE}/${protocol}/${resource}/${encodeURIComponent(name)}`
6587
+ ],
6588
+ { encoding: "utf-8" }
6589
+ );
6590
+ if (result.status !== 0) {
6591
+ return {
6592
+ ok: false,
6593
+ details: result.stderr?.trim() || `curl exited with status ${result.status ?? "unknown"}`
6594
+ };
6595
+ }
6596
+ let value;
6597
+ try {
6598
+ value = JSON.parse(result.stdout);
6599
+ } catch {
6600
+ return {
6601
+ ok: false,
6602
+ details: `${protocol.toUpperCase()} ${resource.slice(0, -1)} API returned invalid JSON`
6603
+ };
6604
+ }
6605
+ if (!isRecord3(value)) {
6606
+ return {
6607
+ ok: false,
6608
+ details: `${protocol.toUpperCase()} ${resource.slice(0, -1)} API returned a non-object`
6609
+ };
6610
+ }
6611
+ return { ok: true, value };
6612
+ }
6613
+ function renderExpectedRoute(route) {
6614
+ const protocol = route.protocol ?? "http";
6615
+ const routerName = buildHostRouteRouterName(route.repoPath, route.name);
6616
+ const routeState = {
6617
+ id: buildHostRouteId(route.repoPath, route.name),
6618
+ name: route.name,
6619
+ host: route.host,
6620
+ protocol,
6621
+ tcpProtocol: route.tcpProtocol,
6622
+ repoPath: route.repoPath,
6623
+ port: route.port,
6624
+ mode: route.mode,
6625
+ upstreamHost: route.upstreamHost,
6626
+ pid: route.pid,
6627
+ command: route.command,
6628
+ workspace: route.workspace,
6629
+ createdAt: "",
6630
+ updatedAt: ""
6631
+ };
6632
+ const document = buildHostRoutesDocument([routeState], false);
6633
+ const protocolDocument = document[protocol];
6634
+ if (!isRecord3(protocolDocument)) return void 0;
6635
+ const routers = protocolDocument.routers;
6636
+ const services = protocolDocument.services;
6637
+ if (!isRecord3(routers) || !isRecord3(services)) return void 0;
6638
+ const routerKey = routerName.slice(0, routerName.lastIndexOf("@"));
6639
+ const router = routers[routerKey];
6640
+ const service = services[routerKey];
6641
+ if (!isRecord3(router) || !isRecord3(service)) return void 0;
6642
+ const rule = router.rule;
6643
+ const serviceReference = router.service;
6644
+ const loadBalancer = service.loadBalancer;
6645
+ const servers = isRecord3(loadBalancer) ? loadBalancer.servers : void 0;
6646
+ if (typeof rule !== "string" || serviceReference !== routerKey || !isRecord3(loadBalancer) || !Array.isArray(servers) || servers.length !== 1 || !isRecord3(servers[0])) {
6647
+ return void 0;
6648
+ }
6649
+ const serverField = protocol === "http" ? "url" : "address";
6650
+ const server = servers[0][serverField];
6651
+ if (typeof server !== "string") return void 0;
6652
+ const provider = routerName.slice(routerName.lastIndexOf("@")).trim();
6653
+ if (!provider.startsWith("@")) return void 0;
6654
+ return {
6655
+ protocol,
6656
+ routerName,
6657
+ serviceKey: routerKey,
6658
+ serviceName: `${routerKey}${provider}`,
6659
+ rule,
6660
+ serverField,
6661
+ server
6662
+ };
6663
+ }
6664
+ function inspectExpectedRouteMatch(route) {
6665
+ const expected = renderExpectedRoute(route);
6666
+ const routeName = buildHostRouteRouterName(route.repoPath, route.name);
6667
+ if (!expected) {
6668
+ return {
6669
+ ok: false,
6670
+ routes: [routeName],
6671
+ details: "host-route renderer returned an incomplete route configuration"
6672
+ };
6673
+ }
6674
+ const routerResult = inspectNamedRouterApi(expected.protocol, "routers", expected.routerName);
6675
+ if (!routerResult.ok) {
6676
+ return { ok: false, routes: [routeName], details: routerResult.details };
6677
+ }
6678
+ if (routerResult.value.status !== "enabled" || routerResult.value.name !== expected.routerName || routerResult.value.rule !== expected.rule || routerResult.value.service !== expected.serviceKey && routerResult.value.service !== expected.serviceName) {
6679
+ return {
6680
+ ok: false,
6681
+ routes: [routeName],
6682
+ details: "active Traefik router does not match the rendered route"
6683
+ };
6684
+ }
6685
+ const serviceResult = inspectNamedRouterApi(expected.protocol, "services", expected.serviceName);
6686
+ if (!serviceResult.ok) {
6687
+ return { ok: false, routes: [routeName], details: serviceResult.details };
6688
+ }
6689
+ const loadBalancer = serviceResult.value.loadBalancer;
6690
+ const servers = isRecord3(loadBalancer) ? loadBalancer.servers : void 0;
6691
+ const server = Array.isArray(servers) && servers.length === 1 ? servers[0] : void 0;
6692
+ if (serviceResult.value.status !== "enabled" || serviceResult.value.name !== expected.serviceName || !isRecord3(loadBalancer) || !Array.isArray(servers) || servers.length !== 1 || !isRecord3(server) || server[expected.serverField] !== expected.server) {
6693
+ return {
6694
+ ok: false,
6695
+ routes: [routeName],
6696
+ details: "active Traefik service does not match the rendered route"
6697
+ };
6698
+ }
6699
+ return { ok: true };
6700
+ }
6701
+ function inspectExpectedRouteMatches(routes) {
6702
+ const mismatched = [];
6703
+ const failures = [];
6704
+ for (const route of routes) {
6705
+ const result = inspectExpectedRouteMatch(route);
6706
+ if (!result.ok) {
6707
+ mismatched.push(...result.routes);
6708
+ failures.push(result.details);
6709
+ }
6710
+ }
6711
+ return mismatched.length === 0 ? { ok: true } : {
6712
+ ok: false,
6713
+ routes: mismatched,
6714
+ details: failures.join("; ")
6715
+ };
6716
+ }
6717
+ async function waitForExpectedRouteMatches(routes, timeoutMs, pollIntervalMs) {
6718
+ const deadline = Date.now() + timeoutMs;
6719
+ let result;
6720
+ do {
6721
+ result = inspectExpectedRouteMatches(routes);
6722
+ if (result.ok) return result;
6723
+ if (Date.now() < deadline) {
6724
+ await sleep(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())));
6725
+ }
6726
+ } while (Date.now() < deadline);
6727
+ return result;
6728
+ }
6729
+ function inspectExpectedRoutes(routes, expectation) {
6730
+ const mismatched = [];
6731
+ const failures = [];
6732
+ for (const protocol of ["http", "tcp"]) {
6733
+ const expected = routes.filter((route) => (route.protocol ?? "http") === protocol).map((route) => buildHostRouteRouterName(route.repoPath, route.name));
6734
+ if (expected.length === 0) continue;
6735
+ const result = inspectRouterApi(protocol);
6736
+ if (!result.ok) {
6737
+ failures.push(result.details);
6738
+ mismatched.push(...expected);
6739
+ continue;
6740
+ }
6741
+ const unexpected = expected.filter(
6742
+ (name) => expectation === "loaded" ? !result.names.has(name) : result.names.has(name)
6743
+ );
6744
+ const boundedAbsenceIsUncertain = result.reachedPageLimit && (expectation === "loaded" ? unexpected.length > 0 : unexpected.length === 0);
6745
+ if (boundedAbsenceIsUncertain) {
6746
+ failures.push(
6747
+ `${protocol.toUpperCase()} router API reached the ${ROUTER_API_PAGE_SIZE}-entry safety limit`
6748
+ );
6749
+ }
6750
+ mismatched.push(...unexpected);
6751
+ if (expectation === "removed" && unexpected.length === 0 && result.reachedPageLimit) {
6752
+ mismatched.push(...expected);
6753
+ }
6754
+ }
6755
+ return mismatched.length === 0 ? { ok: true } : {
6756
+ ok: false,
6757
+ routes: mismatched,
6758
+ details: failures.length > 0 ? failures.join("; ") : expectation === "loaded" ? "router names are absent" : "router names are still present"
6759
+ };
6760
+ }
6761
+ async function waitForExpectedRoutes(routes, expectation, timeoutMs, pollIntervalMs) {
6762
+ const deadline = Date.now() + timeoutMs;
6763
+ let result;
6764
+ do {
6765
+ result = inspectExpectedRoutes(routes, expectation);
6766
+ if (result.ok) return result;
6767
+ if (Date.now() < deadline) {
6768
+ await sleep(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())));
6769
+ }
6770
+ } while (Date.now() < deadline);
6771
+ return result;
6772
+ }
6773
+ async function ensureTraefikRouteExpectation(routes, expectation, options) {
6774
+ if (routes.length === 0) return { restarted: false };
6775
+ const initialTimeoutMs = options.initialTimeoutMs ?? DEFAULT_INITIAL_TIMEOUT_MS;
6776
+ const recoveryTimeoutMs = options.recoveryTimeoutMs ?? DEFAULT_RECOVERY_TIMEOUT_MS;
6777
+ const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
6778
+ const initial = await waitForExpectedRoutes(
6779
+ routes,
6780
+ expectation,
6781
+ initialTimeoutMs,
6782
+ pollIntervalMs
6783
+ );
6784
+ if (initial.ok) return { restarted: false };
6785
+ if (options.allowRestart === false)
6786
+ throw new Error(
6787
+ "Traefik routes did not reload; shared router restart is disabled for this operation."
6788
+ );
6789
+ import_node_fs19.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
6790
+ return withFileLock(
6791
+ ROUTER_RELOAD_LOCK_FILE,
6792
+ {
6793
+ activity: "Traefik route reload recovery",
6794
+ waitMs: initialTimeoutMs + recoveryTimeoutMs + 1e4,
6795
+ fair: true,
6796
+ onWait: createStderrWaitReporter("Traefik route reload recovery", "shared router")
6797
+ },
6798
+ async () => {
6799
+ const recheck = await waitForExpectedRoutes(routes, expectation, 0, pollIntervalMs);
6800
+ if (recheck.ok) return { restarted: false };
6801
+ claimLifecycleEffect();
6802
+ restartRouterStack();
6803
+ const recovered = await waitForExpectedRoutes(
6804
+ routes,
6805
+ expectation,
6806
+ recoveryTimeoutMs,
6807
+ pollIntervalMs
6808
+ );
6809
+ if (recovered.ok) return { restarted: true };
6810
+ const action = expectation === "loaded" ? "load" : "remove";
6811
+ throw new Error(
6812
+ `Traefik did not ${action} file-provider routes after one restart: ${recovered.routes.join(", ")} (${recovered.details}). Inspect: devrouter logs --tail 100`
6813
+ );
6814
+ }
6815
+ );
6816
+ }
6817
+ async function ensureTraefikRoutesLoaded(routes, options = {}) {
6818
+ return ensureTraefikRouteExpectation(routes, "loaded", options);
6819
+ }
6820
+ async function ensureTraefikRoutesMatch(routes, options = {}) {
6821
+ if (routes.length === 0) return;
6822
+ const timeoutMs = options.initialTimeoutMs ?? options.recoveryTimeoutMs ?? DEFAULT_INITIAL_TIMEOUT_MS;
6823
+ const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
6824
+ const result = await waitForExpectedRouteMatches(routes, timeoutMs, pollIntervalMs);
6825
+ if (!result.ok) {
6826
+ throw new Error(
6827
+ `Traefik route configuration did not match rendered routes: ${result.routes.join(", ")} (${result.details})`
6828
+ );
6829
+ }
6830
+ }
6831
+ async function ensureTraefikRoutesRemoved(routes, options = {}) {
6832
+ return ensureTraefikRouteExpectation(routes, "removed", options);
6833
+ }
6834
+ function assertTraefikRoutesRemoved(routes) {
6835
+ const result = inspectExpectedRoutes(routes, "removed");
6836
+ if (!result.ok) throw new Error(`Workspace route cessation is not proven: ${result.details}`);
6837
+ }
6838
+
6839
+ // src/core/workspace-ensure.ts
6840
+ var import_node_child_process21 = require("child_process");
6841
+ var import_node_fs23 = __toESM(require("fs"));
6842
+ var import_node_path27 = __toESM(require("path"));
6843
+
6844
+ // src/core/devpod-mutation.ts
6845
+ var import_node_child_process17 = require("child_process");
6846
+ var import_node_fs20 = __toESM(require("fs"));
6847
+ var import_node_path25 = __toESM(require("path"));
6848
+ var DEVPOD_MUTATION_LOCK_FILE = import_node_path25.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
6849
+ var DEVPOD_MUTATION_WAIT_MS = 18e5;
6850
+ var DevpodStartPostconditionError = class extends Error {
6851
+ };
6852
+ function withMutationLock2(activity, target, operation) {
6853
+ import_node_fs20.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
6854
+ return withFileLockSync(
6855
+ DEVPOD_MUTATION_LOCK_FILE,
6856
+ {
6857
+ activity,
6858
+ target: `'${target}'`,
6859
+ waitMs: DEVPOD_MUTATION_WAIT_MS,
6860
+ fair: true,
6861
+ onWait: createStderrWaitReporter(activity, `'${target}'`)
6862
+ },
6863
+ operation
6864
+ );
6865
+ }
6866
+ function commandFailure2(result) {
6867
+ return [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
6868
+ }
6869
+ function runDevpodAction(action, devpodId, force = false) {
6870
+ const args = action === "delete" ? [action, devpodId, ...force ? ["--force"] : [], "--ignore-not-found"] : [action, devpodId];
6871
+ claimLifecycleEffect();
6872
+ const result = (0, import_node_child_process17.spawnSync)("devpod", args, { encoding: "utf-8" });
6873
+ if (result.status !== 0) {
6874
+ throw new Error(
6875
+ `devpod ${action}${force ? " --force" : ""} failed for '${devpodId}': ${commandFailure2(result) || "unknown error"}`
6876
+ );
6877
+ }
6878
+ }
6879
+ function inspectExactOwnership2(devpodId, worktreePath) {
6880
+ const ownership = inspectDevpodWorkspaceOwnership(
6881
+ listDevpodWorkspaces(worktreePath),
6882
+ devpodId,
6883
+ worktreePath
6884
+ );
6885
+ if (ownership.status === "conflict") throw new Error(ownership.reason);
6359
6886
  return ownership;
6360
6887
  }
6361
6888
  function mutateOwnedDevpodWorkspace(action, devpodId, worktreePath) {
@@ -6472,7 +6999,7 @@ async function startDevpodWorkspace(options) {
6472
6999
  delete env.DEVCONTAINER_COMPOSE_OVERLAY;
6473
7000
  }
6474
7001
  claimLifecycleEffect();
6475
- const result = (0, import_node_child_process16.spawnSync)("devpod", args, {
7002
+ const result = (0, import_node_child_process17.spawnSync)("devpod", args, {
6476
7003
  stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
6477
7004
  env
6478
7005
  });
@@ -6505,7 +7032,7 @@ async function startDevpodWorkspace(options) {
6505
7032
  }
6506
7033
 
6507
7034
  // src/core/http-route-probe.ts
6508
- var import_node_child_process17 = require("child_process");
7035
+ var import_node_child_process18 = require("child_process");
6509
7036
  var DEFAULT_MAX_TIME_SECONDS = 5;
6510
7037
  var PARENT_TIMEOUT_PADDING_MS = 250;
6511
7038
  var CONTRACT_MAX_BUFFER = 1024;
@@ -6538,7 +7065,7 @@ function probeHttpRoute(host, options = {}) {
6538
7065
  args.push("--cacert", CERT_FILE);
6539
7066
  }
6540
7067
  args.push(url);
6541
- const result = (0, import_node_child_process17.spawnSync)("curl", args, { encoding: "utf-8" });
7068
+ const result = (0, import_node_child_process18.spawnSync)("curl", args, { encoding: "utf-8" });
6542
7069
  const stdout = result.stdout?.trim() ?? "";
6543
7070
  const status = /^\d{3}$/.test(stdout) ? Number(stdout) : void 0;
6544
7071
  const ok = result.status === 0 && status !== void 0 && status >= 100 && status < 500;
@@ -6557,7 +7084,7 @@ function probeHttpReadinessSync(host, readiness, maxTimeSeconds, repoPath) {
6557
7084
  }
6558
7085
  let result;
6559
7086
  try {
6560
- result = (0, import_node_child_process17.spawnSync)("curl", args, {
7087
+ result = (0, import_node_child_process18.spawnSync)("curl", args, {
6561
7088
  encoding: "utf-8",
6562
7089
  maxBuffer: CONTRACT_MAX_BUFFER,
6563
7090
  timeout: Math.ceil(parentTimeoutMs)
@@ -6641,8 +7168,8 @@ function transportFailure() {
6641
7168
  }
6642
7169
 
6643
7170
  // src/core/managed-host-preparation.ts
6644
- var import_node_child_process18 = require("child_process");
6645
- var import_node_fs19 = __toESM(require("fs"));
7171
+ var import_node_child_process19 = require("child_process");
7172
+ var import_node_fs21 = __toESM(require("fs"));
6646
7173
  var PREPARATION_TIMEOUT_MS = 6e4;
6647
7174
  var CONFIG_READ_ERROR = "Managed host preparation could not read .devrouter.yml.";
6648
7175
  var CONFIG_CHANGED_ERROR = "Managed host preparation changed .devrouter.yml; refusing to continue.";
@@ -6653,7 +7180,7 @@ function runPreparationCommand(argv, cwd) {
6653
7180
  return new Promise((resolve) => {
6654
7181
  let child;
6655
7182
  try {
6656
- child = (0, import_node_child_process18.spawn)(argv[0], argv.slice(1), {
7183
+ child = (0, import_node_child_process19.spawn)(argv[0], argv.slice(1), {
6657
7184
  cwd,
6658
7185
  detached: true,
6659
7186
  shell: false,
@@ -6685,7 +7212,7 @@ function runPreparationCommand(argv, cwd) {
6685
7212
  }
6686
7213
  function readConfigBytes(configPath) {
6687
7214
  try {
6688
- return import_node_fs19.default.readFileSync(configPath);
7215
+ return import_node_fs21.default.readFileSync(configPath);
6689
7216
  } catch {
6690
7217
  return void 0;
6691
7218
  }
@@ -6719,10 +7246,10 @@ async function runManagedHostPreparation(repoPath, config) {
6719
7246
  }
6720
7247
 
6721
7248
  // src/core/managed-post-start.ts
6722
- var import_node_child_process19 = require("child_process");
7249
+ var import_node_child_process20 = require("child_process");
6723
7250
  var import_node_crypto10 = require("crypto");
6724
- var import_node_fs20 = __toESM(require("fs"));
6725
- var import_node_path24 = __toESM(require("path"));
7251
+ var import_node_fs22 = __toESM(require("fs"));
7252
+ var import_node_path26 = __toESM(require("path"));
6726
7253
  var MANAGED_MARKER = "devrouter:managed devcontainer";
6727
7254
  var MANAGED_ADAPTER_PATH = ".devcontainer/post-start.sh";
6728
7255
  var RUNTIME_HELPER_PATH = "/tmp/devrouter/bin/devrouter-process";
@@ -6735,7 +7262,7 @@ function renderDeliveryScript(targetPath) {
6735
7262
  if (!/^\/tmp\/devrouter\/bin\/[a-zA-Z0-9._-]+$/.test(targetPath)) {
6736
7263
  throw new Error(`Unsafe managed runtime delivery path: ${targetPath}`);
6737
7264
  }
6738
- const temporaryPrefix = import_node_path24.default.posix.basename(targetPath);
7265
+ const temporaryPrefix = import_node_path26.default.posix.basename(targetPath);
6739
7266
  return `set -eu
6740
7267
  umask 077
6741
7268
  runtime_root=/tmp/devrouter
@@ -6759,7 +7286,7 @@ function commandFailure3(result) {
6759
7286
  return [result.error?.message, result.stderr, result.stdout].filter(Boolean).map((value) => String(value).trim()).filter(Boolean).join("\n");
6760
7287
  }
6761
7288
  function deliverRuntimeFile(containerId, targetPath, contents, label) {
6762
- const result = (0, import_node_child_process19.spawnSync)(
7289
+ const result = (0, import_node_child_process20.spawnSync)(
6763
7290
  "docker",
6764
7291
  ["exec", "-i", containerId, "sh", "-c", renderDeliveryScript(targetPath)],
6765
7292
  { input: contents, encoding: "utf-8" }
@@ -6770,8 +7297,8 @@ function deliverRuntimeFile(containerId, targetPath, contents, label) {
6770
7297
  }
6771
7298
  }
6772
7299
  function readRegularFileBytes(filePath) {
6773
- if (!import_node_fs20.default.existsSync(filePath) || !import_node_fs20.default.lstatSync(filePath).isFile()) return void 0;
6774
- return import_node_fs20.default.readFileSync(filePath);
7300
+ if (!import_node_fs22.default.existsSync(filePath) || !import_node_fs22.default.lstatSync(filePath).isFile()) return void 0;
7301
+ return import_node_fs22.default.readFileSync(filePath);
6775
7302
  }
6776
7303
  function readRegularFile(filePath) {
6777
7304
  return readRegularFileBytes(filePath)?.toString("utf-8");
@@ -6781,21 +7308,21 @@ function adapterFingerprint(adapter) {
6781
7308
  }
6782
7309
  function resolveProcessHelperPath() {
6783
7310
  const candidates = [
6784
- import_node_path24.default.resolve(__dirname, "..", "bin", "devrouter-process"),
6785
- import_node_path24.default.resolve(__dirname, "..", "..", "bin", "devrouter-process")
7311
+ import_node_path26.default.resolve(__dirname, "..", "bin", "devrouter-process"),
7312
+ import_node_path26.default.resolve(__dirname, "..", "..", "bin", "devrouter-process")
6786
7313
  ];
6787
- const helperPath = candidates.find((candidate) => import_node_fs20.default.existsSync(candidate));
7314
+ const helperPath = candidates.find((candidate) => import_node_fs22.default.existsSync(candidate));
6788
7315
  if (!helperPath) {
6789
7316
  throw new Error("Could not locate the packaged devrouter-process helper.");
6790
7317
  }
6791
7318
  return helperPath;
6792
7319
  }
6793
7320
  function resolveManagedPostStartPlan(repoPath) {
6794
- const adapterPath = import_node_path24.default.join(repoPath, MANAGED_ADAPTER_PATH);
7321
+ const adapterPath = import_node_path26.default.join(repoPath, MANAGED_ADAPTER_PATH);
6795
7322
  const adapterBytes = readRegularFileBytes(adapterPath);
6796
7323
  const adapterText = adapterBytes?.toString("utf-8");
6797
- const dockerfilePath = import_node_path24.default.join(repoPath, ".devcontainer", "Dockerfile");
6798
- const devcontainerPath = import_node_path24.default.join(repoPath, ".devcontainer", "devcontainer.json");
7324
+ const dockerfilePath = import_node_path26.default.join(repoPath, ".devcontainer", "Dockerfile");
7325
+ const devcontainerPath = import_node_path26.default.join(repoPath, ".devcontainer", "devcontainer.json");
6799
7326
  const dockerfile = readRegularFile(dockerfilePath) ?? "";
6800
7327
  const devcontainer = readRegularFile(devcontainerPath) ?? "";
6801
7328
  const devrouterPattern = [adapterText, dockerfile, devcontainer].filter((value) => value !== void 0).some(
@@ -6836,7 +7363,7 @@ function resolveManagedPostStartPlan(repoPath) {
6836
7363
  }
6837
7364
  function runManagedPostStart(options) {
6838
7365
  if (options.plan.kind !== "runtime") return;
6839
- const helper = import_node_fs20.default.readFileSync(resolveProcessHelperPath());
7366
+ const helper = import_node_fs22.default.readFileSync(resolveProcessHelperPath());
6840
7367
  deliverRuntimeFile(
6841
7368
  options.container.id,
6842
7369
  RUNTIME_HELPER_PATH,
@@ -6850,7 +7377,7 @@ function runManagedPostStart(options) {
6850
7377
  options.plan.adapterContents,
6851
7378
  "the managed post-start adapter"
6852
7379
  );
6853
- const started = (0, import_node_child_process19.spawnSync)(
7380
+ const started2 = (0, import_node_child_process20.spawnSync)(
6854
7381
  "docker",
6855
7382
  [
6856
7383
  "exec",
@@ -6876,8 +7403,8 @@ function runManagedPostStart(options) {
6876
7403
  ],
6877
7404
  { stdio: options.quiet ? ["ignore", 2, "inherit"] : "inherit" }
6878
7405
  );
6879
- if (started.status !== 0) {
6880
- const details = commandFailure3(started);
7406
+ if (started2.status !== 0) {
7407
+ const details = commandFailure3(started2);
6881
7408
  throw new Error(`Managed post-start failed${details ? `: ${details}.` : "."}`);
6882
7409
  }
6883
7410
  }
@@ -6888,7 +7415,7 @@ function assertSafeProcessName(name) {
6888
7415
  }
6889
7416
  function runManagedProcessAction(options) {
6890
7417
  assertSafeProcessName(options.name);
6891
- const result = (0, import_node_child_process19.spawnSync)(
7418
+ const result = (0, import_node_child_process20.spawnSync)(
6892
7419
  "docker",
6893
7420
  [
6894
7421
  "exec",
@@ -7250,420 +7777,105 @@ function collectManagedRuntimeStatus(options) {
7250
7777
  if (state) {
7251
7778
  const desiredState = {
7252
7779
  apps: sortedUnique(state.desired.apps),
7253
- services: sortedUnique(state.desired.services),
7254
- processes: sortedUnique(state.desired.processes)
7255
- };
7256
- const desired = {
7257
- apps: desiredApps,
7258
- services: desiredServices,
7259
- processes: desiredProcesses
7260
- };
7261
- for (const [label, expected, actual] of [
7262
- ["apps", desired.apps, desiredState.apps],
7263
- ["services", desired.services, desiredState.services],
7264
- ["processes", desired.processes, desiredState.processes]
7265
- ]) {
7266
- const mismatch = compareResourceSet(label, expected, actual);
7267
- if (mismatch) drift.push(mismatch);
7268
- }
7269
- if (state.profile !== options.profile)
7270
- drift.push("active profile differs from the selected profile");
7271
- }
7272
- const inspection = inspectManagedRuntime({
7273
- repoPath: options.repoPath,
7274
- config: options.config,
7275
- registeredServices,
7276
- registeredProcesses,
7277
- desiredServices,
7278
- desiredProcesses,
7279
- workspace: options.workspace,
7280
- state
7281
- });
7282
- drift.push(...inspection.drift);
7283
- if (state && inspection.plan && state.sourceConfigSha256 !== inspection.plan.sourceConfigSha256) {
7284
- drift.push("managed Dev Container source configuration changed");
7285
- }
7286
- if (state && inspection.plan && state.effectiveConfigSha256 !== inspection.plan.effectiveConfigSha256) {
7287
- drift.push("managed Dev Container effective configuration changed");
7288
- }
7289
- if (state && inspection.generatedConfigSha256 && state.effectiveConfigSha256 !== inspection.generatedConfigSha256) {
7290
- drift.push("managed generated Dev Container configuration differs from last successful state");
7291
- }
7292
- const routes = exactRoutes(options.repoPath, options.workspace);
7293
- drift.push(...routes.drift);
7294
- const activeApps = routes.apps;
7295
- const anyActive = inspection.primaryActive || inspection.activeServices.length > 0 || Object.values(inspection.baseStatuses).some(isActiveResourceStatus) || inspection.activeProcesses.length > 0 || activeApps.length > 0;
7296
- if (activeApps.length > 0 && !inspection.primaryActive) {
7297
- drift.push("active app routes exist without a running primary container");
7298
- }
7299
- if (desiredApps.some((app) => !activeApps.includes(app)) && anyActive) {
7300
- drift.push("one or more selected app routes are missing");
7301
- }
7302
- if (activeApps.some((app) => !desiredApps.includes(app))) {
7303
- drift.push("one or more active app routes are outside the selected profile");
7304
- }
7305
- if (inspection.activeServices.some((service) => !desiredServices.includes(service))) {
7306
- drift.push("one or more active services are outside the selected profile");
7307
- }
7308
- if (inspection.activeProcesses.some((process2) => !desiredProcesses.includes(process2))) {
7309
- drift.push("one or more active processes are outside the selected profile");
7310
- }
7311
- const positivelyStopped = !inspection.primaryActive && Object.values(inspection.baseStatuses).every(isStoppedOrMissingResourceStatus) && Object.values(inspection.serviceStatuses).every(isStoppedOrMissingResourceStatus) && activeApps.length === 0;
7312
- const onlyDegradedIncident = state?.status === "degraded" && drift.length === 1;
7313
- const requiredServicesReady = desiredServices.every(
7314
- (service) => isReadyResourceStatus(inspection.serviceStatuses[service] ?? "missing")
7315
- );
7316
- const baseServicesReady = Object.values(inspection.baseStatuses).every(isReadyResourceStatus);
7317
- const processesReady = desiredProcesses.every(
7318
- (process2) => isReadyResourceStatus(inspection.processStatuses[process2] ?? "missing")
7319
- );
7320
- const routesReady = desiredApps.every((app) => activeApps.includes(app));
7321
- const ready = inspection.primaryReady && requiredServicesReady && baseServicesReady && processesReady && routesReady && !stateReadFailed && !drift.length;
7322
- const status = positivelyStopped && onlyDegradedIncident ? "stopped" : state?.status === "degraded" ? "failed-transition" : drift.length > 0 ? "drifted" : ready ? "ready" : anyActive ? "starting" : "stopped";
7323
- const active = {
7324
- apps: activeApps,
7325
- services: inspection.activeServices,
7326
- processes: inspection.activeProcesses
7327
- };
7328
- return {
7329
- mode: "managed",
7330
- status,
7331
- profile: options.profile,
7332
- ...state?.profile ? { activeProfile: state.profile } : {},
7333
- ...options.workspace !== void 0 ? { workspace: options.workspace } : {},
7334
- ...state?.devpodId ? { devpodId: state.devpodId } : {},
7335
- ...inspection.composeProject ? { composeProject: inspection.composeProject } : {},
7336
- desired: {
7337
- apps: desiredApps,
7338
- services: desiredServices,
7339
- processes: desiredProcesses
7340
- },
7341
- active,
7342
- serviceStatuses: inspection.serviceStatuses,
7343
- baseServiceStatuses: inspection.baseStatuses,
7344
- processStatuses: inspection.processStatuses,
7345
- drift: sortedUnique(drift),
7346
- ...inspection.plan?.sourceConfigSha256 || state?.sourceConfigSha256 ? { sourceConfigSha256: inspection.plan?.sourceConfigSha256 ?? state?.sourceConfigSha256 } : {},
7347
- ...inspection.plan?.effectiveConfigSha256 || state?.effectiveConfigSha256 ? {
7348
- effectiveConfigSha256: inspection.plan?.effectiveConfigSha256 ?? state?.effectiveConfigSha256
7349
- } : {},
7350
- ...state?.transitionPhase ? { transitionPhase: state.transitionPhase } : {}
7351
- };
7352
- }
7353
-
7354
- // src/core/traefik-route-health.ts
7355
- var import_node_child_process20 = require("child_process");
7356
- var import_node_fs21 = __toESM(require("fs"));
7357
- var import_node_path25 = __toESM(require("path"));
7358
- var ROUTER_API_BASE = "http://127.0.0.1:8080/api";
7359
- var ROUTER_RELOAD_LOCK_FILE = import_node_path25.default.join(DEVROUTER_HOME, "router-reload.lock");
7360
- var ROUTER_API_REQUEST_TIMEOUT_SECONDS = "2";
7361
- var DEFAULT_INITIAL_TIMEOUT_MS = 3e3;
7362
- var DEFAULT_RECOVERY_TIMEOUT_MS = 1e4;
7363
- var DEFAULT_POLL_INTERVAL_MS = 250;
7364
- var ROUTER_API_PAGE_SIZE = 1e3;
7365
- function sleep(ms) {
7366
- return new Promise((resolve) => setTimeout(resolve, ms));
7367
- }
7368
- function inspectRouterApi(protocol) {
7369
- const result = (0, import_node_child_process20.spawnSync)(
7370
- "curl",
7371
- [
7372
- "--silent",
7373
- "--show-error",
7374
- "--fail",
7375
- "--max-time",
7376
- ROUTER_API_REQUEST_TIMEOUT_SECONDS,
7377
- `${ROUTER_API_BASE}/${protocol}/routers?per_page=${ROUTER_API_PAGE_SIZE}`
7378
- ],
7379
- { encoding: "utf-8" }
7380
- );
7381
- if (result.status !== 0) {
7382
- return {
7383
- ok: false,
7384
- details: result.stderr?.trim() || `curl exited with status ${result.status ?? "unknown"}`
7385
- };
7386
- }
7387
- let value;
7388
- try {
7389
- value = JSON.parse(result.stdout);
7390
- } catch {
7391
- return { ok: false, details: `${protocol.toUpperCase()} router API returned invalid JSON` };
7392
- }
7393
- if (!Array.isArray(value)) {
7394
- return { ok: false, details: `${protocol.toUpperCase()} router API returned a non-array` };
7395
- }
7396
- return {
7397
- ok: true,
7398
- reachedPageLimit: value.length >= ROUTER_API_PAGE_SIZE,
7399
- names: new Set(
7400
- value.flatMap((item) => {
7401
- if (!item || typeof item !== "object") return [];
7402
- const name = item.name;
7403
- return typeof name === "string" ? [name] : [];
7404
- })
7405
- )
7406
- };
7407
- }
7408
- function isRecord3(value) {
7409
- return value !== null && typeof value === "object" && !Array.isArray(value);
7410
- }
7411
- function inspectNamedRouterApi(protocol, resource, name) {
7412
- const result = (0, import_node_child_process20.spawnSync)(
7413
- "curl",
7414
- [
7415
- "--silent",
7416
- "--show-error",
7417
- "--fail",
7418
- "--max-time",
7419
- ROUTER_API_REQUEST_TIMEOUT_SECONDS,
7420
- `${ROUTER_API_BASE}/${protocol}/${resource}/${encodeURIComponent(name)}`
7421
- ],
7422
- { encoding: "utf-8" }
7423
- );
7424
- if (result.status !== 0) {
7425
- return {
7426
- ok: false,
7427
- details: result.stderr?.trim() || `curl exited with status ${result.status ?? "unknown"}`
7428
- };
7429
- }
7430
- let value;
7431
- try {
7432
- value = JSON.parse(result.stdout);
7433
- } catch {
7434
- return {
7435
- ok: false,
7436
- details: `${protocol.toUpperCase()} ${resource.slice(0, -1)} API returned invalid JSON`
7437
- };
7438
- }
7439
- if (!isRecord3(value)) {
7440
- return {
7441
- ok: false,
7442
- details: `${protocol.toUpperCase()} ${resource.slice(0, -1)} API returned a non-object`
7443
- };
7444
- }
7445
- return { ok: true, value };
7446
- }
7447
- function renderExpectedRoute(route) {
7448
- const protocol = route.protocol ?? "http";
7449
- const routerName = buildHostRouteRouterName(route.repoPath, route.name);
7450
- const routeState = {
7451
- id: buildHostRouteId(route.repoPath, route.name),
7452
- name: route.name,
7453
- host: route.host,
7454
- protocol,
7455
- tcpProtocol: route.tcpProtocol,
7456
- repoPath: route.repoPath,
7457
- port: route.port,
7458
- mode: route.mode,
7459
- upstreamHost: route.upstreamHost,
7460
- pid: route.pid,
7461
- command: route.command,
7462
- workspace: route.workspace,
7463
- createdAt: "",
7464
- updatedAt: ""
7465
- };
7466
- const document = buildHostRoutesDocument([routeState], false);
7467
- const protocolDocument = document[protocol];
7468
- if (!isRecord3(protocolDocument)) return void 0;
7469
- const routers = protocolDocument.routers;
7470
- const services = protocolDocument.services;
7471
- if (!isRecord3(routers) || !isRecord3(services)) return void 0;
7472
- const routerKey = routerName.slice(0, routerName.lastIndexOf("@"));
7473
- const router = routers[routerKey];
7474
- const service = services[routerKey];
7475
- if (!isRecord3(router) || !isRecord3(service)) return void 0;
7476
- const rule = router.rule;
7477
- const serviceReference = router.service;
7478
- const loadBalancer = service.loadBalancer;
7479
- const servers = isRecord3(loadBalancer) ? loadBalancer.servers : void 0;
7480
- if (typeof rule !== "string" || serviceReference !== routerKey || !isRecord3(loadBalancer) || !Array.isArray(servers) || servers.length !== 1 || !isRecord3(servers[0])) {
7481
- return void 0;
7482
- }
7483
- const serverField = protocol === "http" ? "url" : "address";
7484
- const server = servers[0][serverField];
7485
- if (typeof server !== "string") return void 0;
7486
- const provider = routerName.slice(routerName.lastIndexOf("@")).trim();
7487
- if (!provider.startsWith("@")) return void 0;
7488
- return {
7489
- protocol,
7490
- routerName,
7491
- serviceKey: routerKey,
7492
- serviceName: `${routerKey}${provider}`,
7493
- rule,
7494
- serverField,
7495
- server
7496
- };
7497
- }
7498
- function inspectExpectedRouteMatch(route) {
7499
- const expected = renderExpectedRoute(route);
7500
- const routeName = buildHostRouteRouterName(route.repoPath, route.name);
7501
- if (!expected) {
7502
- return {
7503
- ok: false,
7504
- routes: [routeName],
7505
- details: "host-route renderer returned an incomplete route configuration"
7780
+ services: sortedUnique(state.desired.services),
7781
+ processes: sortedUnique(state.desired.processes)
7782
+ };
7783
+ const desired = {
7784
+ apps: desiredApps,
7785
+ services: desiredServices,
7786
+ processes: desiredProcesses
7506
7787
  };
7788
+ for (const [label, expected, actual] of [
7789
+ ["apps", desired.apps, desiredState.apps],
7790
+ ["services", desired.services, desiredState.services],
7791
+ ["processes", desired.processes, desiredState.processes]
7792
+ ]) {
7793
+ const mismatch = compareResourceSet(label, expected, actual);
7794
+ if (mismatch) drift.push(mismatch);
7795
+ }
7796
+ if (state.profile !== options.profile)
7797
+ drift.push("active profile differs from the selected profile");
7507
7798
  }
7508
- const routerResult = inspectNamedRouterApi(expected.protocol, "routers", expected.routerName);
7509
- if (!routerResult.ok) {
7510
- return { ok: false, routes: [routeName], details: routerResult.details };
7799
+ const inspection = inspectManagedRuntime({
7800
+ repoPath: options.repoPath,
7801
+ config: options.config,
7802
+ registeredServices,
7803
+ registeredProcesses,
7804
+ desiredServices,
7805
+ desiredProcesses,
7806
+ workspace: options.workspace,
7807
+ state
7808
+ });
7809
+ drift.push(...inspection.drift);
7810
+ if (state && inspection.plan && state.sourceConfigSha256 !== inspection.plan.sourceConfigSha256) {
7811
+ drift.push("managed Dev Container source configuration changed");
7511
7812
  }
7512
- if (routerResult.value.status !== "enabled" || routerResult.value.name !== expected.routerName || routerResult.value.rule !== expected.rule || routerResult.value.service !== expected.serviceKey && routerResult.value.service !== expected.serviceName) {
7513
- return {
7514
- ok: false,
7515
- routes: [routeName],
7516
- details: "active Traefik router does not match the rendered route"
7517
- };
7813
+ if (state && inspection.plan && state.effectiveConfigSha256 !== inspection.plan.effectiveConfigSha256) {
7814
+ drift.push("managed Dev Container effective configuration changed");
7518
7815
  }
7519
- const serviceResult = inspectNamedRouterApi(expected.protocol, "services", expected.serviceName);
7520
- if (!serviceResult.ok) {
7521
- return { ok: false, routes: [routeName], details: serviceResult.details };
7816
+ if (state && inspection.generatedConfigSha256 && state.effectiveConfigSha256 !== inspection.generatedConfigSha256) {
7817
+ drift.push("managed generated Dev Container configuration differs from last successful state");
7522
7818
  }
7523
- const loadBalancer = serviceResult.value.loadBalancer;
7524
- const servers = isRecord3(loadBalancer) ? loadBalancer.servers : void 0;
7525
- const server = Array.isArray(servers) && servers.length === 1 ? servers[0] : void 0;
7526
- if (serviceResult.value.status !== "enabled" || serviceResult.value.name !== expected.serviceName || !isRecord3(loadBalancer) || !Array.isArray(servers) || servers.length !== 1 || !isRecord3(server) || server[expected.serverField] !== expected.server) {
7527
- return {
7528
- ok: false,
7529
- routes: [routeName],
7530
- details: "active Traefik service does not match the rendered route"
7531
- };
7819
+ const routes = exactRoutes(options.repoPath, options.workspace);
7820
+ drift.push(...routes.drift);
7821
+ const activeApps = routes.apps;
7822
+ const anyActive = inspection.primaryActive || inspection.activeServices.length > 0 || Object.values(inspection.baseStatuses).some(isActiveResourceStatus) || inspection.activeProcesses.length > 0 || activeApps.length > 0;
7823
+ if (activeApps.length > 0 && !inspection.primaryActive) {
7824
+ drift.push("active app routes exist without a running primary container");
7532
7825
  }
7533
- return { ok: true };
7534
- }
7535
- function inspectExpectedRouteMatches(routes) {
7536
- const mismatched = [];
7537
- const failures = [];
7538
- for (const route of routes) {
7539
- const result = inspectExpectedRouteMatch(route);
7540
- if (!result.ok) {
7541
- mismatched.push(...result.routes);
7542
- failures.push(result.details);
7543
- }
7826
+ if (desiredApps.some((app) => !activeApps.includes(app)) && anyActive) {
7827
+ drift.push("one or more selected app routes are missing");
7544
7828
  }
7545
- return mismatched.length === 0 ? { ok: true } : {
7546
- ok: false,
7547
- routes: mismatched,
7548
- details: failures.join("; ")
7549
- };
7550
- }
7551
- async function waitForExpectedRouteMatches(routes, timeoutMs, pollIntervalMs) {
7552
- const deadline = Date.now() + timeoutMs;
7553
- let result;
7554
- do {
7555
- result = inspectExpectedRouteMatches(routes);
7556
- if (result.ok) return result;
7557
- if (Date.now() < deadline) {
7558
- await sleep(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())));
7559
- }
7560
- } while (Date.now() < deadline);
7561
- return result;
7562
- }
7563
- function inspectExpectedRoutes(routes, expectation) {
7564
- const mismatched = [];
7565
- const failures = [];
7566
- for (const protocol of ["http", "tcp"]) {
7567
- const expected = routes.filter((route) => (route.protocol ?? "http") === protocol).map((route) => buildHostRouteRouterName(route.repoPath, route.name));
7568
- if (expected.length === 0) continue;
7569
- const result = inspectRouterApi(protocol);
7570
- if (!result.ok) {
7571
- failures.push(result.details);
7572
- mismatched.push(...expected);
7573
- continue;
7574
- }
7575
- const unexpected = expected.filter(
7576
- (name) => expectation === "loaded" ? !result.names.has(name) : result.names.has(name)
7577
- );
7578
- const boundedAbsenceIsUncertain = result.reachedPageLimit && (expectation === "loaded" ? unexpected.length > 0 : unexpected.length === 0);
7579
- if (boundedAbsenceIsUncertain) {
7580
- failures.push(
7581
- `${protocol.toUpperCase()} router API reached the ${ROUTER_API_PAGE_SIZE}-entry safety limit`
7582
- );
7583
- }
7584
- mismatched.push(...unexpected);
7585
- if (expectation === "removed" && unexpected.length === 0 && result.reachedPageLimit) {
7586
- mismatched.push(...expected);
7587
- }
7829
+ if (activeApps.some((app) => !desiredApps.includes(app))) {
7830
+ drift.push("one or more active app routes are outside the selected profile");
7588
7831
  }
7589
- return mismatched.length === 0 ? { ok: true } : {
7590
- ok: false,
7591
- routes: mismatched,
7592
- details: failures.length > 0 ? failures.join("; ") : expectation === "loaded" ? "router names are absent" : "router names are still present"
7593
- };
7594
- }
7595
- async function waitForExpectedRoutes(routes, expectation, timeoutMs, pollIntervalMs) {
7596
- const deadline = Date.now() + timeoutMs;
7597
- let result;
7598
- do {
7599
- result = inspectExpectedRoutes(routes, expectation);
7600
- if (result.ok) return result;
7601
- if (Date.now() < deadline) {
7602
- await sleep(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())));
7603
- }
7604
- } while (Date.now() < deadline);
7605
- return result;
7606
- }
7607
- async function ensureTraefikRouteExpectation(routes, expectation, options) {
7608
- if (routes.length === 0) return { restarted: false };
7609
- const initialTimeoutMs = options.initialTimeoutMs ?? DEFAULT_INITIAL_TIMEOUT_MS;
7610
- const recoveryTimeoutMs = options.recoveryTimeoutMs ?? DEFAULT_RECOVERY_TIMEOUT_MS;
7611
- const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
7612
- const initial = await waitForExpectedRoutes(
7613
- routes,
7614
- expectation,
7615
- initialTimeoutMs,
7616
- pollIntervalMs
7832
+ if (inspection.activeServices.some((service) => !desiredServices.includes(service))) {
7833
+ drift.push("one or more active services are outside the selected profile");
7834
+ }
7835
+ if (inspection.activeProcesses.some((process2) => !desiredProcesses.includes(process2))) {
7836
+ drift.push("one or more active processes are outside the selected profile");
7837
+ }
7838
+ const positivelyStopped = !inspection.primaryActive && Object.values(inspection.baseStatuses).every(isStoppedOrMissingResourceStatus) && Object.values(inspection.serviceStatuses).every(isStoppedOrMissingResourceStatus) && activeApps.length === 0;
7839
+ const onlyDegradedIncident = state?.status === "degraded" && drift.length === 1;
7840
+ const requiredServicesReady = desiredServices.every(
7841
+ (service) => isReadyResourceStatus(inspection.serviceStatuses[service] ?? "missing")
7617
7842
  );
7618
- if (initial.ok) return { restarted: false };
7619
- if (options.allowRestart === false)
7620
- throw new Error(
7621
- "Traefik routes did not reload; shared router restart is disabled for this operation."
7622
- );
7623
- import_node_fs21.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
7624
- return withFileLock(
7625
- ROUTER_RELOAD_LOCK_FILE,
7626
- {
7627
- activity: "Traefik route reload recovery",
7628
- waitMs: initialTimeoutMs + recoveryTimeoutMs + 1e4,
7629
- fair: true,
7630
- onWait: createStderrWaitReporter("Traefik route reload recovery", "shared router")
7631
- },
7632
- async () => {
7633
- const recheck = await waitForExpectedRoutes(routes, expectation, 0, pollIntervalMs);
7634
- if (recheck.ok) return { restarted: false };
7635
- claimLifecycleEffect();
7636
- restartRouterStack();
7637
- const recovered = await waitForExpectedRoutes(
7638
- routes,
7639
- expectation,
7640
- recoveryTimeoutMs,
7641
- pollIntervalMs
7642
- );
7643
- if (recovered.ok) return { restarted: true };
7644
- const action = expectation === "loaded" ? "load" : "remove";
7645
- throw new Error(
7646
- `Traefik did not ${action} file-provider routes after one restart: ${recovered.routes.join(", ")} (${recovered.details}). Inspect: devrouter logs --tail 100`
7647
- );
7648
- }
7843
+ const baseServicesReady = Object.values(inspection.baseStatuses).every(isReadyResourceStatus);
7844
+ const processesReady = desiredProcesses.every(
7845
+ (process2) => isReadyResourceStatus(inspection.processStatuses[process2] ?? "missing")
7649
7846
  );
7650
- }
7651
- async function ensureTraefikRoutesLoaded(routes, options = {}) {
7652
- return ensureTraefikRouteExpectation(routes, "loaded", options);
7653
- }
7654
- async function ensureTraefikRoutesMatch(routes, options = {}) {
7655
- if (routes.length === 0) return;
7656
- const timeoutMs = options.initialTimeoutMs ?? options.recoveryTimeoutMs ?? DEFAULT_INITIAL_TIMEOUT_MS;
7657
- const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
7658
- const result = await waitForExpectedRouteMatches(routes, timeoutMs, pollIntervalMs);
7659
- if (!result.ok) {
7660
- throw new Error(
7661
- `Traefik route configuration did not match rendered routes: ${result.routes.join(", ")} (${result.details})`
7662
- );
7663
- }
7664
- }
7665
- async function ensureTraefikRoutesRemoved(routes, options = {}) {
7666
- return ensureTraefikRouteExpectation(routes, "removed", options);
7847
+ const routesReady = desiredApps.every((app) => activeApps.includes(app));
7848
+ const ready = inspection.primaryReady && requiredServicesReady && baseServicesReady && processesReady && routesReady && !stateReadFailed && !drift.length;
7849
+ const status = positivelyStopped && onlyDegradedIncident ? "stopped" : state?.status === "degraded" ? "failed-transition" : drift.length > 0 ? "drifted" : ready ? "ready" : anyActive ? "starting" : "stopped";
7850
+ const active = {
7851
+ apps: activeApps,
7852
+ services: inspection.activeServices,
7853
+ processes: inspection.activeProcesses
7854
+ };
7855
+ return {
7856
+ mode: "managed",
7857
+ status,
7858
+ profile: options.profile,
7859
+ ...state?.profile ? { activeProfile: state.profile } : {},
7860
+ ...options.workspace !== void 0 ? { workspace: options.workspace } : {},
7861
+ ...state?.devpodId ? { devpodId: state.devpodId } : {},
7862
+ ...inspection.composeProject ? { composeProject: inspection.composeProject } : {},
7863
+ desired: {
7864
+ apps: desiredApps,
7865
+ services: desiredServices,
7866
+ processes: desiredProcesses
7867
+ },
7868
+ active,
7869
+ serviceStatuses: inspection.serviceStatuses,
7870
+ baseServiceStatuses: inspection.baseStatuses,
7871
+ processStatuses: inspection.processStatuses,
7872
+ drift: sortedUnique(drift),
7873
+ ...inspection.plan?.sourceConfigSha256 || state?.sourceConfigSha256 ? { sourceConfigSha256: inspection.plan?.sourceConfigSha256 ?? state?.sourceConfigSha256 } : {},
7874
+ ...inspection.plan?.effectiveConfigSha256 || state?.effectiveConfigSha256 ? {
7875
+ effectiveConfigSha256: inspection.plan?.effectiveConfigSha256 ?? state?.effectiveConfigSha256
7876
+ } : {},
7877
+ ...state?.transitionPhase ? { transitionPhase: state.transitionPhase } : {}
7878
+ };
7667
7879
  }
7668
7880
 
7669
7881
  // src/core/workspace-ensure.ts
@@ -7672,11 +7884,11 @@ var DEFAULT_READINESS_TIMEOUT_MS = 12e4;
7672
7884
  var POLL_INTERVAL_MS = 1e3;
7673
7885
  function assertOverlay(container, repoPath) {
7674
7886
  const workingDir = container.labels["com.docker.compose.project.working_dir"];
7675
- if (!workingDir || !sameWorkspacePath(workingDir, import_node_path26.default.join(repoPath, ".devcontainer"))) {
7887
+ if (!workingDir || !sameWorkspacePath(workingDir, import_node_path27.default.join(repoPath, ".devcontainer"))) {
7676
7888
  throw new Error(`Container '${container.id}' does not belong to the exact worktree.`);
7677
7889
  }
7678
7890
  const configFiles = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").filter(Boolean);
7679
- const expectedOverlay = import_node_path26.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
7891
+ const expectedOverlay = import_node_path27.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
7680
7892
  if (!configFiles.some((configFile) => sameWorkspacePath(configFile, expectedOverlay))) {
7681
7893
  throw new Error(`Container '${container.id}' was not started with ${DEVCONTAINER_OVERLAY}.`);
7682
7894
  }
@@ -7871,7 +8083,7 @@ function hasExactManagedComposeProject(repoPath, state) {
7871
8083
  }).some((container) => {
7872
8084
  const workingDir = container.labels["com.docker.compose.project.working_dir"];
7873
8085
  return Boolean(
7874
- workingDir && sameWorkspacePath(workingDir, import_node_path26.default.join(repoPath, ".devcontainer"))
8086
+ workingDir && sameWorkspacePath(workingDir, import_node_path27.default.join(repoPath, ".devcontainer"))
7875
8087
  );
7876
8088
  });
7877
8089
  } catch {
@@ -8255,7 +8467,7 @@ function resolvePrimaryTarget(repoPath) {
8255
8467
  }
8256
8468
  function isPrimaryCheckout(repoPath) {
8257
8469
  try {
8258
- return import_node_fs22.default.statSync(import_node_path26.default.join(repoPath, ".git")).isDirectory();
8470
+ return import_node_fs23.default.statSync(import_node_path27.default.join(repoPath, ".git")).isDirectory();
8259
8471
  } catch {
8260
8472
  return false;
8261
8473
  }
@@ -8316,8 +8528,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
8316
8528
  const target = options.repair ? resolveRepairTarget(repoPath, linked) : linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
8317
8529
  let devpodId = target.devpodId;
8318
8530
  if (target.kind === "linked") {
8319
- const overlayPath = import_node_path26.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
8320
- if (!import_node_fs22.default.existsSync(overlayPath)) {
8531
+ const overlayPath = import_node_path27.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
8532
+ if (!import_node_fs23.default.existsSync(overlayPath)) {
8321
8533
  throw new Error(`Missing required DevPod compose overlay: ${overlayPath}`);
8322
8534
  }
8323
8535
  }
@@ -8387,7 +8599,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
8387
8599
  }
8388
8600
  const apps = proxyAppsFromConfig(runtime.config);
8389
8601
  const parsedUpstreams = apps.map((app) => parseUpstream(app.upstream));
8390
- const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path26.default.basename(repoPath)) ?? "app";
8602
+ const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path27.default.basename(repoPath)) ?? "app";
8391
8603
  for (const [index, app] of apps.entries()) {
8392
8604
  if (!parsedUpstreams[index].host.startsWith(`${aliasPrefix}-`)) {
8393
8605
  const owner = target.kind === "linked" ? "workspace" : "checkout";
@@ -9157,9 +9369,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
9157
9369
  }
9158
9370
  const repairLocked = () => {
9159
9371
  const runtime = resolveWorkspaceRuntimeOrDefault(repoPath);
9160
- import_node_fs22.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
9372
+ import_node_fs23.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
9161
9373
  return withFileLock(
9162
- import_node_path26.default.join(DEVROUTER_HOME, `${runtime}-mutation.lock`),
9374
+ import_node_path27.default.join(DEVROUTER_HOME, `${runtime}-mutation.lock`),
9163
9375
  {
9164
9376
  activity: "Managed runtime repair",
9165
9377
  target: repoPath,
@@ -9230,6 +9442,7 @@ var cancelled = false;
9230
9442
  var lockHeld = false;
9231
9443
  var stopProjects = [];
9232
9444
  var stopBaselineState;
9445
+ var BUSY_EXEC_WAIT_MS = 30 * 60 * 1e3;
9233
9446
  function matchesFence(record4, fence) {
9234
9447
  return Object.entries(reliabilityFence(record4.state)).every(
9235
9448
  ([key, value]) => value === fence[key]
@@ -9347,14 +9560,14 @@ async function executeLifecycleWorker(request, operation) {
9347
9560
  withMutationLock(
9348
9561
  "Verify retained stop",
9349
9562
  request.repoPath,
9350
- () => proveRetainedManagedStop(retained)
9563
+ () => proveManagedStop(retained)
9351
9564
  );
9352
9565
  } else {
9353
9566
  const containers = inspectWorkspaceContainers();
9354
9567
  const owned = containers.filter(
9355
9568
  (container) => sameWorkspacePath(
9356
9569
  container.labels["com.docker.compose.project.working_dir"] ?? "",
9357
- import_node_path27.default.join(request.repoPath, ".devcontainer")
9570
+ import_node_path28.default.join(request.repoPath, ".devcontainer")
9358
9571
  )
9359
9572
  );
9360
9573
  const projects = owned.map((container) => container.labels["com.docker.compose.project"]);
@@ -9428,9 +9641,11 @@ function proveLifecycleStopped() {
9428
9641
  const settle = () => {
9429
9642
  claimLifecycleEffect();
9430
9643
  if (stopBaselineState) {
9431
- if (proveRetainedManagedStop(stopBaselineState).some((container) => container.state.Running)) {
9644
+ const proof = proveManagedStop(stopBaselineState);
9645
+ if (proof.containers.some((container) => container.state.Running))
9432
9646
  throw new Error("Retained workspace workloads remain running.");
9433
- }
9647
+ if (proof.status === "proven-absent")
9648
+ assertTraefikRoutesRemoved(managedStopRouteReferences(stopBaselineState));
9434
9649
  } else {
9435
9650
  for (const project of stopProjects) {
9436
9651
  const containers = inspectManagedStopContainers(project);
@@ -9440,7 +9655,7 @@ function proveLifecycleStopped() {
9440
9655
  const remaining = inspectWorkspaceContainers().filter(
9441
9656
  (container) => sameWorkspacePath(
9442
9657
  container.labels["com.docker.compose.project.working_dir"] ?? "",
9443
- import_node_path27.default.join(request.repoPath, ".devcontainer")
9658
+ import_node_path28.default.join(request.repoPath, ".devcontainer")
9444
9659
  )
9445
9660
  );
9446
9661
  if (remaining.some(
@@ -9476,7 +9691,7 @@ function ensureGuidance(repoPath) {
9476
9691
  function errorMessage(error) {
9477
9692
  return error instanceof Error ? error.message : String(error);
9478
9693
  }
9479
- async function devsyExecOutcome(repoPath, command) {
9694
+ async function devsyExecOutcome(repoPath, command, retainedProof) {
9480
9695
  if (command.length === 0) {
9481
9696
  throw new Error("No command provided. Use `devrouter exec [path] -- <command...>`.");
9482
9697
  }
@@ -9487,12 +9702,25 @@ async function devsyExecOutcome(repoPath, command) {
9487
9702
  `No exact Devsy workspace exists for '${repoPath}'. ${ensureGuidance(repoPath)}`
9488
9703
  );
9489
9704
  }
9490
- const args = ["workspace", "exec", "--result-format", "plain", workspace.id, "--", ...command];
9705
+ const args = [
9706
+ "workspace",
9707
+ "exec",
9708
+ "--result-format",
9709
+ "plain",
9710
+ ...retainedProof ? ["--context", retainedProof.context] : [],
9711
+ retainedProof?.id ?? workspace.id,
9712
+ "--",
9713
+ ...command
9714
+ ];
9491
9715
  return new Promise((resolve, reject) => {
9492
9716
  let child;
9493
9717
  try {
9494
- claimLifecycleEffect();
9495
- child = (0, import_node_child_process22.spawn)("devsy", args, { stdio: "inherit" });
9718
+ const launch = () => {
9719
+ if (retainedProof) revalidateDevsyExecProof(repoPath, retainedProof);
9720
+ claimLifecycleEffect();
9721
+ return (0, import_node_child_process22.spawn)("devsy", args, { stdio: "inherit" });
9722
+ };
9723
+ child = retainedProof ? withMutationLock("Execute retained workspace", repoPath, launch) : launch();
9496
9724
  } catch (error) {
9497
9725
  reject(
9498
9726
  new ExecutionOutcomeError(`devsy exec failed: ${errorMessage(error)}`, {
@@ -9775,11 +10003,14 @@ async function devpodExecOutcome(repoPath, command) {
9775
10003
 
9776
10004
  // src/core/workspace-lifecycle.ts
9777
10005
  var import_node_child_process24 = require("child_process");
9778
- var import_node_fs24 = __toESM(require("fs"));
9779
- var import_node_path28 = __toESM(require("path"));
10006
+ var import_node_fs25 = __toESM(require("fs"));
10007
+ var import_node_path29 = __toESM(require("path"));
9780
10008
 
9781
10009
  // src/core/route-state.ts
9782
- var import_node_fs23 = __toESM(require("fs"));
10010
+ var import_node_fs24 = __toESM(require("fs"));
10011
+ function listRoutesForWorktreePath(worktreePath, routes = listHostRouteState()) {
10012
+ return routes.filter((route) => sameWorkspacePath(route.repoPath, worktreePath));
10013
+ }
9783
10014
  function removeWorkspaceRoutesForWorktree(workspace, worktreePath) {
9784
10015
  return removeHostRoutesWhere(
9785
10016
  (route) => route.workspace === workspace && sameWorkspacePath(route.repoPath, worktreePath)
@@ -9831,8 +10062,24 @@ async function mutateWorkspaceRuntime(action, resolved, worktrees, quiet = false
9831
10062
  "Retained managed runtime has no exact provider registration; routes were preserved."
9832
10063
  );
9833
10064
  }
9834
- const routes = removeWorkspaceRoutesForWorktree(resolved.workspace, resolved.worktreePath);
9835
- await ensureTraefikRoutesRemoved(routes);
10065
+ const retained = action === "stop" ? readManagedRuntimeState(resolved.worktreePath, resolved.workspace) : void 0;
10066
+ let absent = false;
10067
+ const remove = () => {
10068
+ if (retained?.stopBaseline) {
10069
+ absent = proveManagedStop(retained).status === "proven-absent";
10070
+ if (absent && listRoutesForWorktreePath(resolved.worktreePath).some(
10071
+ (route) => route.workspace !== resolved.workspace || !retained.desired.apps.includes(route.name)
10072
+ ))
10073
+ throw new Error("Absent stop contains routes outside the retained desired set.");
10074
+ }
10075
+ claimLifecycleEffect();
10076
+ return removeWorkspaceRoutesForWorktree(resolved.workspace, resolved.worktreePath);
10077
+ };
10078
+ const routes = retained?.stopBaseline ? withMutationLock("Revalidate stop routes", resolved.worktreePath, remove) : remove();
10079
+ await ensureTraefikRoutesRemoved([
10080
+ ...routes,
10081
+ ...absent && retained ? managedStopRouteReferences(retained) : []
10082
+ ]);
9836
10083
  if (!quiet) {
9837
10084
  process.stdout.write(
9838
10085
  `Freed ${routes.length} route(s) for workspace '${resolved.workspace}'.
@@ -9843,6 +10090,7 @@ async function mutateWorkspaceRuntime(action, resolved, worktrees, quiet = false
9843
10090
  ...mutation.status === "changed" ? { devpodId } : {},
9844
10091
  freedRoutes: routes.length,
9845
10092
  providerChanged: mutation.status === "changed",
10093
+ ...absent ? { runtimeAbsent: true } : {},
9846
10094
  workspace: resolved.workspace
9847
10095
  };
9848
10096
  }
@@ -9906,7 +10154,7 @@ async function environmentStop(repoPath, options = {}) {
9906
10154
  repoPath,
9907
10155
  workspace: result.workspace,
9908
10156
  ...result.devpodId ? { devpodId: result.devpodId } : {},
9909
- stopped: !options.delete && result.providerChanged,
10157
+ stopped: !options.delete && (result.providerChanged || result.runtimeAbsent === true),
9910
10158
  ...options.delete ? { deleted: result.providerChanged } : {},
9911
10159
  freedRoutes: result.freedRoutes
9912
10160
  };
@@ -9939,10 +10187,16 @@ async function environmentStop(repoPath, options = {}) {
9939
10187
  }
9940
10188
 
9941
10189
  // src/lifecycle-worker.ts
9942
- process.on("SIGTERM", cancelLifecycleWorker);
9943
- process.on("SIGINT", cancelLifecycleWorker);
9944
- process.on("disconnect", cancelLifecycleWorker);
10190
+ var started = false;
10191
+ function cancel() {
10192
+ cancelLifecycleWorker();
10193
+ if (!started && process.connected) process.disconnect();
10194
+ }
10195
+ process.on("SIGTERM", cancel);
10196
+ process.on("SIGINT", cancel);
10197
+ process.on("disconnect", cancel);
9945
10198
  process.once("message", async (message) => {
10199
+ started = true;
9946
10200
  let result;
9947
10201
  try {
9948
10202
  const request = message.request;
@@ -9953,7 +10207,11 @@ process.once("message", async (message) => {
9953
10207
  return stopped;
9954
10208
  }
9955
10209
  if (request.kind === "exec") {
9956
- const outcome = await devpodExecOutcome(request.repoPath, request.command ?? []);
10210
+ const outcome = request.retainedExecProof ? await devsyExecOutcome(
10211
+ request.repoPath,
10212
+ request.command ?? [],
10213
+ request.retainedExecProof
10214
+ ) : await devpodExecOutcome(request.repoPath, request.command ?? []);
9957
10215
  recordLifecycleOutcome(outcome);
9958
10216
  return outcome;
9959
10217
  }