@devrouter/cli 0.0.65 → 0.0.66

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.
@@ -3202,7 +3202,16 @@ function registration(state) {
3202
3202
  throw new Error("Stop workspace identity changed.");
3203
3203
  resolveGitCommonDir(state.repoPath);
3204
3204
  } else if (state.workspace !== void 0) throw new Error("Stop checkout identity changed.");
3205
- const value = owner.workspace;
3205
+ return identityOf(owner.workspace);
3206
+ }
3207
+ function managedRunnerId(uid, providerId) {
3208
+ if (uid === void 0) {
3209
+ return providerId;
3210
+ }
3211
+ const bytes = Buffer.byteLength(uid);
3212
+ return bytes === 16 || bytes === 40 ? uid : providerId;
3213
+ }
3214
+ function identityOf(value) {
3206
3215
  return {
3207
3216
  context: value.context ?? "",
3208
3217
  providerId: value.id,
@@ -3347,6 +3356,64 @@ function absentRegistrationIdentity(state) {
3347
3356
  }
3348
3357
  return { record: record5, gitCommonDir: resolveGitCommonDir(state.repoPath) };
3349
3358
  }
3359
+ function replacementObservation(state) {
3360
+ resetWorkspaceRuntimeCaches();
3361
+ if (resolveWorkspaceRuntimeOrDefault(state.repoPath) !== "devsy")
3362
+ throw new Error("Stop provider changed.");
3363
+ if (!state.workspace || !isLinkedWorktree(state.repoPath))
3364
+ throw new Error("Replacement stop requires a linked workspace.");
3365
+ const record5 = readWorkspaceOwnership(state.repoPath, state.workspace);
3366
+ 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")
3367
+ throw new Error("Replacement stop workspace ownership changed.");
3368
+ const owner = inspectDevsyWorkspaceOwnership(
3369
+ listDevsyWorkspaces(),
3370
+ state.devpodId,
3371
+ state.repoPath
3372
+ );
3373
+ if (owner.status !== "owned")
3374
+ throw new Error("Replacement stop requires one exact provider registration.");
3375
+ if (listDevpodWorkspacesRaw({ allowMissingExecutable: true }).some(
3376
+ (entry) => entry.id === state.devpodId || sameWorkspacePath(entry.source.localFolder, state.repoPath)
3377
+ ))
3378
+ throw new Error("Replacement stop requires no competing provider registration.");
3379
+ proveLocalDockerSelection(owner.workspace);
3380
+ return owner.workspace;
3381
+ }
3382
+ function proveReplacementStopAbsence(state, baseline, replacement) {
3383
+ const observation = () => {
3384
+ const workspace = replacementObservation(state);
3385
+ if (workspace.uid !== replacement.uid)
3386
+ throw new Error("Replacement stop registration changed during inspection.");
3387
+ if (resolveManagedStopEndpoint() !== baseline.endpoint)
3388
+ throw new Error("Stop endpoint changed.");
3389
+ if (inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId)
3390
+ throw new Error("Stop daemon changed.");
3391
+ return workspace;
3392
+ };
3393
+ const before = observation();
3394
+ if (!(0, import_node_util2.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state))
3395
+ throw new Error("Retained stop generation changed.");
3396
+ assertManagedStopContainersAbsent(
3397
+ baseline.endpoint,
3398
+ baseline.containers.map((container) => container.id)
3399
+ );
3400
+ const oldRunner = managedRunnerId(baseline.uid, baseline.providerId);
3401
+ const newRunner = managedRunnerId(before.uid, baseline.providerId);
3402
+ const runners = oldRunner === newRunner ? [oldRunner] : [oldRunner, newRunner];
3403
+ const assertEmptyPopulation = () => {
3404
+ if (inspectManagedStopContainers(baseline.project, baseline.endpoint).length !== 0 || inspectManagedStopWorkspaceIds(baseline.endpoint, baseline.composeDirectory).length !== 0 || runners.some((id3) => inspectProviderRunnerContainers(baseline.endpoint, id3).length !== 0))
3405
+ throw new Error("Replacement stop observed a remaining workspace population.");
3406
+ };
3407
+ assertEmptyPopulation();
3408
+ const after = observation();
3409
+ if (after.uid !== before.uid || !(0, import_node_util2.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state))
3410
+ throw new Error("Replacement stop authority changed during inspection.");
3411
+ assertManagedStopContainersAbsent(
3412
+ baseline.endpoint,
3413
+ baseline.containers.map((container) => container.id)
3414
+ );
3415
+ assertEmptyPopulation();
3416
+ }
3350
3417
  function proveManagedStop(state) {
3351
3418
  const baseline = validateManagedStopBaseline(state.stopBaseline, state);
3352
3419
  const owner = inspectDevsyWorkspaceOwnership(
@@ -3354,6 +3421,15 @@ function proveManagedStop(state) {
3354
3421
  state.devpodId,
3355
3422
  state.repoPath
3356
3423
  );
3424
+ if (owner.status === "owned") {
3425
+ const identityKeys = ["context", "providerId", "uid", "sourcePath", "sourceContainer"];
3426
+ const current = identityOf(owner.workspace);
3427
+ const changed = identityKeys.filter((key) => baseline[key] !== current[key]);
3428
+ if (changed.length === 1 && changed[0] === "uid" && !baseline.sourceContainer && !current.sourceContainer && state.workspace !== void 0 && isLinkedWorktree(state.repoPath)) {
3429
+ proveReplacementStopAbsence(state, baseline, { uid: current.uid || void 0 });
3430
+ return { status: "proven-absent", containers: [] };
3431
+ }
3432
+ }
3357
3433
  if (owner.status !== "absent")
3358
3434
  return { status: "retained", containers: proveRetainedManagedStop(state) };
3359
3435
  if (baseline.sourceContainer) throw new Error("Absent stop cannot prove a source container.");
@@ -4787,7 +4863,7 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs14.default.rea
4787
4863
  const config = parseConfig(parsed ?? {}, configPath);
4788
4864
  const requiredVersion = config.devrouter?.version;
4789
4865
  if (requiredVersion && !hasWarnedVersionMismatch) {
4790
- const cliVersion = true ? "0.0.65" : "0.0.0-dev";
4866
+ const cliVersion = true ? "0.0.66" : "0.0.0-dev";
4791
4867
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
4792
4868
  hasWarnedVersionMismatch = true;
4793
4869
  process.stderr.write(
package/dist/devrouter.js CHANGED
@@ -2520,7 +2520,7 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs6.default.read
2520
2520
  const config = parseConfig(parsed ?? {}, configPath);
2521
2521
  const requiredVersion = config.devrouter?.version;
2522
2522
  if (requiredVersion && !hasWarnedVersionMismatch) {
2523
- const cliVersion = true ? "0.0.65" : "0.0.0-dev";
2523
+ const cliVersion = true ? "0.0.66" : "0.0.0-dev";
2524
2524
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
2525
2525
  hasWarnedVersionMismatch = true;
2526
2526
  process.stderr.write(
@@ -12140,6 +12140,96 @@ var init_managed_compose_population = __esm({
12140
12140
  }
12141
12141
  });
12142
12142
 
12143
+ // src/core/devsy-exec-proof.ts
12144
+ function proveLocalDockerSelection(workspace) {
12145
+ const name = workspace.providerName;
12146
+ const context = workspace.context;
12147
+ if (!name || !context || !/^[a-z0-9][a-z0-9-]{0,31}$/.test(name) || !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(context))
12148
+ throw new Error("Retained exec provider configuration identity is unavailable.");
12149
+ const file = import_node_path33.default.join(
12150
+ process.env.DEVSY_HOME || import_node_path33.default.join(import_node_os2.default.homedir(), ".devsy"),
12151
+ "contexts",
12152
+ context,
12153
+ "providers",
12154
+ name,
12155
+ "provider.json"
12156
+ );
12157
+ try {
12158
+ if (import_node_fs29.default.statSync(file).size > 1024 * 1024) throw new Error();
12159
+ const config = JSON.parse(import_node_fs29.default.readFileSync(file, "utf8"));
12160
+ const driver = config.agent?.driver ?? "docker";
12161
+ const configuredPath = config.agent?.docker?.path ?? "";
12162
+ if (typeof configuredPath !== "string") throw new Error();
12163
+ const command = configuredPath === "${DOCKER_PATH}" || configuredPath === "$DOCKER_PATH" ? workspace.dockerPathOption || process.env.DOCKER_PATH || "docker" : configuredPath || "docker";
12164
+ if (config.name !== name || driver !== "docker" || command !== "docker") throw new Error();
12165
+ } catch {
12166
+ throw new Error("Retained exec requires a provider configured for the local Docker command.");
12167
+ }
12168
+ return name;
12169
+ }
12170
+ function captureDevsyExecProof(repoPath) {
12171
+ repoPath = comparableWorkspacePath(repoPath);
12172
+ const registry = listDevsyWorkspaces();
12173
+ const workspace = selectDevsyWorkspace(registry, repoPath);
12174
+ if (!workspace?.uid || !workspace.context || workspace.source.container || inspectDevsyWorkspaceOwnership(registry, workspace.id, repoPath).status !== "owned" || inspectDevsyRuntimeStatus(workspace.id) !== "running")
12175
+ throw new Error("Retained exec requires one exact running Devsy registration.");
12176
+ const providerName = proveLocalDockerSelection(workspace);
12177
+ const endpoint = resolveManagedStopEndpoint();
12178
+ if (!supportsManagedStopBaseline(endpoint))
12179
+ throw new Error("Retained exec requires an exact local Docker endpoint.");
12180
+ const daemon = inspectManagedStopDaemon(endpoint);
12181
+ const candidates = workspaceAppContainers(inspectWorkspaceContainers(), repoPath).filter(
12182
+ (container) => container.state.Running
12183
+ );
12184
+ if (candidates.length !== 1)
12185
+ throw new Error("Retained exec requires one exact running primary container.");
12186
+ const candidate = candidates[0];
12187
+ const project = candidate.labels["com.docker.compose.project"];
12188
+ if (!project) throw new Error("Retained exec primary has no Compose identity.");
12189
+ const pinned = workspaceAppContainers(
12190
+ inspectManagedStopContainers(project, endpoint),
12191
+ repoPath
12192
+ ).filter((container) => container.state.Running);
12193
+ if (pinned.length !== 1 || pinned[0].id !== candidate.id)
12194
+ throw new Error("Retained exec primary changed during inspection.");
12195
+ const mounts = pinned[0].mounts.filter(
12196
+ (mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath)
12197
+ );
12198
+ if (mounts.length !== 1 || !mounts[0].Destination.startsWith("/"))
12199
+ throw new Error("Retained exec requires one exact source mount.");
12200
+ const uidBytes = Buffer.byteLength(workspace.uid);
12201
+ const runner = uidBytes === 16 || uidBytes === 40 ? workspace.uid : workspace.id;
12202
+ const runnerContainers = inspectProviderRunnerContainers(endpoint, runner);
12203
+ if (runnerContainers.length !== 1 || runnerContainers[0] !== candidate.id)
12204
+ throw new Error("Retained exec runner does not select the exact primary container.");
12205
+ if (inspectManagedStopRunnerId(endpoint, candidate.id) !== runner || inspectManagedStopDaemon(endpoint) !== daemon)
12206
+ throw new Error("Retained exec provider or Docker identity changed.");
12207
+ return {
12208
+ repoPath,
12209
+ id: workspace.id,
12210
+ uid: workspace.uid,
12211
+ context: workspace.context,
12212
+ providerName,
12213
+ endpoint,
12214
+ daemon,
12215
+ containerId: candidate.id,
12216
+ workspacePath: mounts[0].Destination
12217
+ };
12218
+ }
12219
+ var import_node_fs29, import_node_os2, import_node_path33, import_node_util2;
12220
+ var init_devsy_exec_proof = __esm({
12221
+ "src/core/devsy-exec-proof.ts"() {
12222
+ "use strict";
12223
+ import_node_fs29 = __toESM(require("fs"));
12224
+ import_node_os2 = __toESM(require("os"));
12225
+ import_node_path33 = __toESM(require("path"));
12226
+ import_node_util2 = require("util");
12227
+ init_devpod_environment();
12228
+ init_devsy_workspaces();
12229
+ init_workspace();
12230
+ }
12231
+ });
12232
+
12143
12233
  // src/core/reliability-context.ts
12144
12234
  function claimLifecycleEffect() {
12145
12235
  claim?.();
@@ -12168,7 +12258,7 @@ function resolveGitCommonDir(repoPath) {
12168
12258
  if (result.status !== 0 || !output2) {
12169
12259
  throw commandError("Could not resolve the Git common directory", repoPath, result.stderr);
12170
12260
  }
12171
- return comparableWorkspacePath(import_node_path33.default.isAbsolute(output2) ? output2 : import_node_path33.default.resolve(repoPath, output2));
12261
+ return comparableWorkspacePath(import_node_path34.default.isAbsolute(output2) ? output2 : import_node_path34.default.resolve(repoPath, output2));
12172
12262
  }
12173
12263
  function resolveGitTopLevel(repoPath) {
12174
12264
  const result = (0, import_node_child_process20.spawnSync)("git", ["-C", repoPath, "rev-parse", "--show-toplevel"], {
@@ -12219,7 +12309,7 @@ function listGitWorktrees(repoPath) {
12219
12309
  return worktrees;
12220
12310
  }
12221
12311
  function ownershipDirectory(repoPath) {
12222
- return import_node_path33.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
12312
+ return import_node_path34.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
12223
12313
  }
12224
12314
  function validateWorkspace(value, label) {
12225
12315
  if (typeof value !== "string" || wsFromBranch(value) !== value) {
@@ -12247,7 +12337,7 @@ function validateRecord(value, expectedWorkspace) {
12247
12337
  `workspace ownership file '${expectedWorkspace}' contains identity '${workspace}'`
12248
12338
  );
12249
12339
  }
12250
- if (typeof candidate.worktreePath !== "string" || !import_node_path33.default.isAbsolute(candidate.worktreePath)) {
12340
+ if (typeof candidate.worktreePath !== "string" || !import_node_path34.default.isAbsolute(candidate.worktreePath)) {
12251
12341
  throw new Error("invalid workspace ownership worktreePath");
12252
12342
  }
12253
12343
  if (candidate.branch !== null && typeof candidate.branch !== "string") {
@@ -12265,7 +12355,7 @@ function validateRecord(value, expectedWorkspace) {
12265
12355
  };
12266
12356
  }
12267
12357
  function recordPath(repoPath, workspace) {
12268
- return import_node_path33.default.join(
12358
+ return import_node_path34.default.join(
12269
12359
  ownershipDirectory(repoPath),
12270
12360
  `${validateWorkspace(workspace, "workspace")}.json`
12271
12361
  );
@@ -12273,7 +12363,7 @@ function recordPath(repoPath, workspace) {
12273
12363
  function readRecordFile(filePath, expectedWorkspace) {
12274
12364
  let parsed;
12275
12365
  try {
12276
- parsed = JSON.parse(import_node_fs29.default.readFileSync(filePath, "utf-8"));
12366
+ parsed = JSON.parse(import_node_fs30.default.readFileSync(filePath, "utf-8"));
12277
12367
  } catch (error) {
12278
12368
  if (error instanceof SyntaxError) {
12279
12369
  throw new Error(`invalid workspace ownership JSON at '${filePath}'`);
@@ -12298,7 +12388,7 @@ function listWorkspaceOwnership(repoPath) {
12298
12388
  function listWorkspaceOwnershipInDirectory(directory) {
12299
12389
  let entries;
12300
12390
  try {
12301
- entries = import_node_fs29.default.readdirSync(directory, { withFileTypes: true });
12391
+ entries = import_node_fs30.default.readdirSync(directory, { withFileTypes: true });
12302
12392
  } catch (error) {
12303
12393
  if (error.code === "ENOENT") return [];
12304
12394
  throw error;
@@ -12306,14 +12396,14 @@ function listWorkspaceOwnershipInDirectory(directory) {
12306
12396
  return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
12307
12397
  const workspace = entry.name.slice(0, -".json".length);
12308
12398
  validateWorkspace(workspace, "filename");
12309
- return readRecordFile(import_node_path33.default.join(directory, entry.name), workspace);
12399
+ return readRecordFile(import_node_path34.default.join(directory, entry.name), workspace);
12310
12400
  });
12311
12401
  }
12312
12402
  function writeWorkspaceOwnershipInDirectory(directory, input2) {
12313
12403
  const workspace = validateWorkspace(input2.workspace, "workspace");
12314
12404
  const devpodId = validateWorkspace(input2.devpodId, "devpodId");
12315
12405
  const worktreePath = comparableWorkspacePath(input2.worktreePath);
12316
- const filePath = import_node_path33.default.join(directory, `${workspace}.json`);
12406
+ const filePath = import_node_path34.default.join(directory, `${workspace}.json`);
12317
12407
  const now = (/* @__PURE__ */ new Date()).toISOString();
12318
12408
  const records = listWorkspaceOwnershipInDirectory(directory);
12319
12409
  const existing = records.find((record6) => record6.workspace === workspace);
@@ -12349,9 +12439,9 @@ function writeWorkspaceOwnershipInDirectory(directory, input2) {
12349
12439
  return record5;
12350
12440
  }
12351
12441
  function removeWorkspaceOwnershipInDirectory(directory, workspace) {
12352
- const filePath = import_node_path33.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
12442
+ const filePath = import_node_path34.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
12353
12443
  try {
12354
- import_node_fs29.default.rmSync(filePath);
12444
+ import_node_fs30.default.rmSync(filePath);
12355
12445
  return true;
12356
12446
  } catch (error) {
12357
12447
  if (error.code === "ENOENT") return false;
@@ -12362,7 +12452,7 @@ function sameOwnershipRecord(left, right) {
12362
12452
  return left.version === right.version && left.workspace === right.workspace && sameWorkspacePath(left.worktreePath, right.worktreePath) && left.branch === right.branch && left.devpodId === right.devpodId && left.createdAt === right.createdAt && left.updatedAt === right.updatedAt;
12363
12453
  }
12364
12454
  function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
12365
- const filePath = import_node_path33.default.join(
12455
+ const filePath = import_node_path34.default.join(
12366
12456
  directory,
12367
12457
  `${validateWorkspace(expected.workspace, "workspace")}.json`
12368
12458
  );
@@ -12374,14 +12464,14 @@ function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
12374
12464
  throw error;
12375
12465
  }
12376
12466
  if (!sameOwnershipRecord(current, expected)) return "changed";
12377
- import_node_fs29.default.rmSync(filePath);
12467
+ import_node_fs30.default.rmSync(filePath);
12378
12468
  return "removed";
12379
12469
  }
12380
12470
  function withWorkspaceOwnershipTransaction(repoPath, operation, options = {}) {
12381
12471
  const directory = ownershipDirectory(repoPath);
12382
- import_node_fs29.default.mkdirSync(directory, { recursive: true });
12472
+ import_node_fs30.default.mkdirSync(directory, { recursive: true });
12383
12473
  return withFileLockSync(
12384
- import_node_path33.default.join(directory, ".lock"),
12474
+ import_node_path34.default.join(directory, ".lock"),
12385
12475
  {
12386
12476
  activity: "workspace ownership transaction",
12387
12477
  target: `'${repoPath}'`,
@@ -12414,7 +12504,7 @@ function persistedWorkspaceOwners(repoPath, worktreePath) {
12414
12504
  try {
12415
12505
  workspace = readPersistedWorkspace(worktree.path);
12416
12506
  } catch (error) {
12417
- if (!import_node_fs29.default.existsSync(worktree.path)) continue;
12507
+ if (!import_node_fs30.default.existsSync(worktree.path)) continue;
12418
12508
  throw error;
12419
12509
  }
12420
12510
  if (!workspace) continue;
@@ -12577,7 +12667,7 @@ function inspectWorkspaceOwnership(record5, worktrees, devpods) {
12577
12667
  if (worktree?.locked) {
12578
12668
  return { ownerStatus: "locked", devpodStatus, worktree };
12579
12669
  }
12580
- if ((!worktree || worktree.prunable) && import_node_fs29.default.existsSync(record5.worktreePath)) {
12670
+ if ((!worktree || worktree.prunable) && import_node_fs30.default.existsSync(record5.worktreePath)) {
12581
12671
  return { ownerStatus: "conflict", devpodStatus, worktree };
12582
12672
  }
12583
12673
  if (!worktree || worktree.prunable) {
@@ -12601,20 +12691,20 @@ function listMissingWorkspaceOwnership(repoPath) {
12601
12691
  (record5) => inspectWorkspaceOwnership(record5, worktrees, void 0).ownerStatus === "missing"
12602
12692
  );
12603
12693
  }
12604
- var import_node_child_process20, import_node_fs29, import_node_path33, READ_ONLY_GIT_ENV, OWNERSHIP_VERSION, OWNERSHIP_DIR;
12694
+ var import_node_child_process20, import_node_fs30, import_node_path34, READ_ONLY_GIT_ENV, OWNERSHIP_VERSION, OWNERSHIP_DIR;
12605
12695
  var init_workspace_ownership = __esm({
12606
12696
  "src/core/workspace-ownership.ts"() {
12607
12697
  "use strict";
12608
12698
  import_node_child_process20 = require("child_process");
12609
- import_node_fs29 = __toESM(require("fs"));
12610
- import_node_path33 = __toESM(require("path"));
12699
+ import_node_fs30 = __toESM(require("fs"));
12700
+ import_node_path34 = __toESM(require("path"));
12611
12701
  init_atomic_file();
12612
12702
  init_devpod_workspaces();
12613
12703
  init_file_lock();
12614
12704
  init_workspace();
12615
12705
  READ_ONLY_GIT_ENV = { ...process.env, GIT_OPTIONAL_LOCKS: "0" };
12616
12706
  OWNERSHIP_VERSION = 1;
12617
- OWNERSHIP_DIR = import_node_path33.default.join("devrouter", "workspaces");
12707
+ OWNERSHIP_DIR = import_node_path34.default.join("devrouter", "workspaces");
12618
12708
  }
12619
12709
  });
12620
12710
 
@@ -12636,7 +12726,16 @@ function registration(state) {
12636
12726
  throw new Error("Stop workspace identity changed.");
12637
12727
  resolveGitCommonDir(state.repoPath);
12638
12728
  } else if (state.workspace !== void 0) throw new Error("Stop checkout identity changed.");
12639
- const value = owner.workspace;
12729
+ return identityOf(owner.workspace);
12730
+ }
12731
+ function managedRunnerId(uid, providerId) {
12732
+ if (uid === void 0) {
12733
+ return providerId;
12734
+ }
12735
+ const bytes = Buffer.byteLength(uid);
12736
+ return bytes === 16 || bytes === 40 ? uid : providerId;
12737
+ }
12738
+ function identityOf(value) {
12640
12739
  return {
12641
12740
  context: value.context ?? "",
12642
12741
  providerId: value.id,
@@ -12671,7 +12770,7 @@ function prove(state, baseline) {
12671
12770
  const containers2 = inspectManagedStopContainers(baseline.project, baseline.endpoint);
12672
12771
  const expected = baseline.containers.map((container) => ({ ...container, mounts: orderedMounts(container.mounts) })).sort((a, b) => compareText(a.id, b.id));
12673
12772
  const observed = containers2.map(identity).sort((a, b) => compareText(a.id, b.id));
12674
- if (!(0, import_node_util2.isDeepStrictEqual)(expected, observed) || containers2.some(
12773
+ if (!(0, import_node_util3.isDeepStrictEqual)(expected, observed) || containers2.some(
12675
12774
  (container) => container.labels["com.docker.compose.project.working_dir"] !== baseline.composeDirectory
12676
12775
  )) {
12677
12776
  throw new Error("Retained container population or immutable identity changed.");
@@ -12680,7 +12779,7 @@ function prove(state, baseline) {
12680
12779
  baseline.endpoint,
12681
12780
  baseline.composeDirectory
12682
12781
  ).sort();
12683
- if (!(0, import_node_util2.isDeepStrictEqual)(workspaceIds, expected.map((container) => container.id).sort())) {
12782
+ if (!(0, import_node_util3.isDeepStrictEqual)(workspaceIds, expected.map((container) => container.id).sort())) {
12684
12783
  throw new Error("Workspace contains an unrecorded container population.");
12685
12784
  }
12686
12785
  const primary = baseline.containers.find(
@@ -12692,18 +12791,18 @@ function prove(state, baseline) {
12692
12791
  if (!baseline.sourceContainer && inspectManagedStopRunnerId(baseline.endpoint, primary.id) !== runnerId) {
12693
12792
  throw new Error("Provider-to-container binding changed.");
12694
12793
  }
12695
- if (!(0, import_node_util2.isDeepStrictEqual)(registration(state), owner) || inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId) {
12794
+ if (!(0, import_node_util3.isDeepStrictEqual)(registration(state), owner) || inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId) {
12696
12795
  throw new Error("Stop authority changed during inspection.");
12697
12796
  }
12698
12797
  return containers2;
12699
12798
  }
12700
12799
  function proveRetainedManagedStop(state) {
12701
12800
  const baseline = validateManagedStopBaseline(state.stopBaseline, state);
12702
- if (!(0, import_node_util2.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state)) {
12801
+ if (!(0, import_node_util3.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state)) {
12703
12802
  throw new Error("Retained stop generation changed.");
12704
12803
  }
12705
12804
  const result = prove(state, baseline);
12706
- if (!(0, import_node_util2.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state)) {
12805
+ if (!(0, import_node_util3.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state)) {
12707
12806
  throw new Error("Retained stop generation changed during inspection.");
12708
12807
  }
12709
12808
  return result;
@@ -12728,6 +12827,64 @@ function absentRegistrationIdentity(state) {
12728
12827
  }
12729
12828
  return { record: record5, gitCommonDir: resolveGitCommonDir(state.repoPath) };
12730
12829
  }
12830
+ function replacementObservation(state) {
12831
+ resetWorkspaceRuntimeCaches();
12832
+ if (resolveWorkspaceRuntimeOrDefault(state.repoPath) !== "devsy")
12833
+ throw new Error("Stop provider changed.");
12834
+ if (!state.workspace || !isLinkedWorktree(state.repoPath))
12835
+ throw new Error("Replacement stop requires a linked workspace.");
12836
+ const record5 = readWorkspaceOwnership(state.repoPath, state.workspace);
12837
+ 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")
12838
+ throw new Error("Replacement stop workspace ownership changed.");
12839
+ const owner = inspectDevsyWorkspaceOwnership(
12840
+ listDevsyWorkspaces(),
12841
+ state.devpodId,
12842
+ state.repoPath
12843
+ );
12844
+ if (owner.status !== "owned")
12845
+ throw new Error("Replacement stop requires one exact provider registration.");
12846
+ if (listDevpodWorkspacesRaw({ allowMissingExecutable: true }).some(
12847
+ (entry) => entry.id === state.devpodId || sameWorkspacePath(entry.source.localFolder, state.repoPath)
12848
+ ))
12849
+ throw new Error("Replacement stop requires no competing provider registration.");
12850
+ proveLocalDockerSelection(owner.workspace);
12851
+ return owner.workspace;
12852
+ }
12853
+ function proveReplacementStopAbsence(state, baseline, replacement) {
12854
+ const observation = () => {
12855
+ const workspace = replacementObservation(state);
12856
+ if (workspace.uid !== replacement.uid)
12857
+ throw new Error("Replacement stop registration changed during inspection.");
12858
+ if (resolveManagedStopEndpoint() !== baseline.endpoint)
12859
+ throw new Error("Stop endpoint changed.");
12860
+ if (inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId)
12861
+ throw new Error("Stop daemon changed.");
12862
+ return workspace;
12863
+ };
12864
+ const before = observation();
12865
+ if (!(0, import_node_util3.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state))
12866
+ throw new Error("Retained stop generation changed.");
12867
+ assertManagedStopContainersAbsent(
12868
+ baseline.endpoint,
12869
+ baseline.containers.map((container) => container.id)
12870
+ );
12871
+ const oldRunner = managedRunnerId(baseline.uid, baseline.providerId);
12872
+ const newRunner = managedRunnerId(before.uid, baseline.providerId);
12873
+ const runners = oldRunner === newRunner ? [oldRunner] : [oldRunner, newRunner];
12874
+ const assertEmptyPopulation = () => {
12875
+ if (inspectManagedStopContainers(baseline.project, baseline.endpoint).length !== 0 || inspectManagedStopWorkspaceIds(baseline.endpoint, baseline.composeDirectory).length !== 0 || runners.some((id4) => inspectProviderRunnerContainers(baseline.endpoint, id4).length !== 0))
12876
+ throw new Error("Replacement stop observed a remaining workspace population.");
12877
+ };
12878
+ assertEmptyPopulation();
12879
+ const after = observation();
12880
+ if (after.uid !== before.uid || !(0, import_node_util3.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state))
12881
+ throw new Error("Replacement stop authority changed during inspection.");
12882
+ assertManagedStopContainersAbsent(
12883
+ baseline.endpoint,
12884
+ baseline.containers.map((container) => container.id)
12885
+ );
12886
+ assertEmptyPopulation();
12887
+ }
12731
12888
  function proveManagedStop(state) {
12732
12889
  const baseline = validateManagedStopBaseline(state.stopBaseline, state);
12733
12890
  const owner = inspectDevsyWorkspaceOwnership(
@@ -12735,11 +12892,20 @@ function proveManagedStop(state) {
12735
12892
  state.devpodId,
12736
12893
  state.repoPath
12737
12894
  );
12895
+ if (owner.status === "owned") {
12896
+ const identityKeys = ["context", "providerId", "uid", "sourcePath", "sourceContainer"];
12897
+ const current = identityOf(owner.workspace);
12898
+ const changed = identityKeys.filter((key) => baseline[key] !== current[key]);
12899
+ if (changed.length === 1 && changed[0] === "uid" && !baseline.sourceContainer && !current.sourceContainer && state.workspace !== void 0 && isLinkedWorktree(state.repoPath)) {
12900
+ proveReplacementStopAbsence(state, baseline, { uid: current.uid || void 0 });
12901
+ return { status: "proven-absent", containers: [] };
12902
+ }
12903
+ }
12738
12904
  if (owner.status !== "absent")
12739
12905
  return { status: "retained", containers: proveRetainedManagedStop(state) };
12740
12906
  if (baseline.sourceContainer) throw new Error("Absent stop cannot prove a source container.");
12741
12907
  const before = absentRegistrationIdentity(state);
12742
- if (!(0, import_node_util2.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state))
12908
+ if (!(0, import_node_util3.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state))
12743
12909
  throw new Error("Retained stop generation changed.");
12744
12910
  if (inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId)
12745
12911
  throw new Error("Stop daemon changed.");
@@ -12751,7 +12917,7 @@ function proveManagedStop(state) {
12751
12917
  const runnerId = uidBytes === 16 || uidBytes === 40 ? baseline.uid : baseline.providerId;
12752
12918
  if (inspectManagedStopContainers(baseline.project, baseline.endpoint).length !== 0 || inspectManagedStopWorkspaceIds(baseline.endpoint, baseline.composeDirectory).length !== 0 || inspectProviderRunnerContainers(baseline.endpoint, runnerId).length !== 0)
12753
12919
  throw new Error("Absent stop observed a remaining workspace population.");
12754
- 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))
12920
+ if (inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId || !(0, import_node_util3.isDeepStrictEqual)(absentRegistrationIdentity(state), before) || !(0, import_node_util3.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state))
12755
12921
  throw new Error("Absent stop authority changed during inspection.");
12756
12922
  return { status: "proven-absent", containers: [] };
12757
12923
  }
@@ -12787,15 +12953,16 @@ function managedStopRouteReferences(state) {
12787
12953
  (name) => ["http", "tcp"].map((protocol) => ({ repoPath: state.repoPath, name, protocol }))
12788
12954
  );
12789
12955
  }
12790
- var import_node_os2, import_node_path34, import_node_util2;
12956
+ var import_node_os3, import_node_path35, import_node_util3;
12791
12957
  var init_managed_stop_recovery = __esm({
12792
12958
  "src/core/managed-stop-recovery.ts"() {
12793
12959
  "use strict";
12794
- import_node_os2 = __toESM(require("os"));
12795
- import_node_path34 = __toESM(require("path"));
12796
- import_node_util2 = require("util");
12960
+ import_node_os3 = __toESM(require("os"));
12961
+ import_node_path35 = __toESM(require("path"));
12962
+ import_node_util3 = require("util");
12797
12963
  init_devpod_environment();
12798
12964
  init_devpod_registry();
12965
+ init_devsy_exec_proof();
12799
12966
  init_devsy_workspaces();
12800
12967
  init_managed_compose_population();
12801
12968
  init_managed_runtime_state();
@@ -12929,8 +13096,8 @@ function stopRetainedManagedDevsyWorkspace(options) {
12929
13096
  if (!context || !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(context) || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(devsyId)) {
12930
13097
  throw new Error("Managed stop requires a valid exact Devsy context and workspace ID.");
12931
13098
  }
12932
- const devsyRoot = import_node_path35.default.resolve(process.env.DEVSY_HOME || import_node_path35.default.join(import_node_os3.default.homedir(), ".devsy"));
12933
- const featureDirectory = import_node_path35.default.join(
13099
+ const devsyRoot = import_node_path36.default.resolve(process.env.DEVSY_HOME || import_node_path36.default.join(import_node_os4.default.homedir(), ".devsy"));
13100
+ const featureDirectory = import_node_path36.default.join(
12934
13101
  devsyRoot,
12935
13102
  "contexts",
12936
13103
  context,
@@ -13034,12 +13201,12 @@ function stopRetainedManagedDevsyWorkspace(options) {
13034
13201
  if (providerFailed) throw providerError;
13035
13202
  return true;
13036
13203
  }
13037
- var import_node_os3, import_node_path35;
13204
+ var import_node_os4, import_node_path36;
13038
13205
  var init_managed_devsy_stop = __esm({
13039
13206
  "src/core/managed-devsy-stop.ts"() {
13040
13207
  "use strict";
13041
- import_node_os3 = __toESM(require("os"));
13042
- import_node_path35 = __toESM(require("path"));
13208
+ import_node_os4 = __toESM(require("os"));
13209
+ import_node_path36 = __toESM(require("path"));
13043
13210
  init_devcontainer_profile();
13044
13211
  init_devpod_environment();
13045
13212
  init_devsy_workspaces();
@@ -13082,14 +13249,14 @@ var init_network_provider_binding = __esm({
13082
13249
 
13083
13250
  // src/core/network-claim-lookup.ts
13084
13251
  function networkOwnerKey(repoPath) {
13085
- if (!import_node_path36.default.isAbsolute(repoPath) || import_node_path36.default.resolve(repoPath) !== repoPath)
13252
+ if (!import_node_path37.default.isAbsolute(repoPath) || import_node_path37.default.resolve(repoPath) !== repoPath)
13086
13253
  throw new Error("Invalid network owner path.");
13087
13254
  return (0, import_node_crypto18.createHash)("sha256").update(repoPath).digest("hex");
13088
13255
  }
13089
13256
  function findOwnedNetworkClaim(repoPath, root = NETWORK_CLAIMS_ROOT) {
13090
13257
  let names;
13091
13258
  try {
13092
- names = import_node_fs30.default.readdirSync(root);
13259
+ names = import_node_fs31.default.readdirSync(root);
13093
13260
  } catch (error) {
13094
13261
  if (error.code === "ENOENT") return void 0;
13095
13262
  throw new Error("Network claims cannot be inspected; repair is required.");
@@ -13098,21 +13265,21 @@ function findOwnedNetworkClaim(repoPath, root = NETWORK_CLAIMS_ROOT) {
13098
13265
  if (files.length > 128) throw new Error("Network daemon inventory exceeds its bound.");
13099
13266
  let found;
13100
13267
  for (const file of files) {
13101
- const full = import_node_path36.default.join(root, file);
13102
- const stat = import_node_fs30.default.lstatSync(full);
13268
+ const full = import_node_path37.default.join(root, file);
13269
+ const stat = import_node_fs31.default.lstatSync(full);
13103
13270
  if (!stat.isFile() || stat.size > NETWORK_CLAIMS_MAX_FILE_BYTES)
13104
13271
  throw new Error("Invalid retained network claim file.");
13105
13272
  let value;
13106
- const descriptor = import_node_fs30.default.openSync(
13273
+ const descriptor = import_node_fs31.default.openSync(
13107
13274
  full,
13108
- import_node_fs30.default.constants.O_RDONLY | import_node_fs30.default.constants.O_NONBLOCK | import_node_fs30.default.constants.O_NOFOLLOW
13275
+ import_node_fs31.default.constants.O_RDONLY | import_node_fs31.default.constants.O_NONBLOCK | import_node_fs31.default.constants.O_NOFOLLOW
13109
13276
  );
13110
13277
  try {
13111
- if (!import_node_fs30.default.fstatSync(descriptor).isFile()) throw new Error();
13278
+ if (!import_node_fs31.default.fstatSync(descriptor).isFile()) throw new Error();
13112
13279
  const bytes = Buffer.alloc(NETWORK_CLAIMS_MAX_FILE_BYTES + 1);
13113
13280
  let size = 0;
13114
13281
  while (size < bytes.length) {
13115
- const count = import_node_fs30.default.readSync(descriptor, bytes, size, bytes.length - size, null);
13282
+ const count = import_node_fs31.default.readSync(descriptor, bytes, size, bytes.length - size, null);
13116
13283
  if (!count) break;
13117
13284
  size += count;
13118
13285
  }
@@ -13122,7 +13289,7 @@ function findOwnedNetworkClaim(repoPath, root = NETWORK_CLAIMS_ROOT) {
13122
13289
  } catch {
13123
13290
  throw new Error("Retained network claim could not be read safely.");
13124
13291
  } finally {
13125
- import_node_fs30.default.closeSync(descriptor);
13292
+ import_node_fs31.default.closeSync(descriptor);
13126
13293
  }
13127
13294
  if (typeof value.daemonId !== "string" || `${(0, import_node_crypto18.createHash)("sha256").update(value.daemonId).digest("hex")}.json` !== file)
13128
13295
  throw new Error("Retained network daemon identity is invalid.");
@@ -13138,13 +13305,13 @@ function findOwnedNetworkClaim(repoPath, root = NETWORK_CLAIMS_ROOT) {
13138
13305
  }
13139
13306
  return found;
13140
13307
  }
13141
- var import_node_crypto18, import_node_fs30, import_node_path36;
13308
+ var import_node_crypto18, import_node_fs31, import_node_path37;
13142
13309
  var init_network_claim_lookup = __esm({
13143
13310
  "src/core/network-claim-lookup.ts"() {
13144
13311
  "use strict";
13145
13312
  import_node_crypto18 = require("crypto");
13146
- import_node_fs30 = __toESM(require("fs"));
13147
- import_node_path36 = __toESM(require("path"));
13313
+ import_node_fs31 = __toESM(require("fs"));
13314
+ import_node_path37 = __toESM(require("path"));
13148
13315
  init_network_claims();
13149
13316
  }
13150
13317
  });
@@ -13182,7 +13349,7 @@ var init_network_provider_effect = __esm({
13182
13349
 
13183
13350
  // src/core/devsy-mutation.ts
13184
13351
  function withMutationLock(activity, target, operation) {
13185
- import_node_fs31.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
13352
+ import_node_fs32.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
13186
13353
  return withFileLockSync(
13187
13354
  DEVSY_MUTATION_LOCK_FILE,
13188
13355
  {
@@ -13263,13 +13430,13 @@ function stopOwnedDevsyWorkspace(devsyId, worktreePath) {
13263
13430
  function deleteOwnedDevsyWorkspace(devsyId, worktreePath) {
13264
13431
  return mutateOwnedDevsyWorkspace("delete", devsyId, worktreePath);
13265
13432
  }
13266
- var import_node_child_process21, import_node_fs31, import_node_path37, DEVSY_MUTATION_LOCK_FILE, DEVSY_MUTATION_WAIT_MS;
13433
+ var import_node_child_process21, import_node_fs32, import_node_path38, DEVSY_MUTATION_LOCK_FILE, DEVSY_MUTATION_WAIT_MS;
13267
13434
  var init_devsy_mutation = __esm({
13268
13435
  "src/core/devsy-mutation.ts"() {
13269
13436
  "use strict";
13270
13437
  import_node_child_process21 = require("child_process");
13271
- import_node_fs31 = __toESM(require("fs"));
13272
- import_node_path37 = __toESM(require("path"));
13438
+ import_node_fs32 = __toESM(require("fs"));
13439
+ import_node_path38 = __toESM(require("path"));
13273
13440
  init_devsy_agent();
13274
13441
  init_devsy_workspaces();
13275
13442
  init_file_lock();
@@ -13278,14 +13445,14 @@ var init_devsy_mutation = __esm({
13278
13445
  init_network_provider_effect();
13279
13446
  init_reliability_context();
13280
13447
  init_router();
13281
- DEVSY_MUTATION_LOCK_FILE = import_node_path37.default.join(DEVROUTER_HOME, "devsy-mutation.lock");
13448
+ DEVSY_MUTATION_LOCK_FILE = import_node_path38.default.join(DEVROUTER_HOME, "devsy-mutation.lock");
13282
13449
  DEVSY_MUTATION_WAIT_MS = 18e5;
13283
13450
  }
13284
13451
  });
13285
13452
 
13286
13453
  // src/core/devpod-mutation.ts
13287
13454
  function withMutationLock2(activity, target, operation) {
13288
- import_node_fs32.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
13455
+ import_node_fs33.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
13289
13456
  return withFileLockSync(
13290
13457
  DEVPOD_MUTATION_LOCK_FILE,
13291
13458
  {
@@ -13375,13 +13542,13 @@ function deleteOwnedDevpodWorkspace(devpodId, worktreePath) {
13375
13542
  resetWorkspaceRuntimeCaches();
13376
13543
  return result;
13377
13544
  }
13378
- var import_node_child_process22, import_node_fs32, import_node_path38, DEVPOD_MUTATION_LOCK_FILE, DEVPOD_MUTATION_WAIT_MS;
13545
+ var import_node_child_process22, import_node_fs33, import_node_path39, DEVPOD_MUTATION_LOCK_FILE, DEVPOD_MUTATION_WAIT_MS;
13379
13546
  var init_devpod_mutation = __esm({
13380
13547
  "src/core/devpod-mutation.ts"() {
13381
13548
  "use strict";
13382
13549
  import_node_child_process22 = require("child_process");
13383
- import_node_fs32 = __toESM(require("fs"));
13384
- import_node_path38 = __toESM(require("path"));
13550
+ import_node_fs33 = __toESM(require("fs"));
13551
+ import_node_path39 = __toESM(require("path"));
13385
13552
  init_devpod_workspaces();
13386
13553
  init_devsy_mutation();
13387
13554
  init_file_lock();
@@ -13390,7 +13557,7 @@ var init_devpod_mutation = __esm({
13390
13557
  init_reliability_context();
13391
13558
  init_router();
13392
13559
  init_workspace_runtime();
13393
- DEVPOD_MUTATION_LOCK_FILE = import_node_path38.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
13560
+ DEVPOD_MUTATION_LOCK_FILE = import_node_path39.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
13394
13561
  DEVPOD_MUTATION_WAIT_MS = 18e5;
13395
13562
  }
13396
13563
  });
@@ -13405,10 +13572,10 @@ function inRepositoryWorkspaceScope(repoPath, worktreePath, livePaths) {
13405
13572
  if (livePaths.some((candidate) => sameWorkspacePath(candidate, worktreePath))) return true;
13406
13573
  const comparableRepo = comparableWorkspacePath(repoPath);
13407
13574
  const comparableWorktree = comparableWorkspacePath(worktreePath);
13408
- const localRoot = import_node_path39.default.join(comparableRepo, "trees") + import_node_path39.default.sep;
13575
+ const localRoot = import_node_path40.default.join(comparableRepo, "trees") + import_node_path40.default.sep;
13409
13576
  if (comparableWorktree.startsWith(localRoot)) return true;
13410
- const legacyPrefix = `${import_node_path39.default.basename(comparableRepo)}-`;
13411
- return import_node_path39.default.dirname(comparableWorktree) === import_node_path39.default.dirname(comparableRepo) && import_node_path39.default.basename(comparableWorktree).startsWith(legacyPrefix);
13577
+ const legacyPrefix = `${import_node_path40.default.basename(comparableRepo)}-`;
13578
+ return import_node_path40.default.dirname(comparableWorktree) === import_node_path40.default.dirname(comparableRepo) && import_node_path40.default.basename(comparableWorktree).startsWith(legacyPrefix);
13412
13579
  }
13413
13580
  function previewActions(devpodStatus, routeCount, includeRecord) {
13414
13581
  const actions = [
@@ -13660,11 +13827,11 @@ function applyWorkspaceGc(plan) {
13660
13827
  candidates
13661
13828
  };
13662
13829
  }
13663
- var import_node_path39;
13830
+ var import_node_path40;
13664
13831
  var init_workspace_gc = __esm({
13665
13832
  "src/core/workspace-gc.ts"() {
13666
13833
  "use strict";
13667
- import_node_path39 = __toESM(require("path"));
13834
+ import_node_path40 = __toESM(require("path"));
13668
13835
  init_devpod_mutation();
13669
13836
  init_devpod_workspaces();
13670
13837
  init_host_routes();
@@ -13734,11 +13901,11 @@ function inspectPostgresCredentials(repoPath, config) {
13734
13901
  parseErrors.push(`${app.name}: ${composeFile} (${message})`);
13735
13902
  continue;
13736
13903
  }
13737
- if (!import_node_fs33.default.existsSync(absolutePath)) {
13904
+ if (!import_node_fs34.default.existsSync(absolutePath)) {
13738
13905
  continue;
13739
13906
  }
13740
13907
  try {
13741
- const raw = import_node_fs33.default.readFileSync(absolutePath, "utf-8");
13908
+ const raw = import_node_fs34.default.readFileSync(absolutePath, "utf-8");
13742
13909
  const parsed = import_yaml5.default.parse(raw);
13743
13910
  const root = asRecord2(parsed);
13744
13911
  const services = asRecord2(root?.services);
@@ -14018,7 +14185,7 @@ async function buildDoctorReport(options = {}) {
14018
14185
  const config = runtimeConfig.config;
14019
14186
  loadedConfig = config;
14020
14187
  loadedWorkspace = runtimeConfig.workspace;
14021
- const cliVersion = true ? "0.0.65" : "0.0.0-dev";
14188
+ const cliVersion = true ? "0.0.66" : "0.0.0-dev";
14022
14189
  const configVersion = config.devrouter?.version;
14023
14190
  if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
14024
14191
  addCheck(checks, {
@@ -14048,9 +14215,9 @@ async function buildDoctorReport(options = {}) {
14048
14215
  (app) => app.docker.composeFiles.map((filePath) => ({
14049
14216
  app: app.name,
14050
14217
  filePath,
14051
- absolutePath: import_node_path40.default.resolve(repo.path, filePath)
14218
+ absolutePath: import_node_path41.default.resolve(repo.path, filePath)
14052
14219
  }))
14053
- ).filter((entry) => !import_node_fs33.default.existsSync(entry.absolutePath));
14220
+ ).filter((entry) => !import_node_fs34.default.existsSync(entry.absolutePath));
14054
14221
  addCheck(checks, {
14055
14222
  id: "repo.compose-files",
14056
14223
  level: missingComposeFiles.length === 0 ? "ok" : "error",
@@ -14093,8 +14260,8 @@ async function buildDoctorReport(options = {}) {
14093
14260
  const missingHostCwds = config.apps.filter((app) => app.runtime === "host").map((app) => ({
14094
14261
  app: app.name,
14095
14262
  cwd: app.hostRun.cwd,
14096
- absolutePath: import_node_path40.default.resolve(repo.path, app.hostRun.cwd)
14097
- })).filter((entry) => !import_node_fs33.default.existsSync(entry.absolutePath));
14263
+ absolutePath: import_node_path41.default.resolve(repo.path, app.hostRun.cwd)
14264
+ })).filter((entry) => !import_node_fs34.default.existsSync(entry.absolutePath));
14098
14265
  addCheck(checks, {
14099
14266
  id: "repo.host-cwd",
14100
14267
  level: missingHostCwds.length === 0 ? "ok" : "error",
@@ -14238,12 +14405,12 @@ async function buildDoctorReport(options = {}) {
14238
14405
  nextSteps
14239
14406
  };
14240
14407
  }
14241
- var import_node_fs33, import_node_path40, import_yaml5, POSTGRES_DEFAULTS;
14408
+ var import_node_fs34, import_node_path41, import_yaml5, POSTGRES_DEFAULTS;
14242
14409
  var init_doctor = __esm({
14243
14410
  "src/core/doctor.ts"() {
14244
14411
  "use strict";
14245
- import_node_fs33 = __toESM(require("fs"));
14246
- import_node_path40 = __toESM(require("path"));
14412
+ import_node_fs34 = __toESM(require("fs"));
14413
+ import_node_path41 = __toESM(require("path"));
14247
14414
  import_yaml5 = __toESM(require("yaml"));
14248
14415
  init_docker();
14249
14416
  init_host_routes();
@@ -14652,96 +14819,6 @@ var init_up = __esm({
14652
14819
  }
14653
14820
  });
14654
14821
 
14655
- // src/core/devsy-exec-proof.ts
14656
- function proveLocalDockerSelection(workspace) {
14657
- const name = workspace.providerName;
14658
- const context = workspace.context;
14659
- if (!name || !context || !/^[a-z0-9][a-z0-9-]{0,31}$/.test(name) || !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(context))
14660
- throw new Error("Retained exec provider configuration identity is unavailable.");
14661
- const file = import_node_path41.default.join(
14662
- process.env.DEVSY_HOME || import_node_path41.default.join(import_node_os4.default.homedir(), ".devsy"),
14663
- "contexts",
14664
- context,
14665
- "providers",
14666
- name,
14667
- "provider.json"
14668
- );
14669
- try {
14670
- if (import_node_fs34.default.statSync(file).size > 1024 * 1024) throw new Error();
14671
- const config = JSON.parse(import_node_fs34.default.readFileSync(file, "utf8"));
14672
- const driver = config.agent?.driver ?? "docker";
14673
- const configuredPath = config.agent?.docker?.path ?? "";
14674
- if (typeof configuredPath !== "string") throw new Error();
14675
- const command = configuredPath === "${DOCKER_PATH}" || configuredPath === "$DOCKER_PATH" ? workspace.dockerPathOption || process.env.DOCKER_PATH || "docker" : configuredPath || "docker";
14676
- if (config.name !== name || driver !== "docker" || command !== "docker") throw new Error();
14677
- } catch {
14678
- throw new Error("Retained exec requires a provider configured for the local Docker command.");
14679
- }
14680
- return name;
14681
- }
14682
- function captureDevsyExecProof(repoPath) {
14683
- repoPath = comparableWorkspacePath(repoPath);
14684
- const registry = listDevsyWorkspaces();
14685
- const workspace = selectDevsyWorkspace(registry, repoPath);
14686
- if (!workspace?.uid || !workspace.context || workspace.source.container || inspectDevsyWorkspaceOwnership(registry, workspace.id, repoPath).status !== "owned" || inspectDevsyRuntimeStatus(workspace.id) !== "running")
14687
- throw new Error("Retained exec requires one exact running Devsy registration.");
14688
- const providerName = proveLocalDockerSelection(workspace);
14689
- const endpoint = resolveManagedStopEndpoint();
14690
- if (!supportsManagedStopBaseline(endpoint))
14691
- throw new Error("Retained exec requires an exact local Docker endpoint.");
14692
- const daemon = inspectManagedStopDaemon(endpoint);
14693
- const candidates = workspaceAppContainers(inspectWorkspaceContainers(), repoPath).filter(
14694
- (container) => container.state.Running
14695
- );
14696
- if (candidates.length !== 1)
14697
- throw new Error("Retained exec requires one exact running primary container.");
14698
- const candidate = candidates[0];
14699
- const project = candidate.labels["com.docker.compose.project"];
14700
- if (!project) throw new Error("Retained exec primary has no Compose identity.");
14701
- const pinned = workspaceAppContainers(
14702
- inspectManagedStopContainers(project, endpoint),
14703
- repoPath
14704
- ).filter((container) => container.state.Running);
14705
- if (pinned.length !== 1 || pinned[0].id !== candidate.id)
14706
- throw new Error("Retained exec primary changed during inspection.");
14707
- const mounts = pinned[0].mounts.filter(
14708
- (mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath)
14709
- );
14710
- if (mounts.length !== 1 || !mounts[0].Destination.startsWith("/"))
14711
- throw new Error("Retained exec requires one exact source mount.");
14712
- const uidBytes = Buffer.byteLength(workspace.uid);
14713
- const runner = uidBytes === 16 || uidBytes === 40 ? workspace.uid : workspace.id;
14714
- const runnerContainers = inspectProviderRunnerContainers(endpoint, runner);
14715
- if (runnerContainers.length !== 1 || runnerContainers[0] !== candidate.id)
14716
- throw new Error("Retained exec runner does not select the exact primary container.");
14717
- if (inspectManagedStopRunnerId(endpoint, candidate.id) !== runner || inspectManagedStopDaemon(endpoint) !== daemon)
14718
- throw new Error("Retained exec provider or Docker identity changed.");
14719
- return {
14720
- repoPath,
14721
- id: workspace.id,
14722
- uid: workspace.uid,
14723
- context: workspace.context,
14724
- providerName,
14725
- endpoint,
14726
- daemon,
14727
- containerId: candidate.id,
14728
- workspacePath: mounts[0].Destination
14729
- };
14730
- }
14731
- var import_node_fs34, import_node_os4, import_node_path41, import_node_util3;
14732
- var init_devsy_exec_proof = __esm({
14733
- "src/core/devsy-exec-proof.ts"() {
14734
- "use strict";
14735
- import_node_fs34 = __toESM(require("fs"));
14736
- import_node_os4 = __toESM(require("os"));
14737
- import_node_path41 = __toESM(require("path"));
14738
- import_node_util3 = require("util");
14739
- init_devpod_environment();
14740
- init_devsy_workspaces();
14741
- init_workspace();
14742
- }
14743
- });
14744
-
14745
14822
  // src/core/reliability-model.ts
14746
14823
  function record4(value) {
14747
14824
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -20862,7 +20939,7 @@ var init_version = __esm({
20862
20939
 
20863
20940
  // src/cli.ts
20864
20941
  var import_commander = require("commander");
20865
- var CLI_VERSION = true ? "0.0.65" : "0.0.0-dev";
20942
+ var CLI_VERSION = true ? "0.0.66" : "0.0.0-dev";
20866
20943
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
20867
20944
  function withErrorHandling(action2) {
20868
20945
  return async (...args) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devrouter/cli",
3
- "version": "0.0.65",
3
+ "version": "0.0.66",
4
4
  "description": "Local dev routing CLI with shared Traefik reverse proxy",
5
5
  "author": "Roland Schlaefli",
6
6
  "homepage": "https://github.com/rschlaefli/devrouter#readme",
@@ -0,0 +1,31 @@
1
+ # Upgrade to devrouter 0.0.66
2
+
3
+ This patch release completes managed stop when Devsy replaces a workspace
4
+ registration UID after the workload is positively absent, releasing lifecycle
5
+ journals stuck in `stopping` so a later `ensure` is admitted again.
6
+
7
+ ## Task
8
+
9
+ Install the published release and update `.devrouter.yml` devrouter.version to
10
+ 0.0.66. No schema migration or configuration change is required. Do not change
11
+ the committed configuration, delete registrations, containers, or source files,
12
+ and do not reset lifecycle state manually: the recovery runs inside the existing
13
+ canonical stop command under its normal locks and proofs.
14
+
15
+ ## Validation
16
+
17
+ On a workspace whose stop was previously rejected with a retained provider
18
+ identity error after a registration replacement, run the canonical
19
+ `devrouter stop <path>` and confirm it reports the workspace stopped with the
20
+ absence proof, then run `devrouter ensure <path>` and confirm admission
21
+ succeeds. Confirm `devrouter status --json` shows the expected ready state and
22
+ that no containers, volumes, or source files were removed beyond the normal
23
+ stop contract. For workspaces that never hit the replacement case, confirm
24
+ existing stop, ensure, and `devrouter doctor` behavior is unchanged.
25
+
26
+ ## Report
27
+
28
+ Report the tested CLI version, the exact workspace path or synthetic fixture,
29
+ the stop command result, the ensure result, route state after the run, and any
30
+ diagnostic failure output verbatim. State explicitly whether the replacement
31
+ case was exercised or only the unchanged retained path.