@devrouter/cli 0.0.62 → 0.0.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/devrouter.js CHANGED
@@ -2450,7 +2450,7 @@ function loadRepoConfig(repoPath, read = (file) => import_node_fs6.default.readF
2450
2450
  const config = parseConfig(parsed ?? {}, configPath);
2451
2451
  const requiredVersion = config.devrouter?.version;
2452
2452
  if (requiredVersion && !hasWarnedVersionMismatch) {
2453
- const cliVersion = true ? "0.0.62" : "0.0.0-dev";
2453
+ const cliVersion = true ? "0.0.63" : "0.0.0-dev";
2454
2454
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
2455
2455
  hasWarnedVersionMismatch = true;
2456
2456
  process.stderr.write(
@@ -4634,6 +4634,33 @@ function inspectManagedStopWorkspaceIds(endpoint, composeDirectory) {
4634
4634
  assertUniqueContainerIds(ids);
4635
4635
  return ids;
4636
4636
  }
4637
+ function hasExactManagedStopMissingObjectError(stderr, id3) {
4638
+ if (typeof stderr !== "string") return false;
4639
+ const line = stderr.endsWith("\r\n") ? stderr.slice(0, -2) : stderr.endsWith("\n") ? stderr.slice(0, -1) : stderr;
4640
+ return line.toLowerCase() === `error: no such object: ${id3}`;
4641
+ }
4642
+ function assertManagedStopContainersAbsent(endpoint, ids) {
4643
+ assertManagedStopEndpoint(endpoint);
4644
+ if (!Array.isArray(ids) || ids.length === 0 || ids.length > 256) {
4645
+ throw new Error("Managed stop absence proof requires between 1 and 256 container ids.");
4646
+ }
4647
+ ids.forEach(assertFullContainerId);
4648
+ assertUniqueContainerIds(ids);
4649
+ const env = { ...process.env };
4650
+ delete env.DOCKER_CONTEXT;
4651
+ delete env.DOCKER_HOST;
4652
+ for (const id3 of ids) {
4653
+ const result = (0, import_node_child_process6.spawnSync)("docker", ["--host", endpoint, "inspect", "--format", "{{.Id}}", id3], {
4654
+ env,
4655
+ encoding: "utf-8",
4656
+ timeout: MANAGED_STOP_DOCKER_TIMEOUT_MS,
4657
+ maxBuffer: MANAGED_STOP_DOCKER_MAX_BUFFER
4658
+ });
4659
+ if (result.status !== 1 || result.error || result.signal || result.stdout !== "" && result.stdout !== "\n" && result.stdout !== "\r\n" || !hasExactManagedStopMissingObjectError(result.stderr, id3)) {
4660
+ throw new Error("Managed stop Docker inspection did not prove container absence.");
4661
+ }
4662
+ }
4663
+ }
4637
4664
  function stopPinnedManagedContainer(endpoint, containerId) {
4638
4665
  assertFullContainerId(containerId);
4639
4666
  runManagedStopDocker(["stop", containerId], endpoint, 3e4);
@@ -7142,6 +7169,7 @@ function buildOnboardingPrompt(options = {}) {
7142
7169
  "- TLS: namespaced hosts (`web.<ws>.localhost`) are not covered by the `*.localhost` wildcard; devrouter auto-extends the mkcert cert SANs for active hosts when TLS is enabled.",
7143
7170
  "- Lifecycle: after one-time setup, use `devrouter ensure .` for both primary and linked checkouts; never branch on checkout kind or use live verify as startup. Managed consumer images contain no devrouter package/helper: ensure delivers its matching helper at runtime and invokes an exact captured snapshot of the repository-owned post-start adapter. Keep `.devrouter.yml` as the only consumer-side version pin. Use `devrouter stop .` for a non-destructive pause, `devrouter stop . --delete` only for explicit exact-owner cleanup without removing the checkout, and `devrouter exec . -- <command...>` for container commands. Never substitute raw DevPod/Devsy mutations; they bypass the machine-global ownership lock, which serializes provider mutations in a fair arrival-order queue and lets contenders wait up to thirty minutes with throttled stderr progress before failing with the queue position or holder PID and true durations. `workspace up` creates linked worktrees; destructive worktree removal and GC remain ledger-scoped.",
7144
7171
  "- After a drained interrupted ensure, manual exec can use an exactly proven retained Devsy runtime with the plain local Docker command. Revalidate identity for each tooling command until later preparation reconciles startup; preserve configuration, data and interrupted history. This does not repair absent stop baselines or provide OOM protection.",
7172
+ "- Busy exec waits up to thirty minutes for a positively identified active lifecycle worker on the same checkout, with stderr progress. Do not stop healthy work to make room. Commands stay serial; timeout or cancellation before admission leaves prior work untouched. A lifecycle fence change cancels admission, and uncertain execution is never replayed.",
7145
7173
  "- Ensure reconciles interrupted or never-dispatched ensure after positive worker drainage. Unknown arbitrary exec never replays; use explicit stop to reconcile that uncertainty. Preserve journals and generated configuration if evidence is unavailable. This does not prevent OOM or activate capacity-managed recovery.",
7146
7174
  "- Devsy agent readiness: run `devrouter setup --yes --workspace-runtime devsy` once so Devrouter acquires and verifies the pinned agent in its own machine cache. `doctor` reports `ready`, `missing`, `stale`, or `invalid` without network access, and `ensure` fails before the provider queue when readiness is not `ready`. An explicit `DEVSY_AGENT_BINARY` remains authoritative and must match a pinned official asset.",
7147
7175
  "- A managed adapter plus `postCreateCommand` requires `waitFor` exactly `postCreateCommand` or `postStartCommand`; managed selective config preserves lifecycle fields and changes only `runServices`.",
@@ -8725,6 +8753,9 @@ var init_devcontainer_diagnostics = __esm({
8725
8753
  });
8726
8754
 
8727
8755
  // src/core/route-state.ts
8756
+ function listRoutesForWorktreePath(worktreePath, routes = listHostRouteState()) {
8757
+ return routes.filter((route) => sameWorkspacePath(route.repoPath, worktreePath));
8758
+ }
8728
8759
  function listRoutesForWorktreePaths(worktreePaths) {
8729
8760
  const routes = listHostRouteState();
8730
8761
  const byWorktreePath = /* @__PURE__ */ new Map();
@@ -9584,11 +9615,13 @@ var init_status = __esm({
9584
9615
  });
9585
9616
 
9586
9617
  // src/core/devpod-registry.ts
9587
- function listDevpodWorkspacesRaw() {
9618
+ function listDevpodWorkspacesRaw(options = {}) {
9588
9619
  const result = (0, import_node_child_process12.spawnSync)("devpod", ["list", "--output", "json", "--skip-pro"], {
9589
9620
  encoding: "utf-8"
9590
9621
  });
9591
- if (result.status !== 0) {
9622
+ if (options.allowMissingExecutable && result.error?.code === "ENOENT")
9623
+ return [];
9624
+ if (result.error || result.status !== 0) {
9592
9625
  const details = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
9593
9626
  throw new Error(`devpod list failed: ${details || "devpod is not installed or unavailable"}`);
9594
9627
  }
@@ -10954,7 +10987,56 @@ function proveRetainedManagedStop(state) {
10954
10987
  }
10955
10988
  return result;
10956
10989
  }
10990
+ function absentRegistrationIdentity(state) {
10991
+ resetWorkspaceRuntimeCaches();
10992
+ if (resolveWorkspaceRuntimeOrDefault(state.repoPath) !== "devsy")
10993
+ throw new Error("Stop provider changed.");
10994
+ if (!state.workspace || !isLinkedWorktree(state.repoPath))
10995
+ throw new Error("Absent stop requires a linked workspace.");
10996
+ const record4 = readWorkspaceOwnership(state.repoPath, state.workspace);
10997
+ if (!record4 || record4.workspace !== state.workspace || record4.devpodId !== state.devpodId || !sameWorkspacePath(record4.worktreePath, state.repoPath) || resolveWorktreeWorkspace(state.repoPath) !== state.workspace || inspectWorkspaceOwnership(record4, listGitWorktrees(state.repoPath), void 0).ownerStatus !== "present")
10998
+ throw new Error("Absent stop workspace ownership changed.");
10999
+ for (const entries of [
11000
+ listDevsyWorkspaces(),
11001
+ listDevpodWorkspacesRaw({ allowMissingExecutable: true })
11002
+ ]) {
11003
+ if (entries.some(
11004
+ (entry) => entry.id === state.devpodId || sameWorkspacePath(entry.source.localFolder, state.repoPath)
11005
+ ))
11006
+ throw new Error("Absent stop requires both provider registrations to remain absent.");
11007
+ }
11008
+ return { record: record4, gitCommonDir: resolveGitCommonDir(state.repoPath) };
11009
+ }
11010
+ function proveManagedStop(state) {
11011
+ const baseline = validateManagedStopBaseline(state.stopBaseline, state);
11012
+ const owner = inspectDevsyWorkspaceOwnership(
11013
+ listDevsyWorkspaces(),
11014
+ state.devpodId,
11015
+ state.repoPath
11016
+ );
11017
+ if (owner.status !== "absent")
11018
+ return { status: "retained", containers: proveRetainedManagedStop(state) };
11019
+ if (baseline.sourceContainer) throw new Error("Absent stop cannot prove a source container.");
11020
+ const before = absentRegistrationIdentity(state);
11021
+ if (!(0, import_node_util2.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state))
11022
+ throw new Error("Retained stop generation changed.");
11023
+ if (inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId)
11024
+ throw new Error("Stop daemon changed.");
11025
+ assertManagedStopContainersAbsent(
11026
+ baseline.endpoint,
11027
+ baseline.containers.map((container) => container.id)
11028
+ );
11029
+ const uidBytes = Buffer.byteLength(baseline.uid);
11030
+ const runnerId = uidBytes === 16 || uidBytes === 40 ? baseline.uid : baseline.providerId;
11031
+ if (inspectManagedStopContainers(baseline.project, baseline.endpoint).length !== 0 || inspectManagedStopWorkspaceIds(baseline.endpoint, baseline.composeDirectory).length !== 0 || inspectProviderRunnerContainers(baseline.endpoint, runnerId).length !== 0)
11032
+ throw new Error("Absent stop observed a remaining workspace population.");
11033
+ if (inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId || !(0, import_node_util2.isDeepStrictEqual)(absentRegistrationIdentity(state), before) || !(0, import_node_util2.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state))
11034
+ throw new Error("Absent stop authority changed during inspection.");
11035
+ return { status: "proven-absent", containers: [] };
11036
+ }
10957
11037
  function stopFromManagedBaseline(state) {
11038
+ const proof = proveManagedStop(state);
11039
+ if (proof.status === "proven-absent") return proof.status;
10958
11040
  const baseline = validateManagedStopBaseline(state.stopBaseline, state);
10959
11041
  const stopped = /* @__PURE__ */ new Set();
10960
11042
  for (const initial of proveRetainedManagedStop(state))
@@ -10977,6 +11059,12 @@ function stopFromManagedBaseline(state) {
10977
11059
  }
10978
11060
  if (inspect().some((container) => container.state.Running))
10979
11061
  throw new Error("Retained workloads remain running.");
11062
+ return "retained";
11063
+ }
11064
+ function managedStopRouteReferences(state) {
11065
+ return state.desired.apps.flatMap(
11066
+ (name) => ["http", "tcp"].map((protocol) => ({ repoPath: state.repoPath, name, protocol }))
11067
+ );
10980
11068
  }
10981
11069
  var import_node_os2, import_node_path31, import_node_util2;
10982
11070
  var init_managed_stop_recovery = __esm({
@@ -10986,6 +11074,7 @@ var init_managed_stop_recovery = __esm({
10986
11074
  import_node_path31 = __toESM(require("path"));
10987
11075
  import_node_util2 = require("util");
10988
11076
  init_devpod_environment();
11077
+ init_devpod_registry();
10989
11078
  init_devsy_workspaces();
10990
11079
  init_managed_compose_population();
10991
11080
  init_managed_runtime_state();
@@ -11095,8 +11184,7 @@ function stopRetainedManagedDevsyWorkspace(options) {
11095
11184
  throw new Error("Managed stop requires the exact retained workspace identity.");
11096
11185
  }
11097
11186
  if (state.stopBaseline) {
11098
- stopFromManagedBaseline(state);
11099
- return true;
11187
+ return stopFromManagedBaseline(state) === "proven-absent" ? "proven-absent" : true;
11100
11188
  }
11101
11189
  const workspaceEnv = workspace ? { token: workspace, gitCommonDir: resolveGitCommonDir(repoPath) } : void 0;
11102
11190
  function registration2() {
@@ -11280,12 +11368,14 @@ function inspectExactOwnership(devsyId, worktreePath) {
11280
11368
  }
11281
11369
  function mutateOwnedDevsyWorkspace(action2, devsyId, worktreePath) {
11282
11370
  return withMutationLock(`Devsy ${action2}`, worktreePath, () => {
11283
- if (action2 === "stop" && stopRetainedManagedDevsyWorkspace({
11284
- repoPath: worktreePath,
11285
- devsyId,
11286
- stopProvider: () => runDevsyAction("stop", devsyId)
11287
- }))
11288
- return { status: "changed" };
11371
+ if (action2 === "stop") {
11372
+ const stopped = stopRetainedManagedDevsyWorkspace({
11373
+ repoPath: worktreePath,
11374
+ devsyId,
11375
+ stopProvider: () => runDevsyAction("stop", devsyId)
11376
+ });
11377
+ if (stopped) return { status: stopped === "proven-absent" ? "proven-absent" : "changed" };
11378
+ }
11289
11379
  const before = inspectExactOwnership(devsyId, worktreePath);
11290
11380
  if (before.status === "absent") return { status: "absent" };
11291
11381
  runDevsyAction(action2, devsyId);
@@ -12068,7 +12158,7 @@ async function buildDoctorReport(options = {}) {
12068
12158
  const config = runtimeConfig.config;
12069
12159
  loadedConfig = config;
12070
12160
  loadedWorkspace = runtimeConfig.workspace;
12071
- const cliVersion = true ? "0.0.62" : "0.0.0-dev";
12161
+ const cliVersion = true ? "0.0.63" : "0.0.0-dev";
12072
12162
  const configVersion = config.devrouter?.version;
12073
12163
  if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
12074
12164
  addCheck(checks, {
@@ -13362,7 +13452,7 @@ function applyReliabilityEvent(state, event, nowMs) {
13362
13452
  case "not-started":
13363
13453
  if (next.executionPolicy !== "manual" || next.operation?.kind !== "exec")
13364
13454
  return unchanged(next, "blocked");
13365
- if (next.operation.id !== event.operationId || next.operation.status !== "RUNNING")
13455
+ if (next.operation.id !== event.operationId || !["RUNNING", "DISPATCH_RECORDED"].includes(next.operation.status))
13366
13456
  return unchanged(next, "stale");
13367
13457
  next.operation = { ...next.operation, status: "NOT_LAUNCHED", exitCode: null };
13368
13458
  return transition(next, "accepted");
@@ -13417,6 +13507,11 @@ function sameFence2(record4, fence) {
13417
13507
  ([key, value]) => value === fence[key]
13418
13508
  );
13419
13509
  }
13510
+ function hasDuplicateOperation(record4, requestId, operationId) {
13511
+ return record4.state.operationHistory.some(
13512
+ (operation) => operation.key === requestId || operation.id === operationId
13513
+ );
13514
+ }
13420
13515
  function workerGroupAbsent(pid) {
13421
13516
  try {
13422
13517
  process.kill(-pid, 0);
@@ -13433,7 +13528,7 @@ function signalOwnedGroup(child, signal) {
13433
13528
  if (error.code !== "ESRCH") throw error;
13434
13529
  }
13435
13530
  }
13436
- async function runLifecycleWorker(request) {
13531
+ async function runLifecycleWorker(request, beforeAdmission) {
13437
13532
  if (process.platform === "win32")
13438
13533
  throw new Error("Lifecycle workers require POSIX process-group ownership.");
13439
13534
  const workerPath = import_node_path38.default.join(__dirname, "devrouter-lifecycle-worker.js");
@@ -13445,13 +13540,18 @@ async function runLifecycleWorker(request) {
13445
13540
  execArgv: []
13446
13541
  });
13447
13542
  let ready = false;
13543
+ let closed = false;
13544
+ let registered = false;
13545
+ let dispatchAcknowledged = false;
13546
+ let sendAttempted = false;
13547
+ let readinessTimer;
13448
13548
  let result;
13449
13549
  let cancelled = false;
13450
13550
  let forceTimer;
13451
13551
  let monitor;
13452
13552
  let failure;
13453
13553
  const cancel = () => {
13454
- if (cancelled) return;
13554
+ if (cancelled || closed) return;
13455
13555
  cancelled = true;
13456
13556
  try {
13457
13557
  signalOwnedGroup(child, "SIGTERM");
@@ -13474,20 +13574,56 @@ async function runLifecycleWorker(request) {
13474
13574
  child.once("error", (error) => {
13475
13575
  failure = error;
13476
13576
  });
13477
- child.on("message", (message) => {
13577
+ readinessTimer = setTimeout(() => {
13578
+ failure = new Error("Lifecycle worker readiness timed out before dispatch.");
13579
+ cancel();
13580
+ }, 3e4);
13581
+ child.on("message", async (message) => {
13478
13582
  if (!message || typeof message !== "object") return;
13479
13583
  if ("ready" in message && message.ready === true && !ready) {
13480
13584
  ready = true;
13585
+ if (readinessTimer) clearTimeout(readinessTimer);
13481
13586
  try {
13587
+ await new Promise((resolve2) => setImmediate(resolve2));
13588
+ if (closed) return;
13482
13589
  if (cancelled || !child.pid)
13483
13590
  throw new Error("Lifecycle invocation was cancelled before dispatch.");
13484
13591
  if (request.kind !== "stop") {
13485
13592
  const birth = processBirthIdentity(child.pid);
13486
13593
  if (!birth) throw new Error("Could not prove lifecycle worker incarnation.");
13487
13594
  updateReliabilityOperation(request.identity, (record4) => {
13488
- if (!sameFence2(record4, request.fence) || record4.worker || record4.state.operation?.id !== request.operationId) {
13595
+ beforeAdmission?.();
13596
+ if (!request.admission)
13597
+ throw new Error("Lifecycle operation admission is required.");
13598
+ if (hasDuplicateOperation(record4, request.requestId, request.operationId))
13599
+ throw new Error(
13600
+ "Lifecycle operation request conflicts with an existing operation."
13601
+ );
13602
+ if (!sameFence2(record4, request.fence))
13489
13603
  throw new Error("Lifecycle intent changed before dispatch.");
13604
+ if (record4.worker || record4.revision !== request.admission.expectedRevision) {
13605
+ throw new LifecycleWorkerAdmissionBusyError();
13490
13606
  }
13607
+ const admitted = stepReliability(
13608
+ record4.state,
13609
+ {
13610
+ ...request.fence,
13611
+ type: "operation-request",
13612
+ kind: request.kind === "exec" ? "exec" : "ensure",
13613
+ key: request.requestId,
13614
+ operationId: request.operationId,
13615
+ profile: request.admission.profile,
13616
+ consumer: request.admission.consumer,
13617
+ runtimeRunning: request.admission.runtimeRunning,
13618
+ ...request.admission.recoverInterruptedEnsure ? { recoverInterruptedEnsure: true } : {}
13619
+ },
13620
+ Date.now()
13621
+ );
13622
+ if (admitted.outcome !== "accepted")
13623
+ throw new Error(`Lifecycle admission is ${admitted.outcome}.`);
13624
+ record4.state = admitted.state;
13625
+ record4.outcome = null;
13626
+ request.fence = reliabilityFence(record4.state);
13491
13627
  record4.worker = {
13492
13628
  id: request.workerId,
13493
13629
  operationId: request.operationId,
@@ -13503,6 +13639,7 @@ async function runLifecycleWorker(request) {
13503
13639
  throw new Error("Lifecycle dispatch is blocked.");
13504
13640
  record4.state = transition2.state;
13505
13641
  });
13642
+ registered = true;
13506
13643
  updateReliabilityOperation(request.identity, (record4) => {
13507
13644
  const transition2 = stepReliability(
13508
13645
  record4.state,
@@ -13513,12 +13650,18 @@ async function runLifecycleWorker(request) {
13513
13650
  },
13514
13651
  Date.now()
13515
13652
  );
13516
- record4.state = transition2.state;
13517
13653
  if (!transition2.effects.some((effect2) => effect2.kind === "launch")) {
13518
13654
  throw new Error("Lifecycle dispatch persistence was not acknowledged.");
13519
13655
  }
13656
+ record4.state = transition2.state;
13520
13657
  });
13658
+ dispatchAcknowledged = true;
13521
13659
  }
13660
+ await new Promise((resolve2) => setImmediate(resolve2));
13661
+ if (closed) return;
13662
+ beforeAdmission?.();
13663
+ if (cancelled) throw new Error("Lifecycle invocation was cancelled before dispatch.");
13664
+ sendAttempted = true;
13522
13665
  child.send({ request }, (error) => {
13523
13666
  if (error) {
13524
13667
  failure = error;
@@ -13543,8 +13686,9 @@ async function runLifecycleWorker(request) {
13543
13686
  }
13544
13687
  });
13545
13688
  child.once("close", () => {
13689
+ closed = true;
13546
13690
  try {
13547
- if (request.kind !== "stop") {
13691
+ if (request.kind !== "stop" && registered) {
13548
13692
  updateReliabilityOperation(request.identity, (record4) => {
13549
13693
  if (record4.worker?.id !== request.workerId || record4.worker.pid !== child.pid) return;
13550
13694
  if (!result) {
@@ -13552,7 +13696,7 @@ async function runLifecycleWorker(request) {
13552
13696
  record4.state,
13553
13697
  {
13554
13698
  ...reliabilityFence(record4.state),
13555
- type: "interrupted",
13699
+ type: request.kind === "exec" && dispatchAcknowledged && !sendAttempted ? "not-started" : "interrupted",
13556
13700
  operationId: request.operationId
13557
13701
  },
13558
13702
  Date.now()
@@ -13586,6 +13730,7 @@ async function runLifecycleWorker(request) {
13586
13730
  });
13587
13731
  });
13588
13732
  } finally {
13733
+ if (readinessTimer) clearTimeout(readinessTimer);
13589
13734
  if (monitor) clearInterval(monitor);
13590
13735
  if (forceTimer) clearTimeout(forceTimer);
13591
13736
  process.removeListener("SIGINT", onSignal);
@@ -13595,7 +13740,7 @@ async function runLifecycleWorker(request) {
13595
13740
  function newLifecycleIds() {
13596
13741
  return { requestId: (0, import_node_crypto16.randomUUID)(), operationId: (0, import_node_crypto16.randomUUID)(), workerId: (0, import_node_crypto16.randomUUID)() };
13597
13742
  }
13598
- var import_node_child_process21, import_node_crypto16, import_node_fs31, import_node_path38;
13743
+ var import_node_child_process21, import_node_crypto16, import_node_fs31, import_node_path38, LifecycleWorkerAdmissionBusyError;
13599
13744
  var init_reliability_worker = __esm({
13600
13745
  "src/core/reliability-worker.ts"() {
13601
13746
  "use strict";
@@ -13607,17 +13752,12 @@ var init_reliability_worker = __esm({
13607
13752
  init_reliability_contract();
13608
13753
  init_reliability_model();
13609
13754
  init_reliability_operation_store();
13610
- }
13611
- });
13612
-
13613
- // src/core/managed-host-preparation.ts
13614
- var import_node_child_process22, import_node_fs32;
13615
- var init_managed_host_preparation = __esm({
13616
- "src/core/managed-host-preparation.ts"() {
13617
- "use strict";
13618
- import_node_child_process22 = require("child_process");
13619
- import_node_fs32 = __toESM(require("fs"));
13620
- init_repo_config();
13755
+ LifecycleWorkerAdmissionBusyError = class extends Error {
13756
+ constructor() {
13757
+ super("Lifecycle admission lost to another operation.");
13758
+ this.name = "LifecycleWorkerAdmissionBusyError";
13759
+ }
13760
+ };
13621
13761
  }
13622
13762
  });
13623
13763
 
@@ -13626,7 +13766,7 @@ function sleep(ms) {
13626
13766
  return new Promise((resolve) => setTimeout(resolve, ms));
13627
13767
  }
13628
13768
  function inspectRouterApi(protocol) {
13629
- const result = (0, import_node_child_process23.spawnSync)(
13769
+ const result = (0, import_node_child_process22.spawnSync)(
13630
13770
  "curl",
13631
13771
  [
13632
13772
  "--silent",
@@ -13653,6 +13793,11 @@ function inspectRouterApi(protocol) {
13653
13793
  if (!Array.isArray(value)) {
13654
13794
  return { ok: false, details: `${protocol.toUpperCase()} router API returned a non-array` };
13655
13795
  }
13796
+ if (value.some((item) => !isRecord4(item) || typeof item.name !== "string" || !item.name))
13797
+ return {
13798
+ ok: false,
13799
+ details: `${protocol.toUpperCase()} router API returned malformed entries`
13800
+ };
13656
13801
  return {
13657
13802
  ok: true,
13658
13803
  reachedPageLimit: value.length >= ROUTER_API_PAGE_SIZE,
@@ -13665,6 +13810,9 @@ function inspectRouterApi(protocol) {
13665
13810
  )
13666
13811
  };
13667
13812
  }
13813
+ function isRecord4(value) {
13814
+ return value !== null && typeof value === "object" && !Array.isArray(value);
13815
+ }
13668
13816
  function inspectExpectedRoutes(routes, expectation) {
13669
13817
  const mismatched = [];
13670
13818
  const failures = [];
@@ -13725,7 +13873,7 @@ async function ensureTraefikRouteExpectation(routes, expectation, options) {
13725
13873
  throw new Error(
13726
13874
  "Traefik routes did not reload; shared router restart is disabled for this operation."
13727
13875
  );
13728
- import_node_fs33.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
13876
+ import_node_fs32.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
13729
13877
  return withFileLock(
13730
13878
  ROUTER_RELOAD_LOCK_FILE,
13731
13879
  {
@@ -13756,12 +13904,12 @@ async function ensureTraefikRouteExpectation(routes, expectation, options) {
13756
13904
  async function ensureTraefikRoutesRemoved(routes, options = {}) {
13757
13905
  return ensureTraefikRouteExpectation(routes, "removed", options);
13758
13906
  }
13759
- var import_node_child_process23, import_node_fs33, import_node_path39, ROUTER_API_BASE, ROUTER_RELOAD_LOCK_FILE, ROUTER_API_REQUEST_TIMEOUT_SECONDS, DEFAULT_INITIAL_TIMEOUT_MS, DEFAULT_RECOVERY_TIMEOUT_MS, DEFAULT_POLL_INTERVAL_MS, ROUTER_API_PAGE_SIZE;
13907
+ var import_node_child_process22, import_node_fs32, import_node_path39, ROUTER_API_BASE, ROUTER_RELOAD_LOCK_FILE, ROUTER_API_REQUEST_TIMEOUT_SECONDS, DEFAULT_INITIAL_TIMEOUT_MS, DEFAULT_RECOVERY_TIMEOUT_MS, DEFAULT_POLL_INTERVAL_MS, ROUTER_API_PAGE_SIZE;
13760
13908
  var init_traefik_route_health = __esm({
13761
13909
  "src/core/traefik-route-health.ts"() {
13762
13910
  "use strict";
13763
- import_node_child_process23 = require("child_process");
13764
- import_node_fs33 = __toESM(require("fs"));
13911
+ import_node_child_process22 = require("child_process");
13912
+ import_node_fs32 = __toESM(require("fs"));
13765
13913
  import_node_path39 = __toESM(require("path"));
13766
13914
  init_file_lock();
13767
13915
  init_host_routes();
@@ -13777,6 +13925,17 @@ var init_traefik_route_health = __esm({
13777
13925
  }
13778
13926
  });
13779
13927
 
13928
+ // src/core/managed-host-preparation.ts
13929
+ var import_node_child_process23, import_node_fs33;
13930
+ var init_managed_host_preparation = __esm({
13931
+ "src/core/managed-host-preparation.ts"() {
13932
+ "use strict";
13933
+ import_node_child_process23 = require("child_process");
13934
+ import_node_fs33 = __toESM(require("fs"));
13935
+ init_repo_config();
13936
+ }
13937
+ });
13938
+
13780
13939
  // src/core/workspace-ensure.ts
13781
13940
  function resolveLinkedTarget(repoPath) {
13782
13941
  const snapshots = getWorkspaceRegistrySnapshots();
@@ -13839,6 +13998,11 @@ var init_workspace_ensure = __esm({
13839
13998
  });
13840
13999
 
13841
14000
  // src/core/reliability-lifecycle.ts
14001
+ function matchesFence(record4, fence) {
14002
+ return Object.entries(reliabilityFence(record4.state)).every(
14003
+ ([key, value]) => value === fence[key]
14004
+ );
14005
+ }
13842
14006
  async function withLifecycleOperationLock(repoPath, operation) {
13843
14007
  if (activeWorker && lockHeld) {
13844
14008
  if (!sameWorkspacePath(repoPath, activeWorker.repoPath))
@@ -13886,6 +14050,23 @@ function reconcileDrained(record4) {
13886
14050
  record4.worker = null;
13887
14051
  }
13888
14052
  }
14053
+ function reportBusyExecWait(operationId, waitedMs) {
14054
+ process.stderr.write(
14055
+ `Lifecycle exec ${operationId} is waiting for the existing worker (${Math.ceil(waitedMs / 1e3)}s).
14056
+ `
14057
+ );
14058
+ }
14059
+ function installLifecycleWaitSignals(cancellation) {
14060
+ const onSignal = () => {
14061
+ cancellation.requested = true;
14062
+ };
14063
+ process.once("SIGINT", onSignal);
14064
+ process.once("SIGTERM", onSignal);
14065
+ return () => {
14066
+ process.removeListener("SIGINT", onSignal);
14067
+ process.removeListener("SIGTERM", onSignal);
14068
+ };
14069
+ }
13889
14070
  async function superviseLifecycle(kind, repoPath, options = {}, command) {
13890
14071
  repoPath = comparableWorkspacePath(repoPath);
13891
14072
  if (isLinkedWorktree(repoPath) && !readPersistedWorkspace(repoPath)) {
@@ -13898,58 +14079,113 @@ async function superviseLifecycle(kind, repoPath, options = {}, command) {
13898
14079
  provider: resolveWorkspaceRuntimeOrDefault(repoPath)
13899
14080
  };
13900
14081
  const ids = newLifecycleIds();
13901
- let fence;
13902
- const runtimeRunning = kind === "exec" ? Boolean(resolveRunningWorkspaceContainer(repoPath)) : false;
13903
- const previous = kind === "exec" && identity2.provider === "devsy" ? updateReliabilityOperation(identity2, (record4) => {
13904
- reconcileDrained(record4);
13905
- return record4;
13906
- }) : void 0;
13907
- let retainedExecProof;
13908
- if (previous && !previous.worker && canExecAfterInterruptedEnsure(previous.state)) {
13909
- retainedExecProof = withMutationLock(
13910
- "Prove retained exec",
13911
- repoPath,
13912
- () => captureDevsyExecProof(repoPath)
13913
- );
13914
- }
13915
- updateReliabilityOperation(identity2, (record4) => {
13916
- reconcileDrained(record4);
13917
- if (retainedExecProof && record4.state.operation?.id !== previous?.state.operation?.id)
13918
- throw new Error("Lifecycle operation changed during retained exec proof.");
13919
- if (kind === "stop") {
14082
+ const copiedOptions = { ...options };
14083
+ const copiedCommand = command ? [...command] : void 0;
14084
+ if (kind === "stop") {
14085
+ const fence = updateReliabilityOperation(identity2, (record4) => {
14086
+ reconcileDrained(record4);
13920
14087
  stepRecord(record4, { ...reliabilityFence(record4.state), type: "stop" });
13921
- } else {
13922
- if (record4.worker)
14088
+ return reliabilityFence(record4.state);
14089
+ });
14090
+ return runLifecycleWorker({ kind, repoPath, identity: identity2, ...ids, fence, options: copiedOptions });
14091
+ }
14092
+ const initial = readReliabilityOperation(identity2) ?? updateReliabilityOperation(identity2, (record4) => record4);
14093
+ const expectedFence = reliabilityFence(initial.state);
14094
+ const deadline = Date.now() + BUSY_EXEC_WAIT_MS;
14095
+ const cancellation = { requested: false };
14096
+ const removeSignals = installLifecycleWaitSignals(cancellation);
14097
+ let lastProgress = -Infinity;
14098
+ const assertWaiting = () => {
14099
+ if (cancellation.requested)
14100
+ throw new Error(`Lifecycle exec ${ids.operationId} cancelled before dispatch.`);
14101
+ if (Date.now() >= deadline)
14102
+ throw new Error(
14103
+ `Lifecycle exec ${ids.operationId} admission timed out; command was not launched.`
14104
+ );
14105
+ };
14106
+ try {
14107
+ for (; ; ) {
14108
+ assertWaiting();
14109
+ let previous = readReliabilityOperation(identity2);
14110
+ if (!previous || !matchesFence(previous, expectedFence))
13923
14111
  throw new Error(
13924
- "An earlier lifecycle worker may still be active; use explicit stop to reconcile it."
14112
+ "Lifecycle intent changed while awaiting admission; command was not launched."
13925
14113
  );
13926
- record4.outcome = null;
13927
- stepRecord(record4, {
13928
- ...reliabilityFence(record4.state),
13929
- type: "operation-request",
13930
- kind,
13931
- key: ids.requestId,
13932
- operationId: ids.operationId,
13933
- profile: options.profile ?? record4.state.profile ?? "full",
13934
- consumer: { id: "manual-cli", requiredCapabilities: [], pinned: false },
13935
- runtimeRunning,
13936
- ...retainedExecProof ? { recoverInterruptedEnsure: true } : {}
13937
- });
14114
+ if (hasDuplicateOperation(previous, ids.requestId, ids.operationId))
14115
+ throw new Error("Lifecycle request identity already exists; command was not replayed.");
14116
+ if (previous.worker) {
14117
+ const worker = previous.worker;
14118
+ const birth = processBirthIdentity(worker.pid);
14119
+ if (birth !== worker.birth && workerGroupAbsent(worker.pid)) {
14120
+ previous = updateReliabilityOperation(identity2, (record4) => {
14121
+ reconcileDrained(record4);
14122
+ return record4;
14123
+ });
14124
+ } else {
14125
+ if (!birth || birth !== worker.birth || previous.state.operation?.status === "COMPLETION_UNKNOWN")
14126
+ throw new Error(
14127
+ "Existing lifecycle worker identity or completion is uncertain; preserve its evidence."
14128
+ );
14129
+ if (kind !== "exec")
14130
+ throw new Error("A lifecycle worker is active; wait for its completion before ensure.");
14131
+ const now = Date.now();
14132
+ if (now - lastProgress >= BUSY_EXEC_PROGRESS_MS) {
14133
+ reportBusyExecWait(ids.operationId, now - (deadline - BUSY_EXEC_WAIT_MS));
14134
+ lastProgress = now;
14135
+ }
14136
+ await new Promise(
14137
+ (resolve) => setTimeout(resolve, Math.min(BUSY_EXEC_POLL_MS, deadline - now))
14138
+ );
14139
+ continue;
14140
+ }
14141
+ }
14142
+ if (!previous.worker && previous.state.operation?.status === "NOT_STARTED" && !previous.state.operation.drained)
14143
+ previous = updateReliabilityOperation(identity2, (record4) => {
14144
+ reconcileDrained(record4);
14145
+ return record4;
14146
+ });
14147
+ assertWaiting();
14148
+ const runtimeRunning = kind === "exec" && Boolean(resolveRunningWorkspaceContainer(repoPath));
14149
+ let retainedExecProof;
14150
+ if (kind === "exec" && identity2.provider === "devsy" && canExecAfterInterruptedEnsure(previous.state))
14151
+ retainedExecProof = withMutationLock(
14152
+ "Prove retained exec",
14153
+ repoPath,
14154
+ () => captureDevsyExecProof(repoPath)
14155
+ );
14156
+ assertWaiting();
14157
+ try {
14158
+ return await runLifecycleWorker(
14159
+ {
14160
+ kind,
14161
+ repoPath,
14162
+ identity: identity2,
14163
+ ...ids,
14164
+ fence: { ...expectedFence },
14165
+ options: copiedOptions,
14166
+ ...copiedCommand ? { command: copiedCommand } : {},
14167
+ ...retainedExecProof ? { retainedExecProof } : {},
14168
+ admission: {
14169
+ expectedRevision: previous.revision,
14170
+ profile: copiedOptions.profile ?? previous.state.profile ?? "full",
14171
+ consumer: { id: "manual-cli", requiredCapabilities: [], pinned: false },
14172
+ runtimeRunning,
14173
+ ...retainedExecProof ? { recoverInterruptedEnsure: true } : {}
14174
+ }
14175
+ },
14176
+ assertWaiting
14177
+ );
14178
+ } catch (error) {
14179
+ if (!(error instanceof LifecycleWorkerAdmissionBusyError) || kind !== "exec") throw error;
14180
+ assertWaiting();
14181
+ await new Promise((resolve) => setTimeout(resolve, BUSY_EXEC_POLL_MS));
14182
+ }
13938
14183
  }
13939
- fence = reliabilityFence(record4.state);
13940
- });
13941
- return runLifecycleWorker({
13942
- kind,
13943
- repoPath,
13944
- identity: identity2,
13945
- ...ids,
13946
- fence,
13947
- options,
13948
- ...command ? { command } : {},
13949
- ...retainedExecProof ? { retainedExecProof } : {}
13950
- });
14184
+ } finally {
14185
+ removeSignals();
14186
+ }
13951
14187
  }
13952
- var import_node_path41, activeWorker, lockHeld;
14188
+ var import_node_path41, activeWorker, lockHeld, BUSY_EXEC_WAIT_MS, BUSY_EXEC_POLL_MS, BUSY_EXEC_PROGRESS_MS;
13953
14189
  var init_reliability_lifecycle = __esm({
13954
14190
  "src/core/reliability-lifecycle.ts"() {
13955
14191
  "use strict";
@@ -13966,11 +14202,15 @@ var init_reliability_lifecycle = __esm({
13966
14202
  init_reliability_model();
13967
14203
  init_reliability_operation_store();
13968
14204
  init_reliability_worker();
14205
+ init_traefik_route_health();
13969
14206
  init_workspace();
13970
14207
  init_workspace_ensure();
13971
14208
  init_workspace_runtime();
13972
14209
  init_reliability_context();
13973
14210
  lockHeld = false;
14211
+ BUSY_EXEC_WAIT_MS = 30 * 60 * 1e3;
14212
+ BUSY_EXEC_POLL_MS = 250;
14213
+ BUSY_EXEC_PROGRESS_MS = 1e4;
13974
14214
  }
13975
14215
  });
13976
14216
 
@@ -15632,11 +15872,11 @@ var init_repo_devcontainer = __esm({
15632
15872
  function fail3(message) {
15633
15873
  throw new Error(message);
15634
15874
  }
15635
- function isRecord4(value) {
15875
+ function isRecord5(value) {
15636
15876
  return typeof value === "object" && value !== null && !Array.isArray(value);
15637
15877
  }
15638
15878
  function requireRecord(value, label) {
15639
- if (!isRecord4(value)) fail3(`${label} must be a mapping.`);
15879
+ if (!isRecord5(value)) fail3(`${label} must be a mapping.`);
15640
15880
  return value;
15641
15881
  }
15642
15882
  function requireExactKeys(value, allowed, label) {
@@ -17416,7 +17656,7 @@ var init_workspace_consumption = __esm({
17416
17656
  });
17417
17657
 
17418
17658
  // src/core/workspace-cleanup.ts
17419
- function isRecord5(value) {
17659
+ function isRecord6(value) {
17420
17660
  return value !== null && typeof value === "object" && !Array.isArray(value);
17421
17661
  }
17422
17662
  function isSha(value) {
@@ -17560,14 +17800,14 @@ function parseGitHubChanges(value, project, branch) {
17560
17800
  if (!Array.isArray(value)) return void 0;
17561
17801
  const changes = [];
17562
17802
  for (const item of value) {
17563
- if (!isRecord5(item)) return void 0;
17564
- const repository = isRecord5(item.repository) ? item.repository.nameWithOwner : void 0;
17565
- const headRepository = isRecord5(item.headRepository) ? item.headRepository.nameWithOwner : void 0;
17803
+ if (!isRecord6(item)) return void 0;
17804
+ const repository = isRecord6(item.repository) ? item.repository.nameWithOwner : void 0;
17805
+ const headRepository = isRecord6(item.headRepository) ? item.headRepository.nameWithOwner : void 0;
17566
17806
  if (repository !== project || headRepository !== project || item.headRefName !== branch || !isSha(item.headRefOid) || typeof item.baseRefName !== "string") {
17567
17807
  continue;
17568
17808
  }
17569
17809
  const merged = item.state === "MERGED" && validTimestamp(item.mergedAt);
17570
- const mergeCommit = isRecord5(item.mergeCommit) && isSha(item.mergeCommit.oid) ? item.mergeCommit.oid : void 0;
17810
+ const mergeCommit = isRecord6(item.mergeCommit) && isSha(item.mergeCommit.oid) ? item.mergeCommit.oid : void 0;
17571
17811
  const baseSha = isSha(item.baseRefOid) ? item.baseRefOid : void 0;
17572
17812
  changes.push({
17573
17813
  sourceBranch: branch,
@@ -17584,14 +17824,14 @@ function parseGitLabChanges(value, _project, branch) {
17584
17824
  if (!Array.isArray(value)) return void 0;
17585
17825
  const changes = [];
17586
17826
  for (const item of value) {
17587
- if (!isRecord5(item)) return void 0;
17827
+ if (!isRecord6(item)) return void 0;
17588
17828
  const sourceProjectId = item.source_project_id;
17589
17829
  const targetProjectId = item.target_project_id;
17590
17830
  const sameProject = typeof sourceProjectId === "number" && typeof targetProjectId === "number" && sourceProjectId === targetProjectId;
17591
17831
  if (!sameProject || item.source_branch !== branch || !isSha(item.sha) || typeof item.target_branch !== "string") {
17592
17832
  continue;
17593
17833
  }
17594
- const diffRefs = isRecord5(item.diff_refs) ? item.diff_refs : void 0;
17834
+ const diffRefs = isRecord6(item.diff_refs) ? item.diff_refs : void 0;
17595
17835
  const mergeCommitSha = isSha(item.merge_commit_sha) ? item.merge_commit_sha : void 0;
17596
17836
  const baseSha = diffRefs && isSha(diffRefs.base_sha) ? diffRefs.base_sha : void 0;
17597
17837
  changes.push({
@@ -18413,8 +18653,24 @@ async function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = fals
18413
18653
  "Retained managed runtime has no exact provider registration; routes were preserved."
18414
18654
  );
18415
18655
  }
18416
- const routes = removeWorkspaceRoutesForWorktree(resolved.workspace, resolved.worktreePath);
18417
- await ensureTraefikRoutesRemoved(routes);
18656
+ const retained = action2 === "stop" ? readManagedRuntimeState(resolved.worktreePath, resolved.workspace) : void 0;
18657
+ let absent = false;
18658
+ const remove = () => {
18659
+ if (retained?.stopBaseline) {
18660
+ absent = proveManagedStop(retained).status === "proven-absent";
18661
+ if (absent && listRoutesForWorktreePath(resolved.worktreePath).some(
18662
+ (route) => route.workspace !== resolved.workspace || !retained.desired.apps.includes(route.name)
18663
+ ))
18664
+ throw new Error("Absent stop contains routes outside the retained desired set.");
18665
+ }
18666
+ claimLifecycleEffect();
18667
+ return removeWorkspaceRoutesForWorktree(resolved.workspace, resolved.worktreePath);
18668
+ };
18669
+ const routes = retained?.stopBaseline ? withMutationLock("Revalidate stop routes", resolved.worktreePath, remove) : remove();
18670
+ await ensureTraefikRoutesRemoved([
18671
+ ...routes,
18672
+ ...absent && retained ? managedStopRouteReferences(retained) : []
18673
+ ]);
18418
18674
  if (!quiet) {
18419
18675
  process.stdout.write(
18420
18676
  `Freed ${routes.length} route(s) for workspace '${resolved.workspace}'.
@@ -18425,6 +18681,7 @@ async function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = fals
18425
18681
  ...mutation.status === "changed" ? { devpodId } : {},
18426
18682
  freedRoutes: routes.length,
18427
18683
  providerChanged: mutation.status === "changed",
18684
+ ...absent ? { runtimeAbsent: true } : {},
18428
18685
  workspace: resolved.workspace
18429
18686
  };
18430
18687
  }
@@ -18487,7 +18744,9 @@ var init_workspace_lifecycle = __esm({
18487
18744
  import_node_path49 = __toESM(require("path"));
18488
18745
  init_devpod_mutation();
18489
18746
  init_devpod_workspaces();
18747
+ init_devsy_mutation();
18490
18748
  init_managed_runtime_state();
18749
+ init_managed_stop_recovery();
18491
18750
  init_reliability_lifecycle();
18492
18751
  init_repo_config();
18493
18752
  init_route_state();
@@ -18643,7 +18902,7 @@ var init_version = __esm({
18643
18902
 
18644
18903
  // src/cli.ts
18645
18904
  var import_commander = require("commander");
18646
- var CLI_VERSION = true ? "0.0.62" : "0.0.0-dev";
18905
+ var CLI_VERSION = true ? "0.0.63" : "0.0.0-dev";
18647
18906
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
18648
18907
  function withErrorHandling(action2) {
18649
18908
  return async (...args) => {