@devrouter/cli 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,7 +23,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  ));
24
24
 
25
25
  // src/core/devpod-exec.ts
26
- var import_node_child_process29 = require("child_process");
26
+ var import_node_child_process32 = require("child_process");
27
27
  var import_node_crypto23 = require("crypto");
28
28
 
29
29
  // src/core/devpod-environment.ts
@@ -1164,7 +1164,11 @@ function listDevpodWorkspacesRaw(options = {}) {
1164
1164
  }
1165
1165
  if (result.error || result.status !== 0) {
1166
1166
  const details = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
1167
- throw new Error(`devpod list failed: ${details || "devpod is not installed or unavailable"}`);
1167
+ const failure = new Error(
1168
+ `devpod list failed: ${details || "devpod is not installed or unavailable"}`
1169
+ );
1170
+ if (result.error) failure.code = result.error.code;
1171
+ throw failure;
1168
1172
  }
1169
1173
  let parsed;
1170
1174
  try {
@@ -1303,9 +1307,11 @@ function listDevsyWorkspaces() {
1303
1307
  );
1304
1308
  if (result.status !== 0) {
1305
1309
  const details = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
1306
- throw new Error(
1310
+ const failure = new Error(
1307
1311
  `devsy workspace list failed: ${details || "devsy is not installed or unavailable"}`
1308
1312
  );
1313
+ if (result.error) failure.code = result.error.code;
1314
+ throw failure;
1309
1315
  }
1310
1316
  let parsed;
1311
1317
  try {
@@ -1775,25 +1781,23 @@ function readWorkspaceRuntimeConfig() {
1775
1781
  }
1776
1782
  return config;
1777
1783
  }
1784
+ function readRegistrySnapshot(runtime, read2, snapshots) {
1785
+ try {
1786
+ return read2();
1787
+ } catch (error) {
1788
+ if (error?.code === "ENOENT") return void 0;
1789
+ snapshots.unavailable.push(runtime);
1790
+ snapshots.errors[runtime] = error instanceof Error ? error.message : String(error);
1791
+ return void 0;
1792
+ }
1793
+ }
1778
1794
  function getWorkspaceRegistrySnapshots() {
1779
1795
  if (cachedSnapshots) return cachedSnapshots;
1780
1796
  const snapshots = { unavailable: [], errors: {} };
1781
- if (isRuntimeInstalled("devsy")) {
1782
- try {
1783
- snapshots.devsy = listDevsyWorkspaces();
1784
- } catch (error) {
1785
- snapshots.unavailable.push("devsy");
1786
- snapshots.errors.devsy = error instanceof Error ? error.message : String(error);
1787
- }
1788
- }
1789
- if (isRuntimeInstalled("devpod")) {
1790
- try {
1791
- snapshots.devpod = listDevpodWorkspacesRaw();
1792
- } catch (error) {
1793
- snapshots.unavailable.push("devpod");
1794
- snapshots.errors.devpod = error instanceof Error ? error.message : String(error);
1795
- }
1796
- }
1797
+ const devsy = readRegistrySnapshot("devsy", listDevsyWorkspaces, snapshots);
1798
+ if (devsy !== void 0) snapshots.devsy = devsy;
1799
+ const devpod = readRegistrySnapshot("devpod", listDevpodWorkspacesRaw, snapshots);
1800
+ if (devpod !== void 0) snapshots.devpod = devpod;
1797
1801
  cachedSnapshots = snapshots;
1798
1802
  return snapshots;
1799
1803
  }
@@ -1954,7 +1958,7 @@ function selectDevpodWorkspace(workspaces, repoPath) {
1954
1958
  }
1955
1959
 
1956
1960
  // src/core/devsy-exec.ts
1957
- var import_node_child_process28 = require("child_process");
1961
+ var import_node_child_process31 = require("child_process");
1958
1962
 
1959
1963
  // src/core/devsy-exec-proof.ts
1960
1964
  var import_node_fs7 = __toESM(require("fs"));
@@ -2042,9 +2046,9 @@ function revalidateDevsyExecProof(repoPath, proof) {
2042
2046
  }
2043
2047
 
2044
2048
  // src/core/devsy-mutation.ts
2045
- var import_node_child_process16 = require("child_process");
2046
- var import_node_fs25 = __toESM(require("fs"));
2047
- var import_node_path29 = __toESM(require("path"));
2049
+ var import_node_child_process18 = require("child_process");
2050
+ var import_node_fs26 = __toESM(require("fs"));
2051
+ var import_node_path30 = __toESM(require("path"));
2048
2052
 
2049
2053
  // src/core/devsy-agent.ts
2050
2054
  var import_node_child_process9 = require("child_process");
@@ -2320,9 +2324,9 @@ function requireReadyDevsyAgent(options = {}) {
2320
2324
  }
2321
2325
 
2322
2326
  // src/core/managed-devsy-stop.ts
2323
- var import_node_fs21 = __toESM(require("fs"));
2327
+ var import_node_fs22 = __toESM(require("fs"));
2324
2328
  var import_node_os5 = __toESM(require("os"));
2325
- var import_node_path25 = __toESM(require("path"));
2329
+ var import_node_path26 = __toESM(require("path"));
2326
2330
  var import_node_util5 = require("util");
2327
2331
 
2328
2332
  // src/core/devcontainer-profile.ts
@@ -4040,11 +4044,19 @@ function absentRegistrationIdentity(state) {
4040
4044
  resetWorkspaceRuntimeCaches();
4041
4045
  if (resolveWorkspaceRuntimeOrDefault(state.repoPath) !== "devsy")
4042
4046
  throw new Error("Stop provider changed.");
4043
- if (!state.workspace || !isLinkedWorktree(state.repoPath))
4044
- throw new Error("Absent stop requires a linked workspace.");
4045
- const record5 = readWorkspaceOwnership(state.repoPath, state.workspace);
4046
- if (!record5 || record5.workspace !== state.workspace || record5.devpodId !== state.devpodId || !sameWorkspacePath(record5.worktreePath, state.repoPath) || resolveWorktreeWorkspace(state.repoPath) !== state.workspace || inspectWorkspaceOwnership(record5, listGitWorktrees(state.repoPath), void 0).ownerStatus !== "present")
4047
- throw new Error("Absent stop workspace ownership changed.");
4047
+ const linked = isLinkedWorktree(state.repoPath);
4048
+ if (linked !== (state.workspace !== void 0))
4049
+ throw new Error("Absent stop requires the exact workspace identity.");
4050
+ let identity2;
4051
+ if (linked) {
4052
+ const workspace = state.workspace;
4053
+ const record5 = readWorkspaceOwnership(state.repoPath, workspace);
4054
+ if (!record5 || record5.workspace !== workspace || record5.devpodId !== state.devpodId || !sameWorkspacePath(record5.worktreePath, state.repoPath) || resolveWorktreeWorkspace(state.repoPath) !== workspace || inspectWorkspaceOwnership(record5, listGitWorktrees(state.repoPath), void 0).ownerStatus !== "present")
4055
+ throw new Error("Absent stop workspace ownership changed.");
4056
+ identity2 = { kind: "linked", record: record5, gitCommonDir: resolveGitCommonDir(state.repoPath) };
4057
+ } else {
4058
+ identity2 = { kind: "primary", repoPath: state.repoPath, devpodId: state.devpodId };
4059
+ }
4048
4060
  for (const entries of [
4049
4061
  listDevsyWorkspaces(),
4050
4062
  listDevpodWorkspacesRaw({ allowMissingExecutable: true })
@@ -4054,7 +4066,7 @@ function absentRegistrationIdentity(state) {
4054
4066
  ))
4055
4067
  throw new Error("Absent stop requires both provider registrations to remain absent.");
4056
4068
  }
4057
- return { record: record5, gitCommonDir: resolveGitCommonDir(state.repoPath) };
4069
+ return identity2;
4058
4070
  }
4059
4071
  function replacementObservation(state) {
4060
4072
  resetWorkspaceRuntimeCaches();
@@ -4164,7 +4176,7 @@ function proveManagedStop(state) {
4164
4176
  const identityKeys = ["context", "providerId", "uid", "sourcePath", "sourceContainer"];
4165
4177
  const current = identityOf(owner.workspace);
4166
4178
  const changed = identityKeys.filter((key) => baseline[key] !== current[key]);
4167
- if (changed.length === 0 && state.workspace !== void 0 && isLinkedWorktree(state.repoPath) && inspectManagedStopContainers(baseline.project, baseline.endpoint).length === 0) {
4179
+ if (changed.length === 0 && inspectManagedStopContainers(baseline.project, baseline.endpoint).length === 0) {
4168
4180
  proveAbsentPopulationStop(state, baseline, current);
4169
4181
  return { status: "proven-absent", containers: [] };
4170
4182
  }
@@ -5361,7 +5373,7 @@ function retain(session, previous) {
5361
5373
 
5362
5374
  // src/core/reliability-operation-store.ts
5363
5375
  function installedCliVersion() {
5364
- return true ? "0.1.2" : "0.0.0-dev";
5376
+ return true ? "0.2.0" : "0.0.0-dev";
5365
5377
  }
5366
5378
  function compareCliVersions(left, right) {
5367
5379
  const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
@@ -5501,6 +5513,7 @@ function validate2(record5, identity2) {
5501
5513
  "revision",
5502
5514
  "state",
5503
5515
  "worker",
5516
+ "stopWorkStarted",
5504
5517
  "effectSequence",
5505
5518
  "outcome",
5506
5519
  "result",
@@ -5682,6 +5695,9 @@ function validate2(record5, identity2) {
5682
5695
  throw new Error("Reliability worker identity is invalid.");
5683
5696
  }
5684
5697
  }
5698
+ if (record5.stopWorkStarted !== void 0 && record5.stopWorkStarted !== null) {
5699
+ if (record5.stopWorkStarted !== true) throw new Error("Invalid stop work marker.");
5700
+ }
5685
5701
  }
5686
5702
  var ReliabilityJournalError = class extends Error {
5687
5703
  constructor(cause, location, message) {
@@ -6018,8 +6034,8 @@ function clearStartupWitness(identity2, fence) {
6018
6034
 
6019
6035
  // src/core/repo-config.ts
6020
6036
  var import_node_crypto10 = require("crypto");
6021
- var import_node_fs18 = __toESM(require("fs"));
6022
- var import_node_path22 = __toESM(require("path"));
6037
+ var import_node_fs19 = __toESM(require("fs"));
6038
+ var import_node_path23 = __toESM(require("path"));
6023
6039
  var import_yaml3 = __toESM(require("yaml"));
6024
6040
 
6025
6041
  // src/core/capabilities.ts
@@ -6044,6 +6060,44 @@ function formatSupportedTcpProtocols() {
6044
6060
  return SUPPORTED_TCP_PROTOCOLS.join(", ");
6045
6061
  }
6046
6062
 
6063
+ // src/core/worktree-safety.ts
6064
+ var import_node_child_process13 = require("child_process");
6065
+ var import_node_fs18 = __toESM(require("fs"));
6066
+ var import_node_path22 = __toESM(require("path"));
6067
+
6068
+ // src/core/forge-changes.ts
6069
+ var import_node_child_process12 = require("child_process");
6070
+
6071
+ // src/core/worktree-safety.ts
6072
+ var WORKTREE_LOCAL_SAFETY_CODES = [
6073
+ "git-error",
6074
+ "detached",
6075
+ "index-lock",
6076
+ "operation-in-progress",
6077
+ "submodule",
6078
+ "dirty",
6079
+ "untracked",
6080
+ "ignored-state"
6081
+ ];
6082
+ var WORKTREE_SAFETY_CODES = [
6083
+ ...WORKTREE_LOCAL_SAFETY_CODES,
6084
+ "primary",
6085
+ "locked",
6086
+ "runtime-present",
6087
+ "forge-not-checked",
6088
+ "forge-unavailable",
6089
+ "no-change",
6090
+ "change-open",
6091
+ "change-closed",
6092
+ "head-beyond-merge",
6093
+ "active"
6094
+ ];
6095
+ var DISPOSABLE_OVERRIDE_FILE = import_node_path22.default.join("info", "worktree-reclaim-disposable");
6096
+ function isValidWorktreePattern(pattern) {
6097
+ if (!pattern || pattern.startsWith("/") || pattern.includes("\\")) return false;
6098
+ return pattern.split("/").every((segment) => segment && segment !== "." && segment !== "..");
6099
+ }
6100
+
6047
6101
  // src/core/repo-config.ts
6048
6102
  function compareSemver2(a, b) {
6049
6103
  const parse2 = (v) => {
@@ -6878,6 +6932,7 @@ function parseConfig(raw, configPath) {
6878
6932
  "managedRuntime",
6879
6933
  "profiles",
6880
6934
  "capacity",
6935
+ "worktrees",
6881
6936
  "apps"
6882
6937
  ],
6883
6938
  configPath
@@ -6952,6 +7007,7 @@ function parseConfig(raw, configPath) {
6952
7007
  const managedRuntime = parseManagedRuntime(root.managedRuntime, configPath);
6953
7008
  const profiles = parseProfiles(root.profiles, configPath, apps, managedRuntime);
6954
7009
  const capacity = parseCapacityEstimates(root.capacity, configPath, profiles);
7010
+ const worktrees = parseWorktreesConfig(root.worktrees, configPath);
6955
7011
  return {
6956
7012
  version: 1,
6957
7013
  ...devrouter ? { devrouter } : {},
@@ -6960,9 +7016,30 @@ function parseConfig(raw, configPath) {
6960
7016
  ...managedRuntime ? { managedRuntime } : {},
6961
7017
  ...profiles ? { profiles } : {},
6962
7018
  ...capacity ? { capacity } : {},
7019
+ ...worktrees ? { worktrees } : {},
6963
7020
  apps
6964
7021
  };
6965
7022
  }
7023
+ function parseWorktreesConfig(value, configPath) {
7024
+ if (value === void 0) return void 0;
7025
+ const raw = ensureObject(value, `${configPath}.worktrees`);
7026
+ ensureAllowedKeys(raw, ["disposable", "trim"], `${configPath}.worktrees`);
7027
+ const result = {};
7028
+ for (const key of ["disposable", "trim"]) {
7029
+ if (raw[key] === void 0) continue;
7030
+ const label = `${configPath}.worktrees.${key}`;
7031
+ if (!Array.isArray(raw[key])) throw new Error(`${label} must be an array of patterns.`);
7032
+ result[key] = raw[key].map((pattern, index) => {
7033
+ if (typeof pattern !== "string" || !isValidWorktreePattern(pattern)) {
7034
+ throw new Error(
7035
+ `${label}[${index}] must be a worktree-relative pattern without '..' or a leading '/'.`
7036
+ );
7037
+ }
7038
+ return pattern;
7039
+ });
7040
+ }
7041
+ return result;
7042
+ }
6966
7043
  var PROFILE_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
6967
7044
  var MAX_PROFILES = 32;
6968
7045
  function parseProfiles(value, configPath, apps, managedRuntime) {
@@ -7276,15 +7353,15 @@ function applyProfile(config, profile) {
7276
7353
  return next;
7277
7354
  }
7278
7355
  function resolveRepoPath(repoPath) {
7279
- return import_node_path22.default.resolve(repoPath ?? process.cwd());
7356
+ return import_node_path23.default.resolve(repoPath ?? process.cwd());
7280
7357
  }
7281
7358
  function getRepoConfigPath(repoPath) {
7282
- return import_node_path22.default.join(resolveRepoPath(repoPath), CONFIG_FILE_NAME);
7359
+ return import_node_path23.default.join(resolveRepoPath(repoPath), CONFIG_FILE_NAME);
7283
7360
  }
7284
- function loadRepoConfig(repoPath, read2 = (file) => import_node_fs18.default.readFileSync(file, "utf-8")) {
7361
+ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs19.default.readFileSync(file, "utf-8")) {
7285
7362
  const resolvedRepoPath = resolveRepoPath(repoPath);
7286
7363
  const configPath = getRepoConfigPath(resolvedRepoPath);
7287
- if (!import_node_fs18.default.existsSync(configPath)) {
7364
+ if (!import_node_fs19.default.existsSync(configPath)) {
7288
7365
  throw new Error(
7289
7366
  `Missing ${CONFIG_FILE_NAME} in ${resolvedRepoPath}. Run 'dev repo init --repo ${resolvedRepoPath}' first.`
7290
7367
  );
@@ -7294,7 +7371,7 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs18.default.rea
7294
7371
  const config = parseConfig(parsed ?? {}, configPath);
7295
7372
  const requiredVersion = config.devrouter?.version;
7296
7373
  if (requiredVersion && !hasWarnedVersionMismatch) {
7297
- const cliVersion = true ? "0.1.2" : "0.0.0-dev";
7374
+ const cliVersion = true ? "0.2.0" : "0.0.0-dev";
7298
7375
  if (cliVersion !== "0.0.0-dev" && compareSemver2(requiredVersion, cliVersion) > 0) {
7299
7376
  hasWarnedVersionMismatch = true;
7300
7377
  process.stderr.write(
@@ -7314,7 +7391,7 @@ function namespaceHost(host, workspace) {
7314
7391
  return `${base}.${workspace}${suffix}`;
7315
7392
  }
7316
7393
  function applyWorkspace(config, workspace, repoPath) {
7317
- const defaultToken = wsFromBranch(config.project?.name ?? import_node_path22.default.basename(import_node_path22.default.resolve(repoPath))) ?? "app";
7394
+ const defaultToken = wsFromBranch(config.project?.name ?? import_node_path23.default.basename(import_node_path23.default.resolve(repoPath))) ?? "app";
7318
7395
  const substitutionToken = workspace ?? defaultToken;
7319
7396
  const next = structuredClone(config);
7320
7397
  for (const app of next.apps) {
@@ -7387,7 +7464,7 @@ function resolveAppDependencies(config, app) {
7387
7464
  }
7388
7465
 
7389
7466
  // src/core/docker.ts
7390
- var import_node_child_process12 = require("child_process");
7467
+ var import_node_child_process14 = require("child_process");
7391
7468
  var dockerodeConstructorPromise = null;
7392
7469
  async function getDockerodeConstructor() {
7393
7470
  if (!dockerodeConstructorPromise) {
@@ -7399,7 +7476,7 @@ async function getDockerodeConstructor() {
7399
7476
  return dockerodeConstructorPromise;
7400
7477
  }
7401
7478
  function runDockerContextCommand(args) {
7402
- const result = (0, import_node_child_process12.spawnSync)("docker", ["context", ...args], {
7479
+ const result = (0, import_node_child_process14.spawnSync)("docker", ["context", ...args], {
7403
7480
  encoding: "utf-8"
7404
7481
  });
7405
7482
  if (result.status !== 0) {
@@ -7412,7 +7489,7 @@ function getCurrentDockerContext() {
7412
7489
  return runDockerContextCommand(["show"]);
7413
7490
  }
7414
7491
  function getDockerHostFromContext(context) {
7415
- const result = (0, import_node_child_process12.spawnSync)(
7492
+ const result = (0, import_node_child_process14.spawnSync)(
7416
7493
  "docker",
7417
7494
  ["context", "inspect", context, "--format", "{{ .Endpoints.docker.Host }}"],
7418
7495
  { encoding: "utf-8" }
@@ -7472,16 +7549,16 @@ async function ensureNetwork(name) {
7472
7549
  }
7473
7550
 
7474
7551
  // src/core/tls.ts
7475
- var import_node_child_process13 = require("child_process");
7552
+ var import_node_child_process15 = require("child_process");
7476
7553
  var import_node_crypto11 = require("crypto");
7477
- var import_node_fs19 = __toESM(require("fs"));
7478
- var import_node_path23 = __toESM(require("path"));
7554
+ var import_node_fs20 = __toESM(require("fs"));
7555
+ var import_node_path24 = __toESM(require("path"));
7479
7556
  var DEFAULT_TLS_CERT_HOSTS = ["localhost", "*.localhost"];
7480
- var TLS_CERTIFICATE_LOCK_FILE = import_node_path23.default.join(DEVROUTER_HOME, "tls-certificate.lock");
7557
+ var TLS_CERTIFICATE_LOCK_FILE = import_node_path24.default.join(DEVROUTER_HOME, "tls-certificate.lock");
7481
7558
  var TLS_CERTIFICATE_LOCK_WAIT_MS = 6e4;
7482
7559
  var TLS_CERTIFICATE_WRITE_ATTEMPTS = 2;
7483
7560
  function runOrThrow(command, args) {
7484
- const result = (0, import_node_child_process13.spawnSync)(command, args, {
7561
+ const result = (0, import_node_child_process15.spawnSync)(command, args, {
7485
7562
  encoding: "utf-8"
7486
7563
  });
7487
7564
  if (result.status !== 0) {
@@ -7491,7 +7568,7 @@ function runOrThrow(command, args) {
7491
7568
  return result.stdout.trim();
7492
7569
  }
7493
7570
  function commandExists(command) {
7494
- const result = (0, import_node_child_process13.spawnSync)("sh", ["-c", `command -v ${command}`], { encoding: "utf-8" });
7571
+ const result = (0, import_node_child_process15.spawnSync)("sh", ["-c", `command -v ${command}`], { encoding: "utf-8" });
7495
7572
  return result.status === 0;
7496
7573
  }
7497
7574
  function ensureMkcert() {
@@ -7509,8 +7586,8 @@ function tlsSetupCommand(repoPath) {
7509
7586
  }
7510
7587
  function getMkcertRootCAPath(options = {}) {
7511
7588
  ensureMkcert();
7512
- const rootCAPath = import_node_path23.default.join(runOrThrow("mkcert", ["-CAROOT"]), "rootCA.pem");
7513
- if (!import_node_fs19.default.existsSync(rootCAPath)) {
7589
+ const rootCAPath = import_node_path24.default.join(runOrThrow("mkcert", ["-CAROOT"]), "rootCA.pem");
7590
+ if (!import_node_fs20.default.existsSync(rootCAPath)) {
7514
7591
  throw new Error(
7515
7592
  `mkcert root CA was not found at '${rootCAPath}'. Run: ${tlsSetupCommand(options.repoPath)}`
7516
7593
  );
@@ -7611,11 +7688,11 @@ function compactTLSCertificateHosts(hosts) {
7611
7688
  );
7612
7689
  }
7613
7690
  function readCurrentCertificateHosts(options = {}) {
7614
- if (!import_node_fs19.default.existsSync(CERT_FILE)) {
7691
+ if (!import_node_fs20.default.existsSync(CERT_FILE)) {
7615
7692
  return [];
7616
7693
  }
7617
7694
  try {
7618
- const pem = import_node_fs19.default.readFileSync(CERT_FILE, "utf-8");
7695
+ const pem = import_node_fs20.default.readFileSync(CERT_FILE, "utf-8");
7619
7696
  return parseCertificateDnsHosts(pem);
7620
7697
  } catch (error) {
7621
7698
  if (options.replaceMalformed) {
@@ -7807,11 +7884,11 @@ async function replacePublishedProxyRoutes(repoPath, config, workspace, options
7807
7884
  }
7808
7885
 
7809
7886
  // src/core/traefik-route-health.ts
7810
- var import_node_child_process14 = require("child_process");
7811
- var import_node_fs20 = __toESM(require("fs"));
7812
- var import_node_path24 = __toESM(require("path"));
7887
+ var import_node_child_process16 = require("child_process");
7888
+ var import_node_fs21 = __toESM(require("fs"));
7889
+ var import_node_path25 = __toESM(require("path"));
7813
7890
  var ROUTER_API_BASE = "http://127.0.0.1:8080/api";
7814
- var ROUTER_RELOAD_LOCK_FILE = import_node_path24.default.join(DEVROUTER_HOME, "router-reload.lock");
7891
+ var ROUTER_RELOAD_LOCK_FILE = import_node_path25.default.join(DEVROUTER_HOME, "router-reload.lock");
7815
7892
  var ROUTER_API_REQUEST_TIMEOUT_SECONDS = "2";
7816
7893
  var DEFAULT_INITIAL_TIMEOUT_MS = 3e3;
7817
7894
  var DEFAULT_RECOVERY_TIMEOUT_MS = 1e4;
@@ -7821,7 +7898,7 @@ function sleep(ms) {
7821
7898
  return new Promise((resolve) => setTimeout(resolve, ms));
7822
7899
  }
7823
7900
  function inspectRouterApi(protocol) {
7824
- const result = (0, import_node_child_process14.spawnSync)(
7901
+ const result = (0, import_node_child_process16.spawnSync)(
7825
7902
  "curl",
7826
7903
  [
7827
7904
  "--silent",
@@ -7869,7 +7946,7 @@ function isRecord3(value) {
7869
7946
  return value !== null && typeof value === "object" && !Array.isArray(value);
7870
7947
  }
7871
7948
  function inspectNamedRouterApi(protocol, resource, name) {
7872
- const result = (0, import_node_child_process14.spawnSync)(
7949
+ const result = (0, import_node_child_process16.spawnSync)(
7873
7950
  "curl",
7874
7951
  [
7875
7952
  "--silent",
@@ -8080,7 +8157,7 @@ async function ensureTraefikRouteExpectation(routes, expectation, options) {
8080
8157
  throw new Error(
8081
8158
  "Traefik routes did not reload; shared router restart is disabled for this operation."
8082
8159
  );
8083
- import_node_fs20.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
8160
+ import_node_fs21.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
8084
8161
  return withFileLock(
8085
8162
  ROUTER_RELOAD_LOCK_FILE,
8086
8163
  {
@@ -8146,7 +8223,7 @@ function proveInitialManagedDevsyAbsence(repoPath, expectedId) {
8146
8223
  if (!isLinkedWorktree(repoPath)) return void 0;
8147
8224
  const workspace = resolveWorktreeWorkspace(repoPath);
8148
8225
  if (!workspace || readManagedRuntimeState(repoPath, workspace)) return void 0;
8149
- if (!import_node_fs21.default.existsSync(import_node_path25.default.join(repoPath, ".devrouter.yml"))) return void 0;
8226
+ if (!import_node_fs22.default.existsSync(import_node_path26.default.join(repoPath, ".devrouter.yml"))) return void 0;
8150
8227
  const runtime = loadRuntimeConfig(repoPath, workspace);
8151
8228
  if (!runtime.config.managedRuntime) return void 0;
8152
8229
  const routes = proxyAppsFromConfig(runtime.config).flatMap(
@@ -8206,7 +8283,7 @@ function proveInitialManagedDevsyAbsence(repoPath, expectedId) {
8206
8283
  };
8207
8284
  }
8208
8285
  function stopInitialManagedDevsyWorkspace(repoPath, devsyId) {
8209
- if (!import_node_fs21.default.existsSync(import_node_path25.default.join(repoPath, ".devrouter.yml"))) return false;
8286
+ if (!import_node_fs22.default.existsSync(import_node_path26.default.join(repoPath, ".devrouter.yml"))) return false;
8210
8287
  if (!loadRepoConfig(repoPath).managedRuntime) return false;
8211
8288
  const linked = isLinkedWorktree(repoPath);
8212
8289
  const workspace = linked ? resolveWorktreeWorkspace(repoPath) : void 0;
@@ -8263,8 +8340,8 @@ function stopInitialManagedDevsyWorkspace(repoPath, devsyId) {
8263
8340
  profile: runtime.resolvedProfile,
8264
8341
  linked
8265
8342
  });
8266
- const providerRoot = import_node_path25.default.resolve(process.env.DEVSY_HOME || import_node_path25.default.join(import_node_os5.default.homedir(), ".devsy"));
8267
- const featureDirectory = import_node_path25.default.join(
8343
+ const providerRoot = import_node_path26.default.resolve(process.env.DEVSY_HOME || import_node_path26.default.join(import_node_os5.default.homedir(), ".devsy"));
8344
+ const featureDirectory = import_node_path26.default.join(
8268
8345
  providerRoot,
8269
8346
  "contexts",
8270
8347
  owner.context,
@@ -8372,6 +8449,41 @@ function stopInitialManagedDevsyWorkspace(repoPath, devsyId) {
8372
8449
  throw new Error("Initial managed stop left a workload running.");
8373
8450
  return true;
8374
8451
  }
8452
+ function restoreRecordedManagedDevcontainerConfig(repoPath) {
8453
+ const plan = recordedManagedDevcontainerRestorePlan(repoPath);
8454
+ if (!plan) return false;
8455
+ writeManagedDevcontainerConfig(plan);
8456
+ if (inspectManagedDevcontainerGeneratedConfig(plan).status !== "valid") {
8457
+ throw new Error(
8458
+ `Managed Dev Container path '${plan.generatedPath}' could not be restored from the recorded state.`
8459
+ );
8460
+ }
8461
+ return true;
8462
+ }
8463
+ function recordedManagedDevcontainerRestorePlan(repoPath) {
8464
+ try {
8465
+ if (!import_node_fs22.default.existsSync(import_node_path26.default.join(repoPath, ".devrouter.yml"))) return void 0;
8466
+ const linked = isLinkedWorktree(repoPath);
8467
+ const workspace = linked ? resolveWorktreeWorkspace(repoPath) : void 0;
8468
+ const state = readManagedRuntimeState(repoPath, workspace);
8469
+ if (!state) return void 0;
8470
+ const runtime = loadRuntimeConfig(repoPath, workspace ?? "", state.profile);
8471
+ if (!runtime.config.managedRuntime || runtime.profile !== state.profile || runtime.workspace !== workspace)
8472
+ return void 0;
8473
+ const plan = inspectManagedDevcontainerConfig({
8474
+ repoPath,
8475
+ config: runtime.config,
8476
+ profile: runtime.resolvedProfile,
8477
+ linked
8478
+ });
8479
+ if (inspectManagedDevcontainerGeneratedConfig(plan).status !== "missing") return void 0;
8480
+ if (plan.sourceConfigSha256 !== state.sourceConfigSha256 || plan.effectiveConfigSha256 !== state.effectiveConfigSha256)
8481
+ return void 0;
8482
+ return plan;
8483
+ } catch {
8484
+ return void 0;
8485
+ }
8486
+ }
8375
8487
  function stopRetainedManagedDevsyWorkspace(options) {
8376
8488
  const { repoPath, devsyId } = options;
8377
8489
  const linked = isLinkedWorktree(repoPath);
@@ -8441,7 +8553,7 @@ function stopRetainedManagedDevsyWorkspace(options) {
8441
8553
  const owned = inspectWorkspaceContainers().filter(
8442
8554
  (container) => sameWorkspacePath(
8443
8555
  container.labels["com.docker.compose.project.working_dir"] ?? "",
8444
- import_node_path25.default.join(repoPath, ".devcontainer")
8556
+ import_node_path26.default.join(repoPath, ".devcontainer")
8445
8557
  )
8446
8558
  );
8447
8559
  if (owned.length !== 0) {
@@ -8470,8 +8582,8 @@ function stopRetainedManagedDevsyWorkspace(options) {
8470
8582
  if (!context || !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(context) || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(devsyId)) {
8471
8583
  throw new Error("Managed stop requires a valid exact Devsy context and workspace ID.");
8472
8584
  }
8473
- const devsyRoot = import_node_path25.default.resolve(process.env.DEVSY_HOME || import_node_path25.default.join(import_node_os5.default.homedir(), ".devsy"));
8474
- const featureDirectory = import_node_path25.default.join(
8585
+ const devsyRoot = import_node_path26.default.resolve(process.env.DEVSY_HOME || import_node_path26.default.join(import_node_os5.default.homedir(), ".devsy"));
8586
+ const featureDirectory = import_node_path26.default.join(
8475
8587
  devsyRoot,
8476
8588
  "contexts",
8477
8589
  context,
@@ -8610,13 +8722,13 @@ function networkProviderEnvironment(binding, source) {
8610
8722
 
8611
8723
  // src/core/network-claim-lookup.ts
8612
8724
  var import_node_crypto13 = require("crypto");
8613
- var import_node_fs24 = __toESM(require("fs"));
8614
- var import_node_path28 = __toESM(require("path"));
8725
+ var import_node_fs25 = __toESM(require("fs"));
8726
+ var import_node_path29 = __toESM(require("path"));
8615
8727
 
8616
8728
  // src/core/network-claims.ts
8617
8729
  var import_node_crypto12 = require("crypto");
8618
- var import_node_fs23 = __toESM(require("fs"));
8619
- var import_node_path27 = __toESM(require("path"));
8730
+ var import_node_fs24 = __toESM(require("fs"));
8731
+ var import_node_path28 = __toESM(require("path"));
8620
8732
 
8621
8733
  // src/core/network-capacity.ts
8622
8734
  var import_node_net = require("net");
@@ -8957,10 +9069,10 @@ function overlaps(left, right) {
8957
9069
  }
8958
9070
 
8959
9071
  // src/core/network-policy.ts
8960
- var import_node_fs22 = __toESM(require("fs"));
8961
- var import_node_path26 = __toESM(require("path"));
9072
+ var import_node_fs23 = __toESM(require("fs"));
9073
+ var import_node_path27 = __toESM(require("path"));
8962
9074
  var NETWORK_POLICY_VERSION = 1;
8963
- var NETWORK_POLICY_FILE = import_node_path26.default.join(DEVROUTER_HOME, "network-policy.json");
9075
+ var NETWORK_POLICY_FILE = import_node_path27.default.join(DEVROUTER_HOME, "network-policy.json");
8964
9076
  var DEFAULT_NETWORK_POLICY_ENDPOINT_RESERVE = 8;
8965
9077
  var NETWORK_POLICY_PREFIX_LENGTHS = [24, 25, 26];
8966
9078
  var MAX_NETWORK_POLICY_FILE_BYTES = 64 * 1024;
@@ -9153,7 +9265,7 @@ function hasControlCharacter(value) {
9153
9265
  function readPolicyFile(filePath) {
9154
9266
  let initialStats;
9155
9267
  try {
9156
- initialStats = import_node_fs22.default.lstatSync(filePath);
9268
+ initialStats = import_node_fs23.default.lstatSync(filePath);
9157
9269
  } catch (error) {
9158
9270
  if (isErrno(error, "ENOENT")) return { status: "absent" };
9159
9271
  return { status: "invalid", error: "network policy could not be read" };
@@ -9166,16 +9278,16 @@ function readPolicyFile(filePath) {
9166
9278
  }
9167
9279
  let fileDescriptor;
9168
9280
  try {
9169
- fileDescriptor = import_node_fs22.default.openSync(
9281
+ fileDescriptor = import_node_fs23.default.openSync(
9170
9282
  filePath,
9171
- import_node_fs22.default.constants.O_RDONLY | import_node_fs22.default.constants.O_NONBLOCK | (import_node_fs22.default.constants.O_NOFOLLOW ?? 0)
9283
+ import_node_fs23.default.constants.O_RDONLY | import_node_fs23.default.constants.O_NONBLOCK | (import_node_fs23.default.constants.O_NOFOLLOW ?? 0)
9172
9284
  );
9173
9285
  } catch (error) {
9174
9286
  if (isErrno(error, "ENOENT")) return { status: "absent" };
9175
9287
  return { status: "invalid", error: "network policy could not be read" };
9176
9288
  }
9177
9289
  try {
9178
- const openedStats = import_node_fs22.default.fstatSync(fileDescriptor);
9290
+ const openedStats = import_node_fs23.default.fstatSync(fileDescriptor);
9179
9291
  if (!openedStats.isFile()) {
9180
9292
  return { status: "invalid", error: "network policy must be a regular file" };
9181
9293
  }
@@ -9188,12 +9300,12 @@ function readPolicyFile(filePath) {
9188
9300
  const buffer = Buffer.alloc(openedStats.size);
9189
9301
  let offset = 0;
9190
9302
  while (offset < buffer.length) {
9191
- const bytesRead = import_node_fs22.default.readSync(fileDescriptor, buffer, offset, buffer.length - offset, null);
9303
+ const bytesRead = import_node_fs23.default.readSync(fileDescriptor, buffer, offset, buffer.length - offset, null);
9192
9304
  if (bytesRead === 0)
9193
9305
  return { status: "invalid", error: "network policy changed during read" };
9194
9306
  offset += bytesRead;
9195
9307
  }
9196
- const finalStats = import_node_fs22.default.fstatSync(fileDescriptor);
9308
+ const finalStats = import_node_fs23.default.fstatSync(fileDescriptor);
9197
9309
  if (!finalStats.isFile() || finalStats.size !== openedStats.size) {
9198
9310
  return { status: "invalid", error: "network policy changed during read" };
9199
9311
  }
@@ -9202,7 +9314,7 @@ function readPolicyFile(filePath) {
9202
9314
  return { status: "invalid", error: "network policy could not be read" };
9203
9315
  } finally {
9204
9316
  try {
9205
- import_node_fs22.default.closeSync(fileDescriptor);
9317
+ import_node_fs23.default.closeSync(fileDescriptor);
9206
9318
  } catch {
9207
9319
  }
9208
9320
  }
@@ -9222,7 +9334,7 @@ function isErrno(error, code) {
9222
9334
 
9223
9335
  // src/core/network-claims.ts
9224
9336
  var NETWORK_CLAIMS_VERSION = 1;
9225
- var NETWORK_CLAIMS_ROOT = import_node_path27.default.join(DEVROUTER_HOME, "networks");
9337
+ var NETWORK_CLAIMS_ROOT = import_node_path28.default.join(DEVROUTER_HOME, "networks");
9226
9338
  var NETWORK_CLAIMS_MAX_RECORDS = 256;
9227
9339
  var NETWORK_CLAIMS_MAX_FILE_BYTES = 512 * 1024;
9228
9340
  var NETWORK_CLAIMS_DEFAULT_PREFIX = 26;
@@ -9628,27 +9740,27 @@ function assertExactKeys(value, keys2) {
9628
9740
  function readDocument(store) {
9629
9741
  let descriptor;
9630
9742
  try {
9631
- descriptor = import_node_fs23.default.openSync(
9743
+ descriptor = import_node_fs24.default.openSync(
9632
9744
  store.filePath,
9633
- import_node_fs23.default.constants.O_RDONLY | import_node_fs23.default.constants.O_NONBLOCK | (import_node_fs23.default.constants.O_NOFOLLOW ?? 0)
9745
+ import_node_fs24.default.constants.O_RDONLY | import_node_fs24.default.constants.O_NONBLOCK | (import_node_fs24.default.constants.O_NOFOLLOW ?? 0)
9634
9746
  );
9635
9747
  } catch (error) {
9636
9748
  if (isErrno2(error, "ENOENT")) return void 0;
9637
9749
  throw claimError("invalid-state", "network claim state could not be read");
9638
9750
  }
9639
9751
  try {
9640
- const stat = import_node_fs23.default.fstatSync(descriptor);
9752
+ const stat = import_node_fs24.default.fstatSync(descriptor);
9641
9753
  if (!stat.isFile() || stat.size > NETWORK_CLAIMS_MAX_FILE_BYTES || (stat.mode & 63) !== 0) {
9642
9754
  throw claimError("invalid-state", "network claim state is not a bounded private file");
9643
9755
  }
9644
9756
  const buffer = Buffer.alloc(NETWORK_CLAIMS_MAX_FILE_BYTES + 1);
9645
9757
  let count = 0;
9646
9758
  while (count < buffer.length) {
9647
- const bytesRead = import_node_fs23.default.readSync(descriptor, buffer, count, buffer.length - count, null);
9759
+ const bytesRead = import_node_fs24.default.readSync(descriptor, buffer, count, buffer.length - count, null);
9648
9760
  if (bytesRead === 0) break;
9649
9761
  count += bytesRead;
9650
9762
  }
9651
- const finalStat = import_node_fs23.default.fstatSync(descriptor);
9763
+ const finalStat = import_node_fs24.default.fstatSync(descriptor);
9652
9764
  if (count > NETWORK_CLAIMS_MAX_FILE_BYTES || finalStat.size !== stat.size) {
9653
9765
  throw claimError("invalid-state", "network claim state changed during read");
9654
9766
  }
@@ -9660,7 +9772,7 @@ function readDocument(store) {
9660
9772
  }
9661
9773
  return validateDocument(parsed, store.daemonId);
9662
9774
  } finally {
9663
- import_node_fs23.default.closeSync(descriptor);
9775
+ import_node_fs24.default.closeSync(descriptor);
9664
9776
  }
9665
9777
  }
9666
9778
  function validateDocument(value, daemonId) {
@@ -9764,7 +9876,7 @@ function withStoreLock(daemonId, options, operation) {
9764
9876
  if (!Number.isSafeInteger(waitMs) || waitMs < 0 || waitMs > MAX_LOCK_WAIT_MS) {
9765
9877
  throw claimError("invalid-input", "network claim lock wait is invalid");
9766
9878
  }
9767
- import_node_fs23.default.mkdirSync(store.root, { recursive: true, mode: 448 });
9879
+ import_node_fs24.default.mkdirSync(store.root, { recursive: true, mode: 448 });
9768
9880
  assertDirectory(store.root);
9769
9881
  return withFileLockSync(
9770
9882
  store.lockPath,
@@ -9779,23 +9891,23 @@ function createStore(daemonId, options) {
9779
9891
  return {
9780
9892
  root,
9781
9893
  daemonId,
9782
- filePath: import_node_path27.default.join(root, `${key}.json`),
9783
- lockPath: import_node_path27.default.join(root, `${key}.lock`),
9894
+ filePath: import_node_path28.default.join(root, `${key}.json`),
9895
+ lockPath: import_node_path28.default.join(root, `${key}.lock`),
9784
9896
  writeAtomic: options.writeAtomic ?? writeFileAtomically
9785
9897
  };
9786
9898
  }
9787
9899
  function validateRoot(root) {
9788
- if (typeof root !== "string" || !import_node_path27.default.isAbsolute(root) || root.includes("\0")) {
9900
+ if (typeof root !== "string" || !import_node_path28.default.isAbsolute(root) || root.includes("\0")) {
9789
9901
  throw claimError("invalid-input", "network claim root is invalid");
9790
9902
  }
9791
- const resolved = import_node_path27.default.resolve(root);
9903
+ const resolved = import_node_path28.default.resolve(root);
9792
9904
  if (resolved !== root) throw claimError("invalid-input", "network claim root is invalid");
9793
9905
  return resolved;
9794
9906
  }
9795
9907
  function assertDirectory(directory) {
9796
9908
  let stat;
9797
9909
  try {
9798
- stat = import_node_fs23.default.lstatSync(directory);
9910
+ stat = import_node_fs24.default.lstatSync(directory);
9799
9911
  } catch {
9800
9912
  throw claimError("invalid-state", "network claim root could not be read");
9801
9913
  }
@@ -9830,14 +9942,14 @@ function claimError(code, message) {
9830
9942
 
9831
9943
  // src/core/network-claim-lookup.ts
9832
9944
  function networkOwnerKey(repoPath) {
9833
- if (!import_node_path28.default.isAbsolute(repoPath) || import_node_path28.default.resolve(repoPath) !== repoPath)
9945
+ if (!import_node_path29.default.isAbsolute(repoPath) || import_node_path29.default.resolve(repoPath) !== repoPath)
9834
9946
  throw new Error("Invalid network owner path.");
9835
9947
  return (0, import_node_crypto13.createHash)("sha256").update(repoPath).digest("hex");
9836
9948
  }
9837
9949
  function findOwnedNetworkClaim(repoPath, root = NETWORK_CLAIMS_ROOT) {
9838
9950
  let names;
9839
9951
  try {
9840
- names = import_node_fs24.default.readdirSync(root);
9952
+ names = import_node_fs25.default.readdirSync(root);
9841
9953
  } catch (error) {
9842
9954
  if (error.code === "ENOENT") return void 0;
9843
9955
  throw new Error("Network claims cannot be inspected; repair is required.");
@@ -9846,21 +9958,21 @@ function findOwnedNetworkClaim(repoPath, root = NETWORK_CLAIMS_ROOT) {
9846
9958
  if (files.length > 128) throw new Error("Network daemon inventory exceeds its bound.");
9847
9959
  let found;
9848
9960
  for (const file of files) {
9849
- const full = import_node_path28.default.join(root, file);
9850
- const stat = import_node_fs24.default.lstatSync(full);
9961
+ const full = import_node_path29.default.join(root, file);
9962
+ const stat = import_node_fs25.default.lstatSync(full);
9851
9963
  if (!stat.isFile() || stat.size > NETWORK_CLAIMS_MAX_FILE_BYTES)
9852
9964
  throw new Error("Invalid retained network claim file.");
9853
9965
  let value;
9854
- const descriptor = import_node_fs24.default.openSync(
9966
+ const descriptor = import_node_fs25.default.openSync(
9855
9967
  full,
9856
- import_node_fs24.default.constants.O_RDONLY | import_node_fs24.default.constants.O_NONBLOCK | import_node_fs24.default.constants.O_NOFOLLOW
9968
+ import_node_fs25.default.constants.O_RDONLY | import_node_fs25.default.constants.O_NONBLOCK | import_node_fs25.default.constants.O_NOFOLLOW
9857
9969
  );
9858
9970
  try {
9859
- if (!import_node_fs24.default.fstatSync(descriptor).isFile()) throw new Error();
9971
+ if (!import_node_fs25.default.fstatSync(descriptor).isFile()) throw new Error();
9860
9972
  const bytes2 = Buffer.alloc(NETWORK_CLAIMS_MAX_FILE_BYTES + 1);
9861
9973
  let size = 0;
9862
9974
  while (size < bytes2.length) {
9863
- const count = import_node_fs24.default.readSync(descriptor, bytes2, size, bytes2.length - size, null);
9975
+ const count = import_node_fs25.default.readSync(descriptor, bytes2, size, bytes2.length - size, null);
9864
9976
  if (!count) break;
9865
9977
  size += count;
9866
9978
  }
@@ -9870,7 +9982,7 @@ function findOwnedNetworkClaim(repoPath, root = NETWORK_CLAIMS_ROOT) {
9870
9982
  } catch {
9871
9983
  throw new Error("Retained network claim could not be read safely.");
9872
9984
  } finally {
9873
- import_node_fs24.default.closeSync(descriptor);
9985
+ import_node_fs25.default.closeSync(descriptor);
9874
9986
  }
9875
9987
  if (typeof value.daemonId !== "string" || `${(0, import_node_crypto13.createHash)("sha256").update(value.daemonId).digest("hex")}.json` !== file)
9876
9988
  throw new Error("Retained network daemon identity is invalid.");
@@ -9888,7 +10000,7 @@ function findOwnedNetworkClaim(repoPath, root = NETWORK_CLAIMS_ROOT) {
9888
10000
  }
9889
10001
 
9890
10002
  // src/core/network-provider-inspect.ts
9891
- var import_node_child_process15 = require("child_process");
10003
+ var import_node_child_process17 = require("child_process");
9892
10004
  var import_node_crypto14 = require("crypto");
9893
10005
  function object2(value) {
9894
10006
  if (!value || typeof value !== "object" || Array.isArray(value))
@@ -9912,7 +10024,7 @@ function qualifyNetworkProviderDefinition(value, provider) {
9912
10024
  return (0, import_node_crypto14.createHash)("sha256").update(JSON.stringify({ name: definition.name, agent, options, exec })).digest("hex");
9913
10025
  }
9914
10026
  function read(provider, args) {
9915
- const result = (0, import_node_child_process15.spawnSync)(provider, args, {
10027
+ const result = (0, import_node_child_process17.spawnSync)(provider, args, {
9916
10028
  encoding: "utf8",
9917
10029
  timeout: 5e3,
9918
10030
  maxBuffer: 2 * 1024 * 1024
@@ -10026,7 +10138,7 @@ function networkProviderEffectOptions(provider, providerId, repoPath) {
10026
10138
  }
10027
10139
 
10028
10140
  // src/core/devsy-mutation.ts
10029
- var DEVSY_MUTATION_LOCK_FILE = import_node_path29.default.join(DEVROUTER_HOME, "devsy-mutation.lock");
10141
+ var DEVSY_MUTATION_LOCK_FILE = import_node_path30.default.join(DEVROUTER_HOME, "devsy-mutation.lock");
10030
10142
  var DEVSY_MUTATION_WAIT_MS = 18e5;
10031
10143
  var DevsyStartPostconditionError = class extends Error {
10032
10144
  };
@@ -10041,7 +10153,7 @@ function failedStartMayHaveAttached(devsyId, repoPath) {
10041
10153
  }
10042
10154
  }
10043
10155
  function withMutationLock(activity, target, operation) {
10044
- import_node_fs25.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
10156
+ import_node_fs26.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
10045
10157
  return withFileLockSync(
10046
10158
  DEVSY_MUTATION_LOCK_FILE,
10047
10159
  {
@@ -10055,7 +10167,7 @@ function withMutationLock(activity, target, operation) {
10055
10167
  );
10056
10168
  }
10057
10169
  function withMutationLockAsync(activity, target, operation) {
10058
- import_node_fs25.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
10170
+ import_node_fs26.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
10059
10171
  return withFileLock(
10060
10172
  DEVSY_MUTATION_LOCK_FILE,
10061
10173
  {
@@ -10076,7 +10188,7 @@ var DEVSY_STDERR_TAIL_BYTES = 8192;
10076
10188
  function runDevsyUp(args, env, quiet) {
10077
10189
  return new Promise((resolve) => {
10078
10190
  claimLifecycleEffect();
10079
- const child = (0, import_node_child_process16.spawn)("devsy", args, {
10191
+ const child = (0, import_node_child_process18.spawn)("devsy", args, {
10080
10192
  stdio: ["inherit", quiet ? 2 : "inherit", "pipe"],
10081
10193
  env
10082
10194
  });
@@ -10110,7 +10222,7 @@ function runDevsyAction(action, devsyId, force = false, repoPath) {
10110
10222
  const args = action === "delete" ? ["delete", devsyId, ...force ? ["--force"] : [], "--ignore-not-found"] : ["stop", devsyId];
10111
10223
  claimLifecycleEffect();
10112
10224
  const binding = repoPath ? networkProviderEffectOptions("devsy", devsyId, repoPath) : { args: [] };
10113
- const result = (0, import_node_child_process16.spawnSync)("devsy", ["workspace", ...args, ...binding.args], {
10225
+ const result = (0, import_node_child_process18.spawnSync)("devsy", ["workspace", ...args, ...binding.args], {
10114
10226
  encoding: "utf-8",
10115
10227
  ...binding.env ? { env: binding.env } : {}
10116
10228
  });
@@ -10127,6 +10239,7 @@ function inspectExactOwnership(devsyId, worktreePath) {
10127
10239
  }
10128
10240
  function mutateOwnedDevsyWorkspace(action, devsyId, worktreePath) {
10129
10241
  return withMutationLock(`Devsy ${action}`, worktreePath, () => {
10242
+ restoreRecordedManagedDevcontainerConfig(worktreePath);
10130
10243
  if (action === "stop") {
10131
10244
  const stopped = stopRetainedManagedDevsyWorkspace({
10132
10245
  repoPath: worktreePath,
@@ -10293,21 +10406,21 @@ var ExecutionOutcomeError = class extends Error {
10293
10406
 
10294
10407
  // src/core/reliability-lifecycle.ts
10295
10408
  var import_node_crypto22 = require("crypto");
10296
- var import_node_path40 = __toESM(require("path"));
10409
+ var import_node_path41 = __toESM(require("path"));
10297
10410
  var import_node_util7 = require("util");
10298
10411
 
10299
10412
  // src/core/capacity-policy.ts
10300
- var import_node_fs26 = __toESM(require("fs"));
10301
- var import_node_path30 = __toESM(require("path"));
10413
+ var import_node_fs27 = __toESM(require("fs"));
10414
+ var import_node_path31 = __toESM(require("path"));
10302
10415
 
10303
10416
  // src/core/controller-client.ts
10304
10417
  var import_node_crypto15 = require("crypto");
10305
- var import_node_fs27 = __toESM(require("fs"));
10418
+ var import_node_fs28 = __toESM(require("fs"));
10306
10419
  var import_node_net2 = __toESM(require("net"));
10307
- var import_node_path32 = __toESM(require("path"));
10420
+ var import_node_path33 = __toESM(require("path"));
10308
10421
 
10309
10422
  // src/core/controller-protocol.ts
10310
- var import_node_path31 = __toESM(require("path"));
10423
+ var import_node_path32 = __toESM(require("path"));
10311
10424
  var CONTROLLER_FRAME_BYTES = 64 * 1024;
10312
10425
  var CONTROLLER_MAX_OPERATION_REQUEST_BYTES = 32 * 1024;
10313
10426
  var CONTROLLER_PROFILE_TOKEN_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
@@ -10647,7 +10760,7 @@ function handleOperationRequest(state, event) {
10647
10760
  if (state.phase === "stopping" || state.desired === "parked-for-capacity")
10648
10761
  return blocked(
10649
10762
  state,
10650
- `phase is '${state.phase}' with desired '${state.desired}'; wait for the running stop to finish.`
10763
+ `phase is '${state.phase}' with desired '${state.desired}'; wait for the running stop to finish, or when no stop worker is running rerun 'devrouter stop <path> --delete', removing the exact provider registration first when the stop cannot prove the recorded population.`
10651
10764
  );
10652
10765
  if (event.kind === "exec" && (!event.runtimeRunning || state.desired !== "running" && state.intentRevision !== 0))
10653
10766
  return blocked(
@@ -11104,10 +11217,10 @@ function stepReliability(state, event, nowMs) {
11104
11217
  }
11105
11218
 
11106
11219
  // src/core/reliability-worker.ts
11107
- var import_node_child_process17 = require("child_process");
11220
+ var import_node_child_process19 = require("child_process");
11108
11221
  var import_node_crypto17 = require("crypto");
11109
- var import_node_fs28 = __toESM(require("fs"));
11110
- var import_node_path33 = __toESM(require("path"));
11222
+ var import_node_fs29 = __toESM(require("fs"));
11223
+ var import_node_path34 = __toESM(require("path"));
11111
11224
 
11112
11225
  // src/core/lifecycle-progress.ts
11113
11226
  var ROLES = {
@@ -11174,20 +11287,20 @@ function workerGroupAbsent(pid) {
11174
11287
  }
11175
11288
 
11176
11289
  // src/core/workspace-ensure.ts
11177
- var import_node_child_process27 = require("child_process");
11178
- var import_node_fs34 = __toESM(require("fs"));
11179
- var import_node_path39 = __toESM(require("path"));
11290
+ var import_node_child_process30 = require("child_process");
11291
+ var import_node_fs35 = __toESM(require("fs"));
11292
+ var import_node_path40 = __toESM(require("path"));
11180
11293
 
11181
11294
  // src/core/devpod-mutation.ts
11182
- var import_node_child_process18 = require("child_process");
11183
- var import_node_fs29 = __toESM(require("fs"));
11184
- var import_node_path34 = __toESM(require("path"));
11185
- var DEVPOD_MUTATION_LOCK_FILE = import_node_path34.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
11295
+ var import_node_child_process20 = require("child_process");
11296
+ var import_node_fs30 = __toESM(require("fs"));
11297
+ var import_node_path35 = __toESM(require("path"));
11298
+ var DEVPOD_MUTATION_LOCK_FILE = import_node_path35.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
11186
11299
  var DEVPOD_MUTATION_WAIT_MS = 18e5;
11187
11300
  var DevpodStartPostconditionError = class extends Error {
11188
11301
  };
11189
11302
  function withMutationLock2(activity, target, operation) {
11190
- import_node_fs29.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
11303
+ import_node_fs30.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
11191
11304
  return withFileLockSync(
11192
11305
  DEVPOD_MUTATION_LOCK_FILE,
11193
11306
  {
@@ -11207,7 +11320,7 @@ function runDevpodAction(action, devpodId, force = false, repoPath) {
11207
11320
  const args = action === "delete" ? [action, devpodId, ...force ? ["--force"] : [], "--ignore-not-found"] : [action, devpodId];
11208
11321
  claimLifecycleEffect();
11209
11322
  const binding = repoPath ? networkProviderEffectOptions("devpod", devpodId, repoPath) : { args: [] };
11210
- const result = (0, import_node_child_process18.spawnSync)("devpod", [...args, ...binding.args], {
11323
+ const result = (0, import_node_child_process20.spawnSync)("devpod", [...args, ...binding.args], {
11211
11324
  encoding: "utf-8",
11212
11325
  ...binding.env ? { env: binding.env } : {}
11213
11326
  });
@@ -11349,7 +11462,7 @@ async function startDevpodWorkspace(options) {
11349
11462
  }
11350
11463
  claimLifecycleEffect();
11351
11464
  network?.beforeDispatch?.();
11352
- const result = (0, import_node_child_process18.spawnSync)("devpod", args, {
11465
+ const result = (0, import_node_child_process20.spawnSync)("devpod", args, {
11353
11466
  stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
11354
11467
  env
11355
11468
  });
@@ -11389,8 +11502,8 @@ async function startDevpodWorkspace(options) {
11389
11502
  }
11390
11503
 
11391
11504
  // src/core/host-port-claims.ts
11392
- var import_node_child_process19 = require("child_process");
11393
- var import_node_path35 = __toESM(require("path"));
11505
+ var import_node_child_process21 = require("child_process");
11506
+ var import_node_path36 = __toESM(require("path"));
11394
11507
  var COMPOSE_RENDER_TIMEOUT_MS = 3e4;
11395
11508
  var COMPOSE_RENDER_MAX_BUFFER = 8 * 1024 * 1024;
11396
11509
  var DOCKER_TIMEOUT_MS = 1e4;
@@ -11493,7 +11606,7 @@ function hostPortBindingsConflict(desired, holder) {
11493
11606
  }
11494
11607
  var HOST_PORT_INSPECT_TEMPLATE = '{"id":{{json .Id}},"name":{{json .Name}},"labels":{"com.docker.compose.project":{{json (index .Config.Labels "com.docker.compose.project")}},"com.docker.compose.project.working_dir":{{json (index .Config.Labels "com.docker.compose.project.working_dir")}}},"ports":{{json .NetworkSettings.Ports}}}';
11495
11608
  function runDocker(args) {
11496
- const result = (0, import_node_child_process19.spawnSync)("docker", args, {
11609
+ const result = (0, import_node_child_process21.spawnSync)("docker", args, {
11497
11610
  ...networkDockerOptions(),
11498
11611
  encoding: "utf-8",
11499
11612
  timeout: DOCKER_TIMEOUT_MS,
@@ -11563,7 +11676,7 @@ function listHolderSnapshots() {
11563
11676
  }
11564
11677
  function renderManagedComposeModel(plan, workspace) {
11565
11678
  const fileArgs = plan.composeFiles.flatMap((file) => ["-f", file]);
11566
- const result = (0, import_node_child_process19.spawnSync)(
11679
+ const result = (0, import_node_child_process21.spawnSync)(
11567
11680
  "docker",
11568
11681
  [
11569
11682
  "compose",
@@ -11598,8 +11711,8 @@ function renderManagedComposeModel(plan, workspace) {
11598
11711
  }
11599
11712
  }
11600
11713
  function attributeHolderWorktree(workingDir) {
11601
- if (import_node_path35.default.basename(workingDir) !== ".devcontainer") return void 0;
11602
- return comparableWorkspacePath(import_node_path35.default.dirname(workingDir));
11714
+ if (import_node_path36.default.basename(workingDir) !== ".devcontainer") return void 0;
11715
+ return comparableWorkspacePath(import_node_path36.default.dirname(workingDir));
11603
11716
  }
11604
11717
  function holderAttribution(holder, repoPath) {
11605
11718
  const attribution = {};
@@ -11634,7 +11747,7 @@ function detectHostPortClaimConflicts(options) {
11634
11747
  );
11635
11748
  if (desired.length === 0) return [];
11636
11749
  const holders = listHolderSnapshots();
11637
- const devcontainerDir = import_node_path35.default.join(options.repoPath, ".devcontainer");
11750
+ const devcontainerDir = import_node_path36.default.join(options.repoPath, ".devcontainer");
11638
11751
  const conflicts = [];
11639
11752
  for (const binding of desired) {
11640
11753
  for (const holder of holders) {
@@ -11658,7 +11771,7 @@ function detectHostPortClaimConflicts(options) {
11658
11771
  }
11659
11772
 
11660
11773
  // src/core/http-route-probe.ts
11661
- var import_node_child_process20 = require("child_process");
11774
+ var import_node_child_process22 = require("child_process");
11662
11775
  var DEFAULT_MAX_TIME_SECONDS = 5;
11663
11776
  var PARENT_TIMEOUT_PADDING_MS = 250;
11664
11777
  var CONTRACT_MAX_BUFFER = 1024;
@@ -11691,7 +11804,7 @@ function probeHttpRoute(host, options = {}) {
11691
11804
  args.push("--cacert", CERT_FILE);
11692
11805
  }
11693
11806
  args.push(url);
11694
- const result = (0, import_node_child_process20.spawnSync)("curl", args, { encoding: "utf-8" });
11807
+ const result = (0, import_node_child_process22.spawnSync)("curl", args, { encoding: "utf-8" });
11695
11808
  const stdout = result.stdout?.trim() ?? "";
11696
11809
  const status = /^\d{3}$/.test(stdout) ? Number(stdout) : void 0;
11697
11810
  const ok = result.status === 0 && status !== void 0 && status >= 100 && status < 500;
@@ -11710,7 +11823,7 @@ function probeHttpReadinessSync(host, readiness, maxTimeSeconds, repoPath) {
11710
11823
  }
11711
11824
  let result;
11712
11825
  try {
11713
- result = (0, import_node_child_process20.spawnSync)("curl", args, {
11826
+ result = (0, import_node_child_process22.spawnSync)("curl", args, {
11714
11827
  encoding: "utf-8",
11715
11828
  maxBuffer: CONTRACT_MAX_BUFFER,
11716
11829
  timeout: Math.ceil(parentTimeoutMs)
@@ -11794,8 +11907,8 @@ function transportFailure() {
11794
11907
  }
11795
11908
 
11796
11909
  // src/core/managed-host-preparation.ts
11797
- var import_node_child_process21 = require("child_process");
11798
- var import_node_fs30 = __toESM(require("fs"));
11910
+ var import_node_child_process23 = require("child_process");
11911
+ var import_node_fs31 = __toESM(require("fs"));
11799
11912
  var PREPARATION_TIMEOUT_MS = 6e4;
11800
11913
  var CONFIG_READ_ERROR = "Managed host preparation could not read .devrouter.yml.";
11801
11914
  var CONFIG_CHANGED_ERROR = "Managed host preparation changed .devrouter.yml; refusing to continue.";
@@ -11806,7 +11919,7 @@ function runPreparationCommand(argv, cwd) {
11806
11919
  return new Promise((resolve) => {
11807
11920
  let child;
11808
11921
  try {
11809
- child = (0, import_node_child_process21.spawn)(argv[0], argv.slice(1), {
11922
+ child = (0, import_node_child_process23.spawn)(argv[0], argv.slice(1), {
11810
11923
  cwd,
11811
11924
  detached: true,
11812
11925
  shell: false,
@@ -11838,7 +11951,7 @@ function runPreparationCommand(argv, cwd) {
11838
11951
  }
11839
11952
  function readConfigBytes(configPath) {
11840
11953
  try {
11841
- return import_node_fs30.default.readFileSync(configPath);
11954
+ return import_node_fs31.default.readFileSync(configPath);
11842
11955
  } catch {
11843
11956
  return void 0;
11844
11957
  }
@@ -11872,10 +11985,10 @@ async function runManagedHostPreparation(repoPath, config) {
11872
11985
  }
11873
11986
 
11874
11987
  // src/core/managed-post-start.ts
11875
- var import_node_child_process22 = require("child_process");
11988
+ var import_node_child_process24 = require("child_process");
11876
11989
  var import_node_crypto18 = require("crypto");
11877
- var import_node_fs31 = __toESM(require("fs"));
11878
- var import_node_path36 = __toESM(require("path"));
11990
+ var import_node_fs32 = __toESM(require("fs"));
11991
+ var import_node_path37 = __toESM(require("path"));
11879
11992
  var MANAGED_MARKER = "devrouter:managed devcontainer";
11880
11993
  var MANAGED_ADAPTER_PATH = ".devcontainer/post-start.sh";
11881
11994
  var RUNTIME_HELPER_PATH = "/tmp/devrouter/bin/devrouter-process";
@@ -11888,7 +12001,7 @@ function renderDeliveryScript(targetPath) {
11888
12001
  if (!/^\/tmp\/devrouter\/bin\/[a-zA-Z0-9._-]+$/.test(targetPath)) {
11889
12002
  throw new Error(`Unsafe managed runtime delivery path: ${targetPath}`);
11890
12003
  }
11891
- const temporaryPrefix = import_node_path36.default.posix.basename(targetPath);
12004
+ const temporaryPrefix = import_node_path37.default.posix.basename(targetPath);
11892
12005
  return `set -eu
11893
12006
  umask 077
11894
12007
  runtime_root=/tmp/devrouter
@@ -11912,7 +12025,7 @@ function commandFailure3(result) {
11912
12025
  return [result.error?.message, result.stderr, result.stdout].filter(Boolean).map((value) => String(value).trim()).filter(Boolean).join("\n");
11913
12026
  }
11914
12027
  function deliverRuntimeFile(containerId, targetPath, contents, label) {
11915
- const result = (0, import_node_child_process22.spawnSync)(
12028
+ const result = (0, import_node_child_process24.spawnSync)(
11916
12029
  "docker",
11917
12030
  ["exec", "-i", containerId, "sh", "-c", renderDeliveryScript(targetPath)],
11918
12031
  { input: contents, encoding: "utf-8", ...networkDockerOptions() }
@@ -11923,8 +12036,8 @@ function deliverRuntimeFile(containerId, targetPath, contents, label) {
11923
12036
  }
11924
12037
  }
11925
12038
  function readRegularFileBytes(filePath) {
11926
- if (!import_node_fs31.default.existsSync(filePath) || !import_node_fs31.default.lstatSync(filePath).isFile()) return void 0;
11927
- return import_node_fs31.default.readFileSync(filePath);
12039
+ if (!import_node_fs32.default.existsSync(filePath) || !import_node_fs32.default.lstatSync(filePath).isFile()) return void 0;
12040
+ return import_node_fs32.default.readFileSync(filePath);
11928
12041
  }
11929
12042
  function readRegularFile(filePath) {
11930
12043
  return readRegularFileBytes(filePath)?.toString("utf-8");
@@ -11934,21 +12047,21 @@ function adapterFingerprint(adapter) {
11934
12047
  }
11935
12048
  function resolveProcessHelperPath() {
11936
12049
  const candidates = [
11937
- import_node_path36.default.resolve(__dirname, "..", "bin", "devrouter-process"),
11938
- import_node_path36.default.resolve(__dirname, "..", "..", "bin", "devrouter-process")
12050
+ import_node_path37.default.resolve(__dirname, "..", "bin", "devrouter-process"),
12051
+ import_node_path37.default.resolve(__dirname, "..", "..", "bin", "devrouter-process")
11939
12052
  ];
11940
- const helperPath = candidates.find((candidate) => import_node_fs31.default.existsSync(candidate));
12053
+ const helperPath = candidates.find((candidate) => import_node_fs32.default.existsSync(candidate));
11941
12054
  if (!helperPath) {
11942
12055
  throw new Error("Could not locate the packaged devrouter-process helper.");
11943
12056
  }
11944
12057
  return helperPath;
11945
12058
  }
11946
12059
  function resolveManagedPostStartPlan(repoPath) {
11947
- const adapterPath = import_node_path36.default.join(repoPath, MANAGED_ADAPTER_PATH);
12060
+ const adapterPath = import_node_path37.default.join(repoPath, MANAGED_ADAPTER_PATH);
11948
12061
  const adapterBytes = readRegularFileBytes(adapterPath);
11949
12062
  const adapterText = adapterBytes?.toString("utf-8");
11950
- const dockerfilePath = import_node_path36.default.join(repoPath, ".devcontainer", "Dockerfile");
11951
- const devcontainerPath = import_node_path36.default.join(repoPath, ".devcontainer", "devcontainer.json");
12063
+ const dockerfilePath = import_node_path37.default.join(repoPath, ".devcontainer", "Dockerfile");
12064
+ const devcontainerPath = import_node_path37.default.join(repoPath, ".devcontainer", "devcontainer.json");
11952
12065
  const dockerfile = readRegularFile(dockerfilePath) ?? "";
11953
12066
  const devcontainer = readRegularFile(devcontainerPath) ?? "";
11954
12067
  const devrouterPattern = [adapterText, dockerfile, devcontainer].filter((value) => value !== void 0).some(
@@ -11989,7 +12102,7 @@ function resolveManagedPostStartPlan(repoPath) {
11989
12102
  }
11990
12103
  function runManagedPostStart(options) {
11991
12104
  if (options.plan.kind !== "runtime") return;
11992
- const helper = import_node_fs31.default.readFileSync(resolveProcessHelperPath());
12105
+ const helper = import_node_fs32.default.readFileSync(resolveProcessHelperPath());
11993
12106
  deliverRuntimeFile(
11994
12107
  options.container.id,
11995
12108
  RUNTIME_HELPER_PATH,
@@ -12003,7 +12116,7 @@ function runManagedPostStart(options) {
12003
12116
  options.plan.adapterContents,
12004
12117
  "the managed post-start adapter"
12005
12118
  );
12006
- const started2 = (0, import_node_child_process22.spawnSync)(
12119
+ const started2 = (0, import_node_child_process24.spawnSync)(
12007
12120
  "docker",
12008
12121
  [
12009
12122
  "exec",
@@ -12044,7 +12157,7 @@ function assertSafeProcessName(name) {
12044
12157
  }
12045
12158
  function runManagedProcessAction(options) {
12046
12159
  assertSafeProcessName(options.name);
12047
- const result = (0, import_node_child_process22.spawnSync)(
12160
+ const result = (0, import_node_child_process24.spawnSync)(
12048
12161
  "docker",
12049
12162
  [
12050
12163
  "exec",
@@ -12082,6 +12195,96 @@ function runManagedProcessAction(options) {
12082
12195
  return "drifted";
12083
12196
  }
12084
12197
 
12198
+ // src/core/managed-mount-nesting.ts
12199
+ var import_node_child_process25 = require("child_process");
12200
+ var NESTED_MOUNT_REASON = "no configured mount is nested inside another configured mount";
12201
+ var UNREADABLE_MOUNT_REASON = "the container's own mount table could not be read";
12202
+ function normalizeDestination(value) {
12203
+ const trimmed = value.trim().replace(/\/+$/, "");
12204
+ return trimmed === "" ? "/" : trimmed;
12205
+ }
12206
+ function isNestedWithin(destination, candidate) {
12207
+ return candidate === "/" ? destination !== "/" : destination.startsWith(`${candidate}/`);
12208
+ }
12209
+ function unescapeMountField(value) {
12210
+ return value.replace(
12211
+ /\\([0-7]{3})/g,
12212
+ (_match, octal) => String.fromCharCode(Number.parseInt(octal, 8))
12213
+ );
12214
+ }
12215
+ function parseMountinfoDestinations(text4) {
12216
+ const destinations = /* @__PURE__ */ new Set();
12217
+ for (const line of text4.split("\n")) {
12218
+ const fields2 = line.split(" ");
12219
+ if (fields2.length < 5 || !/^\d+$/.test(fields2[0] ?? "")) continue;
12220
+ destinations.add(normalizeDestination(unescapeMountField(fields2[4] ?? "")));
12221
+ }
12222
+ return [...destinations];
12223
+ }
12224
+ function isNestedIndex(destinations, index) {
12225
+ return destinations.some(
12226
+ (candidate, other) => other !== index && isNestedWithin(destinations[index] ?? "", candidate)
12227
+ );
12228
+ }
12229
+ function configuredDestinations(configured) {
12230
+ return configured.map((mount) => normalizeDestination(mount.Destination));
12231
+ }
12232
+ function classifyManagedMountNesting(configured, effectiveDestinations, containerId) {
12233
+ const destinations = configuredDestinations(configured);
12234
+ const nested = configured.filter((_mount, index) => isNestedIndex(destinations, index));
12235
+ const identity2 = containerId ? { container: containerId } : {};
12236
+ if (nested.length === 0) {
12237
+ return { status: "not-applicable", checked: [], unwound: [], reason: NESTED_MOUNT_REASON };
12238
+ }
12239
+ if (effectiveDestinations === null) {
12240
+ return {
12241
+ status: "unverified",
12242
+ checked: [],
12243
+ unwound: [],
12244
+ ...identity2,
12245
+ reason: UNREADABLE_MOUNT_REASON
12246
+ };
12247
+ }
12248
+ const effective = new Set(effectiveDestinations.map(normalizeDestination));
12249
+ const checked = [];
12250
+ const unwound = [];
12251
+ for (const mount of nested) {
12252
+ const destination = normalizeDestination(mount.Destination);
12253
+ if (effective.has(destination)) {
12254
+ checked.push(mount.Destination);
12255
+ continue;
12256
+ }
12257
+ const nestedWithin = destinations.filter((candidate) => isNestedWithin(destination, candidate)).sort((a, b) => a.length - b.length)[0];
12258
+ unwound.push({
12259
+ destination: mount.Destination,
12260
+ source: mount.Source,
12261
+ nestedWithin: nestedWithin ?? "/"
12262
+ });
12263
+ }
12264
+ return {
12265
+ status: unwound.length > 0 ? "unwound" : "effective",
12266
+ checked,
12267
+ unwound,
12268
+ ...identity2
12269
+ };
12270
+ }
12271
+ function readContainerMountinfo(containerId, options) {
12272
+ const result = (0, import_node_child_process25.spawnSync)("docker", ["exec", containerId, "cat", "/proc/self/mountinfo"], {
12273
+ encoding: "utf-8",
12274
+ timeout: options.timeoutMs ?? 5e3,
12275
+ stdio: ["ignore", "pipe", "pipe"],
12276
+ ...networkDockerOptions()
12277
+ });
12278
+ if (result.status !== 0 || typeof result.stdout !== "string") return null;
12279
+ const destinations = parseMountinfoDestinations(result.stdout);
12280
+ return destinations.length > 0 ? destinations : null;
12281
+ }
12282
+ function observeManagedMountNesting(container, options = {}) {
12283
+ const destinations = configuredDestinations(container.mounts);
12284
+ const effective = destinations.some((_destination, index) => isNestedIndex(destinations, index)) ? readContainerMountinfo(container.id, options) : null;
12285
+ return classifyManagedMountNesting(container.mounts, effective, container.id);
12286
+ }
12287
+
12085
12288
  // src/core/managed-runtime-status.ts
12086
12289
  function recoveryPath(repoPath) {
12087
12290
  if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(repoPath)) return repoPath;
@@ -12362,6 +12565,7 @@ function inspectManagedRuntime(options) {
12362
12565
  (mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath)
12363
12566
  )?.Destination;
12364
12567
  const primaryActive = Boolean(primary?.state.Running);
12568
+ const mountNesting = primary && primaryActive ? observeManagedMountNesting(primary) : void 0;
12365
12569
  if (generatedConfigMissing && (state || primaryActive || managedServicesActive)) {
12366
12570
  drift.push("managed generated Dev Container configuration is missing");
12367
12571
  }
@@ -12428,7 +12632,8 @@ function inspectManagedRuntime(options) {
12428
12632
  processStatuses,
12429
12633
  activeServices: sortedUnique(activeServices),
12430
12634
  activeProcesses: sortedUnique(activeProcesses),
12431
- drift
12635
+ drift,
12636
+ ...mountNesting ? { mountNesting } : {}
12432
12637
  };
12433
12638
  }
12434
12639
  function collectManagedRuntimeStatus(options) {
@@ -12576,6 +12781,7 @@ function collectManagedRuntimeStatus(options) {
12576
12781
  baseServiceStatuses: inspection.baseStatuses,
12577
12782
  processStatuses: inspection.processStatuses,
12578
12783
  drift: sortedUnique(drift),
12784
+ ...inspection.mountNesting ? { mountNesting: inspection.mountNesting } : {},
12579
12785
  ...inspection.plan?.sourceConfigSha256 || state?.sourceConfigSha256 ? { sourceConfigSha256: inspection.plan?.sourceConfigSha256 ?? state?.sourceConfigSha256 } : {},
12580
12786
  ...inspection.plan?.effectiveConfigSha256 || state?.effectiveConfigSha256 ? {
12581
12787
  effectiveConfigSha256: inspection.plan?.effectiveConfigSha256 ?? state?.effectiveConfigSha256
@@ -12585,19 +12791,19 @@ function collectManagedRuntimeStatus(options) {
12585
12791
  }
12586
12792
 
12587
12793
  // src/core/network-inventory.ts
12588
- var import_node_child_process23 = require("child_process");
12794
+ var import_node_child_process26 = require("child_process");
12589
12795
  var NetworkInventoryError = class extends Error {
12590
12796
  };
12591
12797
  var MAX_IDS = 4096;
12592
12798
  var MAX_OUTPUT_BYTES = 2 * 1024 * 1024;
12593
12799
  var FULL_ID = /^[a-f0-9]{64}$/;
12594
- var NETWORK_TEMPLATE = '{"id":{{json .Id}},"name":{{json .Name}},"driver":{{json .Driver}},"ipam":{{json .IPAM.Config}},"activeEndpoints":{{len .Containers}},"project":{{json (index .Labels "com.docker.compose.project")}},"network":{{json (index .Labels "com.docker.compose.network")}}';
12800
+ var NETWORK_TEMPLATE = '{"id":{{json .Id}},"name":{{json .Name}},"driver":{{json .Driver}},"ipam":{{json .IPAM.Config}},"activeEndpoints":{{len .Containers}},"project":{{json (index .Labels "com.docker.compose.project")}},"network":{{json (index .Labels "com.docker.compose.network")}}}';
12595
12801
  var CONTAINER_TEMPLATE = '{"id":{{json .Id}},"networks":{{json .NetworkSettings.Networks}}}';
12596
12802
  function readDocker(endpoint, args, timeoutMs) {
12597
12803
  const env = { ...process.env };
12598
12804
  delete env.DOCKER_CONTEXT;
12599
12805
  delete env.DOCKER_HOST;
12600
- const result = (0, import_node_child_process23.spawnSync)("docker", ["--host", endpoint, ...args], {
12806
+ const result = (0, import_node_child_process26.spawnSync)("docker", ["--host", endpoint, ...args], {
12601
12807
  env,
12602
12808
  encoding: "utf-8",
12603
12809
  timeout: timeoutMs,
@@ -12628,7 +12834,14 @@ function id4(value) {
12628
12834
  }
12629
12835
  function rows(output) {
12630
12836
  const trimmed = output.trim();
12631
- return trimmed ? trimmed.split(/\r?\n/).map((line) => JSON.parse(line)) : [];
12837
+ return trimmed ? trimmed.split(/\r?\n/).map((line) => parseRecord(line)) : [];
12838
+ }
12839
+ function parseRecord(line) {
12840
+ try {
12841
+ return JSON.parse(line);
12842
+ } catch {
12843
+ throw new NetworkInventoryError("Docker network inventory contains a malformed record.");
12844
+ }
12632
12845
  }
12633
12846
  function ids(output) {
12634
12847
  const values = output.trim() ? output.trim().split(/\r?\n/).map(id4) : [];
@@ -12673,7 +12886,7 @@ function collectDockerNetworkInventory(options = {}, dependencies = {}) {
12673
12886
  return output;
12674
12887
  };
12675
12888
  const info = record4(
12676
- JSON.parse(
12889
+ parseRecord(
12677
12890
  read2(["info", "--format", '{"id":{{json .ID}},"pools":{{json .DefaultAddressPools}}}'])
12678
12891
  )
12679
12892
  );
@@ -12754,37 +12967,37 @@ function collectDockerNetworkInventory(options = {}, dependencies = {}) {
12754
12967
  }
12755
12968
 
12756
12969
  // src/core/network-routes.ts
12757
- var import_node_child_process24 = require("child_process");
12970
+ var import_node_child_process27 = require("child_process");
12758
12971
  var import_node_net3 = require("net");
12759
12972
 
12760
12973
  // src/core/network-local-daemon.ts
12761
- var import_node_fs32 = __toESM(require("fs"));
12762
- var import_node_path37 = __toESM(require("path"));
12974
+ var import_node_fs33 = __toESM(require("fs"));
12975
+ var import_node_path38 = __toESM(require("path"));
12763
12976
  function hasLocalDockerNetworkNamespace(endpoint, procRoot = "/proc") {
12764
12977
  const deadline = Date.now() + 2e3;
12765
12978
  try {
12766
12979
  if (!endpoint.startsWith("unix:///")) return false;
12767
12980
  const socket = endpoint.slice("unix://".length);
12768
- const table = import_node_fs32.default.readFileSync(import_node_path37.default.join(procRoot, "net/unix"), "utf8");
12981
+ const table = import_node_fs33.default.readFileSync(import_node_path38.default.join(procRoot, "net/unix"), "utf8");
12769
12982
  if (Buffer.byteLength(table) > 1024 * 1024) return false;
12770
12983
  const rows2 = table.split("\n").map((line) => line.trim().split(/\s+/)).filter((row) => row[7] === socket && row[3] === "00010000");
12771
12984
  if (rows2.length !== 1 || !/^\d+$/.test(rows2[0][6])) return false;
12772
12985
  const inode = `socket:[${rows2[0][6]}]`;
12773
- const ownNamespace = import_node_fs32.default.readlinkSync(import_node_path37.default.join(procRoot, "self/ns/net"));
12774
- const pids = import_node_fs32.default.readdirSync(procRoot).filter((name) => /^\d+$/.test(name));
12986
+ const ownNamespace = import_node_fs33.default.readlinkSync(import_node_path38.default.join(procRoot, "self/ns/net"));
12987
+ const pids = import_node_fs33.default.readdirSync(procRoot).filter((name) => /^\d+$/.test(name));
12775
12988
  if (pids.length > 4096) return false;
12776
12989
  for (const pid of pids) {
12777
12990
  if (Date.now() > deadline) return false;
12778
12991
  try {
12779
- const root = import_node_path37.default.join(procRoot, pid);
12780
- if (import_node_path37.default.basename(import_node_fs32.default.readlinkSync(import_node_path37.default.join(root, "exe"))) !== "dockerd") continue;
12781
- if (import_node_fs32.default.readlinkSync(import_node_path37.default.join(root, "ns/net")) !== ownNamespace) continue;
12782
- const descriptors = import_node_fs32.default.readdirSync(import_node_path37.default.join(root, "fd"));
12992
+ const root = import_node_path38.default.join(procRoot, pid);
12993
+ if (import_node_path38.default.basename(import_node_fs33.default.readlinkSync(import_node_path38.default.join(root, "exe"))) !== "dockerd") continue;
12994
+ if (import_node_fs33.default.readlinkSync(import_node_path38.default.join(root, "ns/net")) !== ownNamespace) continue;
12995
+ const descriptors = import_node_fs33.default.readdirSync(import_node_path38.default.join(root, "fd"));
12783
12996
  if (descriptors.length > 4096) continue;
12784
12997
  if (descriptors.some((fd) => {
12785
12998
  if (Date.now() > deadline) return false;
12786
12999
  try {
12787
- return import_node_fs32.default.readlinkSync(import_node_path37.default.join(root, "fd", fd)) === inode;
13000
+ return import_node_fs33.default.readlinkSync(import_node_path38.default.join(root, "fd", fd)) === inode;
12788
13001
  } catch {
12789
13002
  return false;
12790
13003
  }
@@ -12823,7 +13036,7 @@ function parseLinuxNetworkRoutes(output) {
12823
13036
  function collectNetworkRoutes(options) {
12824
13037
  if ((options.platform ?? process.platform) !== "linux" || !hasLocalDockerNetworkNamespace(options.endpoint))
12825
13038
  return { status: "unknown", routes: [] };
12826
- const result = (0, import_node_child_process24.spawnSync)("ip", ["-j", "-4", "route", "show", "table", "all"], {
13039
+ const result = (0, import_node_child_process27.spawnSync)("ip", ["-j", "-4", "route", "show", "table", "all"], {
12827
13040
  encoding: "utf8",
12828
13041
  timeout: 5e3,
12829
13042
  maxBuffer: 1024 * 1024
@@ -13672,10 +13885,10 @@ function fingerprintComposeNetworkModel(input) {
13672
13885
  }
13673
13886
 
13674
13887
  // src/core/network-compose-files.ts
13675
- var import_node_child_process25 = require("child_process");
13888
+ var import_node_child_process28 = require("child_process");
13676
13889
  var import_node_crypto20 = require("crypto");
13677
- var import_node_fs33 = __toESM(require("fs"));
13678
- var import_node_path38 = __toESM(require("path"));
13890
+ var import_node_fs34 = __toESM(require("fs"));
13891
+ var import_node_path39 = __toESM(require("path"));
13679
13892
  var import_yaml4 = __toESM(require("yaml"));
13680
13893
  var OVERLAY = "docker-compose.devrouter-network.yml";
13681
13894
  var MARKER = "# devrouter:managed workspace network";
@@ -13688,12 +13901,12 @@ function object3(value) {
13688
13901
  function inspectNetworkComposeFiles(input) {
13689
13902
  const authoredNetworks = {};
13690
13903
  for (const file of input.plan.composeFiles) {
13691
- const stat = import_node_fs33.default.lstatSync(file);
13904
+ const stat = import_node_fs34.default.lstatSync(file);
13692
13905
  if (!stat.isFile() || stat.size > MAX_BYTES2)
13693
13906
  throw new Error("Network Compose source is unavailable or too large.");
13694
13907
  let source;
13695
13908
  try {
13696
- source = object3(import_yaml4.default.parse(import_node_fs33.default.readFileSync(file, "utf8")));
13909
+ source = object3(import_yaml4.default.parse(import_node_fs34.default.readFileSync(file, "utf8")));
13697
13910
  } catch {
13698
13911
  throw new Error("Network Compose source could not be parsed safely.");
13699
13912
  }
@@ -13709,7 +13922,7 @@ function inspectNetworkComposeFiles(input) {
13709
13922
  const env = managedComposeEnvironment(input.workspace);
13710
13923
  delete env.DOCKER_CONTEXT;
13711
13924
  delete env.DOCKER_HOST;
13712
- const result = (0, import_node_child_process25.spawnSync)(
13925
+ const result = (0, import_node_child_process28.spawnSync)(
13713
13926
  "docker",
13714
13927
  [
13715
13928
  "--host",
@@ -13743,8 +13956,8 @@ function inspectNetworkComposeFiles(input) {
13743
13956
  function prepareNetworkComposeFiles(plan, subnet) {
13744
13957
  const overlay = deriveComposeNetworkOverlay(subnet);
13745
13958
  if (overlay.status !== "ready" || !overlay.overlay) throw new Error("Network subnet is invalid.");
13746
- const overlayPath = import_node_path38.default.join(plan.composeDirectory, OVERLAY);
13747
- const ignored = (0, import_node_child_process25.spawnSync)("git", [
13959
+ const overlayPath = import_node_path39.default.join(plan.composeDirectory, OVERLAY);
13960
+ const ignored = (0, import_node_child_process28.spawnSync)("git", [
13748
13961
  "-C",
13749
13962
  plan.composeDirectory,
13750
13963
  "check-ignore",
@@ -13758,9 +13971,9 @@ function prepareNetworkComposeFiles(plan, subnet) {
13758
13971
  const bytes2 = `${MARKER}
13759
13972
  ${import_yaml4.default.stringify(overlay.overlay)}`;
13760
13973
  const assertOwnership = () => {
13761
- if (!import_node_fs33.default.existsSync(overlayPath)) return;
13762
- const stat = import_node_fs33.default.lstatSync(overlayPath);
13763
- if (!stat.isFile() || stat.size > MAX_BYTES2 || import_node_fs33.default.readFileSync(overlayPath, "utf8") !== bytes2)
13974
+ if (!import_node_fs34.default.existsSync(overlayPath)) return;
13975
+ const stat = import_node_fs34.default.lstatSync(overlayPath);
13976
+ if (!stat.isFile() || stat.size > MAX_BYTES2 || import_node_fs34.default.readFileSync(overlayPath, "utf8") !== bytes2)
13764
13977
  throw new Error("Retained network overlay changed; repair is required.");
13765
13978
  };
13766
13979
  assertOwnership();
@@ -13786,7 +13999,7 @@ ${JSON.stringify(extended.config, null, 2)}
13786
13999
  }
13787
14000
 
13788
14001
  // src/core/network-connected-route.ts
13789
- var import_node_child_process26 = require("child_process");
14002
+ var import_node_child_process29 = require("child_process");
13790
14003
  function bounds(cidr) {
13791
14004
  const canonical = parseIPv4Cidr(cidr);
13792
14005
  if (!canonical) throw new Error("Invalid connected route.");
@@ -13811,7 +14024,7 @@ function inspectClaimedBridge(claim2, networkId) {
13811
14024
  const env = { ...process.env };
13812
14025
  delete env.DOCKER_CONTEXT;
13813
14026
  delete env.DOCKER_HOST;
13814
- const result = (0, import_node_child_process26.spawnSync)(
14027
+ const result = (0, import_node_child_process29.spawnSync)(
13815
14028
  "docker",
13816
14029
  [
13817
14030
  "--host",
@@ -13838,7 +14051,7 @@ function inspectClaimedBridge(claim2, networkId) {
13838
14051
  const [first, last] = bounds(claim2.subnet);
13839
14052
  const [address] = bounds(gateway);
13840
14053
  if (address <= first || address >= last) throw new Error();
13841
- const interfaces = (0, import_node_child_process26.spawnSync)("ip", ["-j", "-d", "-4", "address", "show", "dev", bridge], {
14054
+ const interfaces = (0, import_node_child_process29.spawnSync)("ip", ["-j", "-d", "-4", "address", "show", "dev", bridge], {
13842
14055
  encoding: "utf8",
13843
14056
  timeout: 5e3,
13844
14057
  maxBuffer: 65536
@@ -14276,11 +14489,11 @@ var DEFAULT_READINESS_TIMEOUT_MS = 12e4;
14276
14489
  var POLL_INTERVAL_MS = 1e3;
14277
14490
  function assertOverlay(container, repoPath) {
14278
14491
  const workingDir = container.labels["com.docker.compose.project.working_dir"];
14279
- if (!workingDir || !sameWorkspacePath(workingDir, import_node_path39.default.join(repoPath, ".devcontainer"))) {
14492
+ if (!workingDir || !sameWorkspacePath(workingDir, import_node_path40.default.join(repoPath, ".devcontainer"))) {
14280
14493
  throw new Error(`Container '${container.id}' does not belong to the exact worktree.`);
14281
14494
  }
14282
14495
  const configFiles = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").filter(Boolean);
14283
- const expectedOverlay = import_node_path39.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
14496
+ const expectedOverlay = import_node_path40.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
14284
14497
  if (!configFiles.some((configFile) => sameWorkspacePath(configFile, expectedOverlay))) {
14285
14498
  throw new Error(`Container '${container.id}' was not started with ${DEVCONTAINER_OVERLAY}.`);
14286
14499
  }
@@ -14294,7 +14507,7 @@ function assertWorktreeOwnership(container, repoPath) {
14294
14507
  function isInsideWorktree(candidate, repoPath) {
14295
14508
  const normalizedCandidate = comparableWorkspacePath(candidate);
14296
14509
  const normalizedRoot = comparableWorkspacePath(repoPath);
14297
- return normalizedCandidate === normalizedRoot || normalizedCandidate.startsWith(`${normalizedRoot}${import_node_path39.default.sep}`);
14510
+ return normalizedCandidate === normalizedRoot || normalizedCandidate.startsWith(`${normalizedRoot}${import_node_path40.default.sep}`);
14298
14511
  }
14299
14512
  function assertReady(container, label) {
14300
14513
  if (!container.state.Running) {
@@ -14486,7 +14699,7 @@ function hasExactManagedComposeProject(repoPath, state) {
14486
14699
  }).some((container) => {
14487
14700
  const workingDir = container.labels["com.docker.compose.project.working_dir"];
14488
14701
  return Boolean(
14489
- workingDir && sameWorkspacePath(workingDir, import_node_path39.default.join(repoPath, ".devcontainer"))
14702
+ workingDir && sameWorkspacePath(workingDir, import_node_path40.default.join(repoPath, ".devcontainer"))
14490
14703
  );
14491
14704
  });
14492
14705
  } catch {
@@ -14594,7 +14807,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
14594
14807
  target
14595
14808
  });
14596
14809
  if (target.kind === "linked") {
14597
- const workspaceEnv = (0, import_node_child_process27.spawnSync)(
14810
+ const workspaceEnv = (0, import_node_child_process30.spawnSync)(
14598
14811
  "docker",
14599
14812
  ["exec", appContainer.id, "printenv", "WORKSPACE"],
14600
14813
  { encoding: "utf-8", ...networkDockerOptions() }
@@ -14604,7 +14817,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
14604
14817
  `Workspace app container must expose WORKSPACE='${target.workspace}' (got '${workspaceEnv.stdout.trim() || "(empty)"}').`
14605
14818
  );
14606
14819
  }
14607
- const devrouterWorkspaceEnv = (0, import_node_child_process27.spawnSync)(
14820
+ const devrouterWorkspaceEnv = (0, import_node_child_process30.spawnSync)(
14608
14821
  "docker",
14609
14822
  ["exec", appContainer.id, "printenv", "DEVROUTER_WORKSPACE"],
14610
14823
  { encoding: "utf-8", ...networkDockerOptions() }
@@ -14615,7 +14828,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
14615
14828
  );
14616
14829
  }
14617
14830
  }
14618
- const gitCheck = (0, import_node_child_process27.spawnSync)(
14831
+ const gitCheck = (0, import_node_child_process30.spawnSync)(
14619
14832
  "docker",
14620
14833
  [
14621
14834
  "exec",
@@ -14888,14 +15101,14 @@ function resolvePrimaryTarget(repoPath) {
14888
15101
  }
14889
15102
  function isPrimaryCheckout(repoPath) {
14890
15103
  try {
14891
- return import_node_fs34.default.statSync(import_node_path39.default.join(repoPath, ".git")).isDirectory();
15104
+ return import_node_fs35.default.statSync(import_node_path40.default.join(repoPath, ".git")).isDirectory();
14892
15105
  } catch {
14893
15106
  return false;
14894
15107
  }
14895
15108
  }
14896
15109
  function openUrls(urls) {
14897
15110
  for (const url of urls) {
14898
- const opened = (0, import_node_child_process27.spawnSync)("open", [url], { encoding: "utf-8" });
15111
+ const opened = (0, import_node_child_process30.spawnSync)("open", [url], { encoding: "utf-8" });
14899
15112
  if (opened.status !== 0) {
14900
15113
  process.stderr.write(`Warning: could not open '${url}'.
14901
15114
  `);
@@ -14952,8 +15165,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
14952
15165
  const target = options.repair ? resolveRepairTarget(repoPath, linked) : linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
14953
15166
  let devpodId = target.devpodId;
14954
15167
  if (target.kind === "linked") {
14955
- const overlayPath = import_node_path39.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
14956
- if (!import_node_fs34.default.existsSync(overlayPath)) {
15168
+ const overlayPath = import_node_path40.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
15169
+ if (!import_node_fs35.default.existsSync(overlayPath)) {
14957
15170
  throw new Error(`Missing required DevPod compose overlay: ${overlayPath}`);
14958
15171
  }
14959
15172
  }
@@ -15053,7 +15266,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
15053
15266
  }
15054
15267
  const apps = proxyAppsFromConfig(runtime.config);
15055
15268
  const parsedUpstreams = apps.map((app) => parseUpstream(app.upstream));
15056
- const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path39.default.basename(repoPath)) ?? "app";
15269
+ const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path40.default.basename(repoPath)) ?? "app";
15057
15270
  for (const [index, app] of apps.entries()) {
15058
15271
  if (!parsedUpstreams[index].host.startsWith(`${aliasPrefix}-`)) {
15059
15272
  const owner = target.kind === "linked" ? "workspace" : "checkout";
@@ -15178,7 +15391,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
15178
15391
  transitionPhase = "service-start";
15179
15392
  reportLifecycleProgress("service-start");
15180
15393
  claimLifecycleEffect();
15181
- const result = (0, import_node_child_process27.spawnSync)("docker", ["start", ...stopped.map((entry) => entry.id)], {
15394
+ const result = (0, import_node_child_process30.spawnSync)("docker", ["start", ...stopped.map((entry) => entry.id)], {
15182
15395
  ...networkDockerOptions(),
15183
15396
  encoding: "utf-8",
15184
15397
  timeout: DEFAULT_READINESS_TIMEOUT_MS,
@@ -15895,9 +16108,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
15895
16108
  }
15896
16109
  const repairLocked = () => {
15897
16110
  const runtime = resolveWorkspaceRuntimeOrDefault(repoPath);
15898
- import_node_fs34.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
16111
+ import_node_fs35.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
15899
16112
  return withFileLock(
15900
- import_node_path39.default.join(DEVROUTER_HOME, `${runtime}-mutation.lock`),
16113
+ import_node_path40.default.join(DEVROUTER_HOME, `${runtime}-mutation.lock`),
15901
16114
  {
15902
16115
  activity: "Managed runtime repair",
15903
16116
  target: repoPath,
@@ -16111,7 +16324,7 @@ async function executeLifecycleWorker(request, operation) {
16111
16324
  const owned = containers.filter(
16112
16325
  (container) => sameWorkspacePath(
16113
16326
  container.labels["com.docker.compose.project.working_dir"] ?? "",
16114
- import_node_path40.default.join(request.repoPath, ".devcontainer")
16327
+ import_node_path41.default.join(request.repoPath, ".devcontainer")
16115
16328
  )
16116
16329
  );
16117
16330
  const projects = owned.map((container) => container.labels["com.docker.compose.project"]);
@@ -16122,6 +16335,11 @@ async function executeLifecycleWorker(request, operation) {
16122
16335
  ];
16123
16336
  for (const project of stopProjects) inspectManagedStopContainers(project);
16124
16337
  }
16338
+ updateReliabilityOperation(request.identity, (record5) => {
16339
+ if (!matchesFence(record5, request.fence))
16340
+ throw new Error("Lifecycle intent changed before stop work began.");
16341
+ record5.stopWorkStarted = true;
16342
+ });
16125
16343
  } else {
16126
16344
  updateReliabilityOperation(request.identity, (record5) => {
16127
16345
  stepRecord(record5, {
@@ -16217,7 +16435,7 @@ function proveLifecycleStopped() {
16217
16435
  const remaining = inspectWorkspaceContainers().filter(
16218
16436
  (container) => sameWorkspacePath(
16219
16437
  container.labels["com.docker.compose.project.working_dir"] ?? "",
16220
- import_node_path40.default.join(request.repoPath, ".devcontainer")
16438
+ import_node_path41.default.join(request.repoPath, ".devcontainer")
16221
16439
  )
16222
16440
  );
16223
16441
  if (remaining.some(
@@ -16233,6 +16451,7 @@ function proveLifecycleStopped() {
16233
16451
  const settlement = updateReliabilityOperation(request.identity, (record5) => {
16234
16452
  if (!matchesFence(record5, request.fence) || record5.worker)
16235
16453
  throw new Error("Stop proof was superseded or an earlier worker remains.");
16454
+ delete record5.stopWorkStarted;
16236
16455
  if (record5.version === 2) {
16237
16456
  if (record5.capacity) record5.capacity.validUntilMs = 0;
16238
16457
  return record5.state.environmentId;
@@ -16246,7 +16465,7 @@ function proveLifecycleStopped() {
16246
16465
  return void 0;
16247
16466
  });
16248
16467
  if (settlement) {
16249
- const capacity = createLifecycleCapacityStore(import_node_path40.default.join(DEVROUTER_HOME, "controller"));
16468
+ const capacity = createLifecycleCapacityStore(import_node_path41.default.join(DEVROUTER_HOME, "controller"));
16250
16469
  for (let attempt = 0; ; attempt++) {
16251
16470
  try {
16252
16471
  const state = capacity.inspect();
@@ -16271,6 +16490,7 @@ function proveLifecycleStopped() {
16271
16490
  if (!matchesFence(record5, request.fence) || record5.worker || record5.state.environmentId !== settlement || record5.capacity && record5.capacity.validUntilMs !== 0)
16272
16491
  throw new Error("Capacity settlement was superseded before journal confirmation.");
16273
16492
  record5.capacity = null;
16493
+ delete record5.stopWorkStarted;
16274
16494
  if (record5.enrollment) {
16275
16495
  record5.phaseSettlement = null;
16276
16496
  record5.activeProfile = null;
@@ -16324,7 +16544,7 @@ async function devsyExecOutcome(repoPath, command, retainedProof) {
16324
16544
  const launch = () => {
16325
16545
  if (retainedProof) revalidateDevsyExecProof(repoPath, retainedProof);
16326
16546
  claimLifecycleEffect();
16327
- return (0, import_node_child_process28.spawn)("devsy", args, { stdio: "inherit" });
16547
+ return (0, import_node_child_process31.spawn)("devsy", args, { stdio: "inherit" });
16328
16548
  };
16329
16549
  child = retainedProof ? withMutationLock("Execute retained workspace", repoPath, launch) : launch();
16330
16550
  } catch (error) {
@@ -16372,7 +16592,7 @@ function ensureGuidance2(repoPath) {
16372
16592
  return `Run 'devrouter ensure ${repoPath}' first.`;
16373
16593
  }
16374
16594
  function assertRunningDevpod(devpodId, repoPath) {
16375
- const result = (0, import_node_child_process29.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
16595
+ const result = (0, import_node_child_process32.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
16376
16596
  encoding: "utf-8"
16377
16597
  });
16378
16598
  if (result.status !== 0) {
@@ -16551,7 +16771,7 @@ async function devpodExecOutcome(repoPath, command) {
16551
16771
  let child;
16552
16772
  try {
16553
16773
  claimLifecycleEffect();
16554
- child = (0, import_node_child_process29.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
16774
+ child = (0, import_node_child_process32.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
16555
16775
  } catch (error) {
16556
16776
  settled = true;
16557
16777
  reject(
@@ -16608,12 +16828,12 @@ async function devpodExecOutcome(repoPath, command) {
16608
16828
  }
16609
16829
 
16610
16830
  // src/core/workspace-lifecycle.ts
16611
- var import_node_child_process30 = require("child_process");
16612
- var import_node_fs36 = __toESM(require("fs"));
16613
- var import_node_path41 = __toESM(require("path"));
16831
+ var import_node_child_process33 = require("child_process");
16832
+ var import_node_fs37 = __toESM(require("fs"));
16833
+ var import_node_path42 = __toESM(require("path"));
16614
16834
 
16615
16835
  // src/core/route-state.ts
16616
- var import_node_fs35 = __toESM(require("fs"));
16836
+ var import_node_fs36 = __toESM(require("fs"));
16617
16837
  function listRoutesForWorktreePath(worktreePath, routes = listHostRouteState()) {
16618
16838
  return routes.filter((route) => sameWorkspacePath(route.repoPath, worktreePath));
16619
16839
  }