@devrouter/cli 0.0.44 → 0.0.46

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
@@ -316,13 +316,13 @@ Config-level \`envMap\` on dependency references aliases per-dep vars to app-exp
316
316
 
317
317
  ## Workspace isolation (parallel git worktrees / agents)
318
318
 
319
- Run several worktrees of one repo in parallel without host/route collisions. A **workspace token** spans the DevPod id, devrouter routes, \`\${WORKSPACE}\` proxy upstreams, and devcontainer aliases.
319
+ Run several worktrees of one repo in parallel without host/route collisions. A **workspace token** spans the workspace-runtime id, devrouter routes, \`\${WORKSPACE}\` proxy upstreams, and devcontainer aliases.
320
320
 
321
- - **Identity**: each managed linked worktree stores a local token in Git metadata plus a durable owner record in the repository's Git common directory. The record survives linked-worktree removal and binds the exact path to its DevPod ID. First use reuses an exact-path DevPod or derives a sanitized branch/path slug. Later flags or \`DEVROUTER_WORKSPACE\` may repeat the identity but cannot rename it. Ambiguous identities fail closed. The primary checkout remains non-namespaced.
321
+ - **Identity**: each managed linked worktree stores a local token in Git metadata plus a durable owner record in the repository's Git common directory. The record survives linked-worktree removal and binds the exact path to its workspace-runtime ID. First use reconciles persisted metadata, the exact-path owner record, and both DevPod and Devsy registries. It reuses an established agreement, keeps the readable sanitized branch/path slug when free, or claims a deterministic hash-suffixed fallback on collision before provider or route mutation. Later flags or \`DEVROUTER_WORKSPACE\` may repeat the identity but cannot rename it. Unreadable or conflicting evidence fails closed. The primary checkout remains non-namespaced.
322
322
  - **When active**: hosts auto-namespace (\`web.localhost\` \u2192 \`web.<ws>.localhost\`), \`\${WORKSPACE}\` in \`upstream\` is substituted with the token, and the docker \`router\` key is suffixed per workspace. Managed \`ensure\` rejects every HTTP/TCP proxy upstream outside that exact alias namespace before it mutates DevPod or routes. The runtime config is computed in memory only \u2014 the committed \`.devrouter.yml\` is never rewritten.
323
323
  - **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.
324
324
  - **devcontainer integration**: managed scaffolds list the base compose file, then \`\${localEnv:DEVCONTAINER_COMPOSE_OVERLAY:docker-compose.default.yml}\`; custom repositories may keep another default overlay. Selecting \`.devcontainer/docker-compose.devrouter.yml\` for linked worktrees must pass \`WORKSPACE\` and \`DEVROUTER_WORKSPACE\` across the combined base/overlay config and bind-mount \`\${DEVROUTER_GIT_COMMON_DIR}\` to the same absolute app-container path. The app exposes \`\${WORKSPACE}-app\`; the proxy uses \`upstream: \${WORKSPACE}-app:<port>\`.
325
- - **Lifecycle**: after one-time \`setup\`, use \`ensure .\` for both primary and linked checkouts; never branch manually on checkout kind or use live verify as startup. \`stop .\` is non-destructive; \`stop . --delete\` is explicit exact-owner cleanup without worktree removal; and \`exec . -- <command...>\` runs one-shot commands only in the exact running workspace runtime (DevPod or Devsy). Never substitute raw \`devpod up\`, \`stop\`, \`delete\`, or the Devsy equivalents: they bypass devrouter's machine-global ownership lock. Runtime selection is path-aware: \`DEVROUTER_WORKSPACE_RUNTIME=devpod|devsy\` forces one runtime, an exact-path registry owner wins next, then the machine preference from \`devrouter setup --workspace-runtime\`, then installed-CLI auto-detection. \`workspace up\` creates linked worktrees; destructive worktree removal and GC remain ledger-scoped. Dirty or locked full down fails before side effects.
325
+ - **Lifecycle**: after one-time \`setup\`, use \`ensure .\` for both primary and linked checkouts; never branch manually on checkout kind or use live verify as startup. \`stop .\` is non-destructive; \`stop . --delete\` is explicit exact-owner cleanup without worktree removal; and \`exec . -- <command...>\` runs one-shot commands only in the exact running workspace runtime (DevPod or Devsy). Never substitute raw \`devpod up\`, \`stop\`, \`delete\`, or the Devsy equivalents: they bypass devrouter's 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. Runtime selection is path-aware: \`DEVROUTER_WORKSPACE_RUNTIME=devpod|devsy\` forces one runtime, an exact-path registry owner wins next, then the machine preference from \`devrouter setup --workspace-runtime\`, then installed-CLI auto-detection. \`workspace up\` creates linked worktrees; destructive worktree removal and GC remain ledger-scoped. Dirty or locked full down fails before side effects. A failed Devsy start that reports the agent-binary acquisition failure names the \`DEVSY_AGENT_BINARY\` remediation.
326
326
  - **Managed process identity**: \`ensure\` executes an exact captured adapter snapshot. Default reuse includes command argv, workspace, and adapter SHA-256. Set \`DEVROUTER_PROCESS_FINGERPRINT_ENV\` only to comma-separated non-secret environment names whose values affect runtime identity; secret-like names are rejected and raw values are never persisted.
327
327
  - **Route state**: the versioned Traefik dynamic file is authoritative for both metadata and rendering. JSON is a compatibility mirror; valid headerless generations migrate automatically, while corrupt canonical metadata fails closed.
328
328
  - **Cleanup**: \`workspace cleanup --repo . --inactive-for 30d --json\` is a report-only, no-\`--yes\` report for managed linked workspaces. It joins ownership (\`present|missing|locked|conflict\`), workspace runtime registration, runtime state (\`running|stopped|busy|not-found|absent|unknown\`), checkout, route, advisory activity, and integration evidence without mutating the workspace runtime, routes, ownership, Git, Docker, applications, worktrees, or branches. Local DevPod/Devsy list/status checks always run; \`--check-merged\` alone enables read-only origin and matching GitHub/GitLab checks. Treat \`not-found\` as stale runtime after Docker pruning; busy, unavailable, or conflicting evidence suppresses destructive suggestions. Explicit \`gc\`/\`down\` can remove exact stale registration only after expected-ID \`NotFound\` proof and ownership revalidation. GC never removes Git worktrees, branches, or prune state. Git has no worktree-removal hook.
@@ -898,16 +898,65 @@ function parseLockOwner(value) {
898
898
  return { pid };
899
899
  }
900
900
  }
901
- function isLockOwnerLive(owner) {
902
- if (!isProcessAlive(owner.pid)) return false;
901
+ function parseCanonicalLockOwner(value) {
902
+ const trimmed = value.trim();
903
+ return CANONICAL_OWNER_RE.test(trimmed) ? parseLockOwner(trimmed) : void 0;
904
+ }
905
+ function parseLockRecord(value) {
906
+ const trimmed = value.trim();
907
+ const lastColon = trimmed.lastIndexOf(":");
908
+ if (lastColon >= 0 && CANONICAL_OWNER_RE.test(trimmed.slice(0, lastColon))) {
909
+ const tail = Number(trimmed.slice(lastColon + 1));
910
+ if (Number.isInteger(tail) && tail > 0) {
911
+ return { owner: parseLockOwner(trimmed.slice(0, lastColon)), acquiredAtMs: tail };
912
+ }
913
+ }
914
+ return { owner: parseLockOwner(trimmed) };
915
+ }
916
+ function isLockOwnerLive(owner, cache, now = Date.now()) {
917
+ const cacheKey = `${owner.pid}:${owner.processBirth ?? "legacy"}`;
918
+ if (!isProcessAlive(owner.pid)) {
919
+ cache.delete(cacheKey);
920
+ return false;
921
+ }
903
922
  if (!owner.processBirth) return true;
923
+ const cached = cache.get(cacheKey);
924
+ if (cached && now - cached.checkedAtMs < PROCESS_BIRTH_RECHECK_INTERVAL_MS) {
925
+ return cached.live;
926
+ }
904
927
  const currentBirth = processBirthIdentity(owner.pid);
905
- return currentBirth === void 0 || currentBirth === owner.processBirth;
928
+ const live = currentBirth === void 0 || currentBirth === owner.processBirth;
929
+ cache.set(cacheKey, { checkedAtMs: now, live });
930
+ return live;
906
931
  }
907
932
  function sameFile(left, right) {
908
933
  return left.dev === right.dev && left.ino === right.ino;
909
934
  }
910
- function tryReclaimStaleLock(lockPath, staleLinkPath) {
935
+ function inspectFairQueue(lockPath, ownTicketPath, livenessCache) {
936
+ const directory = import_node_path4.default.dirname(lockPath);
937
+ const prefix = `${import_node_path4.default.basename(lockPath)}.queue.`;
938
+ for (; ; ) {
939
+ const names = import_node_fs4.default.readdirSync(directory).filter((name) => name.startsWith(prefix)).sort();
940
+ const ownName = import_node_path4.default.basename(ownTicketPath);
941
+ const position = names.indexOf(ownName);
942
+ if (position < 0) {
943
+ throw new Error("provider mutation queue ticket disappeared before acquisition");
944
+ }
945
+ const leaderPath = import_node_path4.default.join(directory, names[0]);
946
+ let leader;
947
+ try {
948
+ leader = parseCanonicalLockOwner(import_node_fs4.default.readFileSync(leaderPath, "utf-8"));
949
+ } catch (error) {
950
+ if (error.code === "ENOENT") continue;
951
+ throw error;
952
+ }
953
+ if (leader && isLockOwnerLive(leader, livenessCache)) {
954
+ return { leaderPid: leader.pid, position: position + 1 };
955
+ }
956
+ import_node_fs4.default.rmSync(leaderPath, { force: true });
957
+ }
958
+ }
959
+ function tryReclaimStaleLock(lockPath, staleLinkPath, livenessCache) {
911
960
  let fd;
912
961
  try {
913
962
  fd = import_node_fs4.default.openSync(lockPath, "r");
@@ -918,9 +967,9 @@ function tryReclaimStaleLock(lockPath, staleLinkPath) {
918
967
  throw error;
919
968
  }
920
969
  try {
921
- const owner = parseLockOwner(import_node_fs4.default.readFileSync(fd, "utf-8").trim());
922
- if (owner && isLockOwnerLive(owner)) {
923
- return { kind: "live", pid: owner.pid };
970
+ const record = parseLockRecord(import_node_fs4.default.readFileSync(fd, "utf-8"));
971
+ if (record.owner && isLockOwnerLive(record.owner, livenessCache)) {
972
+ return { kind: "live", pid: record.owner.pid, acquiredAtMs: record.acquiredAtMs };
924
973
  }
925
974
  const staleStat = import_node_fs4.default.fstatSync(fd);
926
975
  if (staleStat.nlink !== 1) {
@@ -965,28 +1014,87 @@ function acquireFileLock(lockPath, options) {
965
1014
  const owner = `${process.pid}:${Buffer.from(processBirth).toString("base64url")}:${ownerId}`;
966
1015
  const candidatePath = `${lockPath}.${process.pid}.${ownerId}.candidate`;
967
1016
  const staleLinkPath = `${candidatePath}.stale`;
1017
+ const enqueuedAtMs = Date.now();
1018
+ const queueTicketPath = `${lockPath}.queue.${String(enqueuedAtMs).padStart(13, "0")}.${String(process.pid).padStart(10, "0")}.${ownerId}`;
1019
+ const queueCandidatePath = `${queueTicketPath}.candidate`;
968
1020
  const deadline = Date.now() + (options.waitMs ?? 0);
1021
+ const waitStartedAt = Date.now();
1022
+ const progressIntervalMs = options.progressIntervalMs ?? DEFAULT_WAIT_PROGRESS_INTERVAL_MS;
1023
+ let lastProgressAt = waitStartedAt;
969
1024
  let reclaimAttempts = 0;
1025
+ const livenessCache = /* @__PURE__ */ new Map();
970
1026
  import_node_fs4.default.writeFileSync(candidatePath, `${owner}
1027
+ `, {
1028
+ encoding: "utf-8",
1029
+ flag: "wx"
1030
+ });
1031
+ if (options.fair) {
1032
+ import_node_fs4.default.writeFileSync(queueCandidatePath, `${owner}
971
1033
  `, { encoding: "utf-8", flag: "wx" });
1034
+ import_node_fs4.default.renameSync(queueCandidatePath, queueTicketPath);
1035
+ }
972
1036
  try {
973
1037
  for (; ; ) {
1038
+ let queueState;
1039
+ if (options.fair) {
1040
+ queueState = inspectFairQueue(lockPath, queueTicketPath, livenessCache);
1041
+ }
1042
+ if (queueState && queueState.position > 1) {
1043
+ const now = Date.now();
1044
+ if (now >= deadline) {
1045
+ const target = options.target ? ` for ${options.target}` : "";
1046
+ const waitedSeconds = Math.round((now - waitStartedAt) / 1e3);
1047
+ throw new Error(
1048
+ `${options.activity} is still queued${target} (position ${queueState.position}, ahead PID ${queueState.leaderPid}); gave up after waiting ${waitedSeconds}s`
1049
+ );
1050
+ }
1051
+ if (options.onWait && now - lastProgressAt >= progressIntervalMs) {
1052
+ lastProgressAt = now;
1053
+ options.onWait({
1054
+ waitingMs: now - waitStartedAt,
1055
+ holderPid: queueState.leaderPid,
1056
+ queuePosition: queueState.position,
1057
+ waitingOn: "queue"
1058
+ });
1059
+ }
1060
+ sleepSync(20);
1061
+ continue;
1062
+ }
1063
+ const acquiredAtMs = Date.now();
1064
+ const record = `${owner}:${acquiredAtMs}`;
1065
+ import_node_fs4.default.writeFileSync(candidatePath, `${record}
1066
+ `, "utf-8");
974
1067
  try {
975
1068
  import_node_fs4.default.linkSync(candidatePath, lockPath);
976
- return owner;
1069
+ return record;
977
1070
  } catch (error) {
978
1071
  if (error.code !== "EEXIST") {
979
1072
  throw error;
980
1073
  }
981
1074
  }
982
- const state = tryReclaimStaleLock(lockPath, staleLinkPath);
1075
+ const state = tryReclaimStaleLock(lockPath, staleLinkPath, livenessCache);
983
1076
  if (state.kind === "reclaimed") {
984
1077
  continue;
985
1078
  }
986
1079
  if (state.kind === "live") {
987
- if (Date.now() >= deadline) {
1080
+ const now = Date.now();
1081
+ if (now >= deadline) {
988
1082
  const target = options.target ? ` for ${options.target}` : "";
989
- throw new Error(`${options.activity} is already running${target} (PID ${state.pid})`);
1083
+ const waitedSeconds = Math.round((now - waitStartedAt) / 1e3);
1084
+ const heldSeconds = state.acquiredAtMs !== void 0 ? `, held for ${Math.round((now - state.acquiredAtMs) / 1e3)}s` : "";
1085
+ throw new Error(
1086
+ `${options.activity} is already running${target} (PID ${state.pid}${heldSeconds}); gave up after waiting ${waitedSeconds}s`
1087
+ );
1088
+ }
1089
+ if (options.onWait && now - lastProgressAt >= progressIntervalMs) {
1090
+ lastProgressAt = now;
1091
+ options.onWait({
1092
+ waitingMs: now - waitStartedAt,
1093
+ holderPid: state.pid,
1094
+ holderHeldMs: state.acquiredAtMs !== void 0 ? Math.max(0, now - state.acquiredAtMs) : void 0,
1095
+ queuePosition: queueState?.position,
1096
+ waitingOn: "lock"
1097
+ });
990
1098
  }
991
1099
  sleepSync(20);
992
1100
  continue;
@@ -1002,6 +1110,8 @@ function acquireFileLock(lockPath, options) {
1002
1110
  } finally {
1003
1111
  import_node_fs4.default.rmSync(candidatePath, { force: true });
1004
1112
  import_node_fs4.default.rmSync(staleLinkPath, { force: true });
1113
+ import_node_fs4.default.rmSync(queueCandidatePath, { force: true });
1114
+ import_node_fs4.default.rmSync(queueTicketPath, { force: true });
1005
1115
  }
1006
1116
  }
1007
1117
  function releaseFileLock(lockPath, owner) {
@@ -1031,19 +1141,33 @@ async function withFileLock(lockPath, options, operation) {
1031
1141
  releaseFileLock(lockPath, owner);
1032
1142
  }
1033
1143
  }
1034
- var import_node_child_process2, import_node_crypto2, import_node_fs4;
1144
+ function createStderrWaitReporter(activity, target) {
1145
+ return (progress) => {
1146
+ const heldSeconds = progress.holderHeldMs !== void 0 ? `, held for ${Math.round(progress.holderHeldMs / 1e3)}s` : "";
1147
+ const status = progress.waitingOn === "queue" ? `waiting in provider queue position ${progress.queuePosition} led by PID ${progress.holderPid}` : progress.queuePosition !== void 0 && progress.queuePosition > 1 ? `waiting in provider queue position ${progress.queuePosition}; provider lock held by PID ${progress.holderPid}${heldSeconds}` : `waiting for the provider lock held by PID ${progress.holderPid}${heldSeconds}`;
1148
+ process.stderr.write(
1149
+ `${activity} for ${target}: ${status}; waited ${Math.round(progress.waitingMs / 1e3)}s so far
1150
+ `
1151
+ );
1152
+ };
1153
+ }
1154
+ var import_node_child_process2, import_node_crypto2, import_node_fs4, import_node_path4, DEFAULT_WAIT_PROGRESS_INTERVAL_MS, PROCESS_BIRTH_RECHECK_INTERVAL_MS, CANONICAL_OWNER_RE;
1035
1155
  var init_file_lock = __esm({
1036
1156
  "src/core/file-lock.ts"() {
1037
1157
  "use strict";
1038
1158
  import_node_child_process2 = require("child_process");
1039
1159
  import_node_crypto2 = require("crypto");
1040
1160
  import_node_fs4 = __toESM(require("fs"));
1161
+ import_node_path4 = __toESM(require("path"));
1162
+ DEFAULT_WAIT_PROGRESS_INTERVAL_MS = 1e4;
1163
+ PROCESS_BIRTH_RECHECK_INTERVAL_MS = 1e3;
1164
+ CANONICAL_OWNER_RE = /^[0-9]+:[A-Za-z0-9_-]+:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
1041
1165
  }
1042
1166
  });
1043
1167
 
1044
1168
  // src/core/workspace.ts
1045
1169
  function comparableWorkspacePath(filePath) {
1046
- const resolved = import_node_path4.default.resolve(filePath);
1170
+ const resolved = import_node_path5.default.resolve(filePath);
1047
1171
  try {
1048
1172
  return import_node_fs5.default.realpathSync.native(resolved);
1049
1173
  } catch {
@@ -1057,8 +1181,23 @@ function wsFromBranch(branch) {
1057
1181
  const slug = branch.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+/, "").slice(0, MAX_WORKSPACE_LENGTH).replace(/-+$/, "");
1058
1182
  return slug.length > 0 ? slug : void 0;
1059
1183
  }
1184
+ function workspaceIdentityFallback(source, attempt) {
1185
+ const readablePrefix = (wsFromBranch(source) ?? "workspace").slice(0, WORKSPACE_IDENTITY_PREFIX_LENGTH).replace(/-+$/, "");
1186
+ const prefix = readablePrefix || "workspace";
1187
+ const hash = (0, import_node_crypto3.createHash)("sha256").update(WORKSPACE_IDENTITY_HASH_DOMAIN, "utf-8").update("\0", "utf-8").update(source, "utf-8").update("\0", "utf-8").update(String(attempt), "utf-8").digest("hex").slice(0, WORKSPACE_IDENTITY_HASH_LENGTH);
1188
+ return `${prefix}-${hash}`;
1189
+ }
1190
+ function workspaceIdentityCandidates(source) {
1191
+ const candidates = [];
1192
+ const legacy = wsFromBranch(source);
1193
+ if (legacy) candidates.push(legacy);
1194
+ for (let attempt = 0; candidates.length < WORKSPACE_IDENTITY_CANDIDATE_LIMIT; attempt += 1) {
1195
+ candidates.push(workspaceIdentityFallback(source, attempt));
1196
+ }
1197
+ return candidates;
1198
+ }
1060
1199
  function isLinkedWorktree(repoPath) {
1061
- const gitPath = import_node_path4.default.join(repoPath, ".git");
1200
+ const gitPath = import_node_path5.default.join(repoPath, ".git");
1062
1201
  let stat;
1063
1202
  try {
1064
1203
  stat = import_node_fs5.default.statSync(gitPath);
@@ -1082,7 +1221,7 @@ function isLinkedWorktree(repoPath) {
1082
1221
  return /(^|\/)worktrees\//.test(gitdir);
1083
1222
  }
1084
1223
  function gitMetadataDir(repoPath) {
1085
- const gitPath = import_node_path4.default.join(repoPath, ".git");
1224
+ const gitPath = import_node_path5.default.join(repoPath, ".git");
1086
1225
  let stat;
1087
1226
  try {
1088
1227
  stat = import_node_fs5.default.statSync(gitPath);
@@ -1099,7 +1238,7 @@ function gitMetadataDir(repoPath) {
1099
1238
  if (!match) {
1100
1239
  return void 0;
1101
1240
  }
1102
- return import_node_path4.default.resolve(repoPath, match[1].trim());
1241
+ return import_node_path5.default.resolve(repoPath, match[1].trim());
1103
1242
  }
1104
1243
  function validatePersistedWorkspace(value) {
1105
1244
  const workspace = value.trim();
@@ -1113,7 +1252,7 @@ function readPersistedWorkspace(repoPath) {
1113
1252
  if (!gitDir) {
1114
1253
  return void 0;
1115
1254
  }
1116
- const metadataPath = import_node_path4.default.join(gitDir, WORKSPACE_METADATA_FILE);
1255
+ const metadataPath = import_node_path5.default.join(gitDir, WORKSPACE_METADATA_FILE);
1117
1256
  try {
1118
1257
  return validatePersistedWorkspace(import_node_fs5.default.readFileSync(metadataPath, "utf-8"));
1119
1258
  } catch (error) {
@@ -1138,8 +1277,8 @@ function persistWorkspace(repoPath, value) {
1138
1277
  if (!gitDir) {
1139
1278
  throw new Error(`cannot persist workspace identity: '${repoPath}' is not a Git checkout`);
1140
1279
  }
1141
- import_node_fs5.default.writeFileSync(import_node_path4.default.join(gitDir, WORKSPACE_METADATA_FILE), `${workspace}
1142
- `, "utf-8");
1280
+ writeFileAtomically(import_node_path5.default.join(gitDir, WORKSPACE_METADATA_FILE), `${workspace}
1281
+ `);
1143
1282
  return workspace;
1144
1283
  }
1145
1284
  async function withWorkspaceLifecycleLock(repoPath, operation) {
@@ -1147,7 +1286,7 @@ async function withWorkspaceLifecycleLock(repoPath, operation) {
1147
1286
  if (!gitDir) {
1148
1287
  throw new Error(`cannot lock workspace lifecycle: '${repoPath}' is not a Git checkout`);
1149
1288
  }
1150
- const lockPath = import_node_path4.default.join(gitDir, WORKSPACE_LOCK_FILE);
1289
+ const lockPath = import_node_path5.default.join(gitDir, WORKSPACE_LOCK_FILE);
1151
1290
  return withFileLock(
1152
1291
  lockPath,
1153
1292
  { activity: "workspace lifecycle", target: `'${repoPath}'` },
@@ -1175,7 +1314,7 @@ function deriveLinkedWorktreeWorkspace(repoPath, linkedWorktreeBranch) {
1175
1314
  return void 0;
1176
1315
  }
1177
1316
  const branch = currentBranch(repoPath);
1178
- return wsFromBranch(branch ?? import_node_path4.default.basename(import_node_path4.default.resolve(repoPath)));
1317
+ return wsFromBranch(branch ?? import_node_path5.default.basename(import_node_path5.default.resolve(repoPath)));
1179
1318
  }
1180
1319
  function resolveWorktreeWorkspace(repoPath, linkedWorktreeBranch) {
1181
1320
  return readPersistedWorkspace(repoPath) ?? deriveLinkedWorktreeWorkspace(repoPath, linkedWorktreeBranch);
@@ -1198,15 +1337,21 @@ function resolveWorkspace(repoPath, override) {
1198
1337
  }
1199
1338
  return deriveLinkedWorktreeWorkspace(repoPath);
1200
1339
  }
1201
- var import_node_child_process3, import_node_fs5, import_node_path4, MAX_WORKSPACE_LENGTH, WORKSPACE_METADATA_FILE, WORKSPACE_LOCK_FILE;
1340
+ var import_node_child_process3, import_node_crypto3, import_node_fs5, import_node_path5, MAX_WORKSPACE_LENGTH, WORKSPACE_IDENTITY_CANDIDATE_LIMIT, WORKSPACE_IDENTITY_HASH_LENGTH, WORKSPACE_IDENTITY_PREFIX_LENGTH, WORKSPACE_IDENTITY_HASH_DOMAIN, WORKSPACE_METADATA_FILE, WORKSPACE_LOCK_FILE;
1202
1341
  var init_workspace = __esm({
1203
1342
  "src/core/workspace.ts"() {
1204
1343
  "use strict";
1205
1344
  import_node_child_process3 = require("child_process");
1345
+ import_node_crypto3 = require("crypto");
1206
1346
  import_node_fs5 = __toESM(require("fs"));
1207
- import_node_path4 = __toESM(require("path"));
1347
+ import_node_path5 = __toESM(require("path"));
1348
+ init_atomic_file();
1208
1349
  init_file_lock();
1209
1350
  MAX_WORKSPACE_LENGTH = 32;
1351
+ WORKSPACE_IDENTITY_CANDIDATE_LIMIT = 16;
1352
+ WORKSPACE_IDENTITY_HASH_LENGTH = 8;
1353
+ WORKSPACE_IDENTITY_PREFIX_LENGTH = MAX_WORKSPACE_LENGTH - WORKSPACE_IDENTITY_HASH_LENGTH - 1;
1354
+ WORKSPACE_IDENTITY_HASH_DOMAIN = "devrouter-workspace-identity-v1";
1210
1355
  WORKSPACE_METADATA_FILE = "devrouter-workspace";
1211
1356
  WORKSPACE_LOCK_FILE = "devrouter-workspace.lock";
1212
1357
  }
@@ -1623,7 +1768,7 @@ function listHostRoutes(tlsEnabled) {
1623
1768
  protocol: isTcp ? `tcp/${tcpProtocol}` : "http",
1624
1769
  appName: route.name,
1625
1770
  serviceName: route.name,
1626
- projectName: import_node_path5.default.basename(route.repoPath),
1771
+ projectName: import_node_path6.default.basename(route.repoPath),
1627
1772
  hosts: [route.host],
1628
1773
  // TCP routes are reached via an SNI-aware client on the shared protocol
1629
1774
  // port; surface a protocol-scheme URL rather than http(s)://.
@@ -1638,12 +1783,12 @@ function listHostRoutes(tlsEnabled) {
1638
1783
  };
1639
1784
  });
1640
1785
  }
1641
- var import_node_fs6, import_node_path5, import_node_util, import_yaml, LOOPBACK_HOSTS, UPSTREAM_RE, TCP_TLS_ALPN_PROTOCOLS, ROUTE_METADATA_PREFIX, ROUTE_METADATA_FAMILY_PREFIX, STATE_LOCK_FILE;
1786
+ var import_node_fs6, import_node_path6, import_node_util, import_yaml, LOOPBACK_HOSTS, UPSTREAM_RE, TCP_TLS_ALPN_PROTOCOLS, ROUTE_METADATA_PREFIX, ROUTE_METADATA_FAMILY_PREFIX, STATE_LOCK_FILE;
1642
1787
  var init_host_routes = __esm({
1643
1788
  "src/core/host-routes.ts"() {
1644
1789
  "use strict";
1645
1790
  import_node_fs6 = __toESM(require("fs"));
1646
- import_node_path5 = __toESM(require("path"));
1791
+ import_node_path6 = __toESM(require("path"));
1647
1792
  import_node_util = require("util");
1648
1793
  import_yaml = __toESM(require("yaml"));
1649
1794
  init_atomic_file();
@@ -2491,10 +2636,10 @@ function appToNodeValue(app) {
2491
2636
  return value;
2492
2637
  }
2493
2638
  function resolveRepoPath(repoPath) {
2494
- return import_node_path6.default.resolve(repoPath ?? process.cwd());
2639
+ return import_node_path7.default.resolve(repoPath ?? process.cwd());
2495
2640
  }
2496
2641
  function getRepoConfigPath(repoPath) {
2497
- return import_node_path6.default.join(resolveRepoPath(repoPath), CONFIG_FILE_NAME);
2642
+ return import_node_path7.default.join(resolveRepoPath(repoPath), CONFIG_FILE_NAME);
2498
2643
  }
2499
2644
  function loadRepoConfig(repoPath) {
2500
2645
  const resolvedRepoPath = resolveRepoPath(repoPath);
@@ -2509,7 +2654,7 @@ function loadRepoConfig(repoPath) {
2509
2654
  const config = parseConfig(parsed ?? {}, configPath);
2510
2655
  const requiredVersion = config.devrouter?.version;
2511
2656
  if (requiredVersion && !hasWarnedVersionMismatch) {
2512
- const cliVersion = true ? "0.0.44" : "0.0.0-dev";
2657
+ const cliVersion = true ? "0.0.46" : "0.0.0-dev";
2513
2658
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
2514
2659
  hasWarnedVersionMismatch = true;
2515
2660
  process.stderr.write(
@@ -2542,7 +2687,7 @@ function initRepoConfig(repoPath, options = {}) {
2542
2687
  }
2543
2688
  } : {},
2544
2689
  project: {
2545
- name: import_node_path6.default.basename(resolvedRepoPath)
2690
+ name: import_node_path7.default.basename(resolvedRepoPath)
2546
2691
  },
2547
2692
  apps: []
2548
2693
  };
@@ -2771,7 +2916,7 @@ function namespaceHost(host, workspace) {
2771
2916
  return `${base}.${workspace}${suffix}`;
2772
2917
  }
2773
2918
  function applyWorkspace(config, workspace, repoPath) {
2774
- const defaultToken = wsFromBranch(config.project?.name ?? import_node_path6.default.basename(import_node_path6.default.resolve(repoPath))) ?? "app";
2919
+ const defaultToken = wsFromBranch(config.project?.name ?? import_node_path7.default.basename(import_node_path7.default.resolve(repoPath))) ?? "app";
2775
2920
  const substitutionToken = workspace ?? defaultToken;
2776
2921
  const next = structuredClone(config);
2777
2922
  for (const app of next.apps) {
@@ -2853,12 +2998,12 @@ function resolveAppDependencies(config, app) {
2853
2998
  }
2854
2999
  return results;
2855
3000
  }
2856
- var import_node_fs7, import_node_path6, import_yaml2, hasWarnedVersionMismatch, CONFIG_FILE_NAME, DEFAULT_TCP_PROTOCOL, VALID_HOSTNAME_RE, DEVROUTER_VERSION_RE, VALID_ENV_NAME_RE, VALID_ENV_VAR_RE, UPSTREAM_TEMPLATE_RE, MAX_COMMAND_LENGTH, DEFAULT_HOST_STRATEGY, PROFILE_NAME_RE, MAX_PROFILES;
3001
+ var import_node_fs7, import_node_path7, import_yaml2, hasWarnedVersionMismatch, CONFIG_FILE_NAME, DEFAULT_TCP_PROTOCOL, VALID_HOSTNAME_RE, DEVROUTER_VERSION_RE, VALID_ENV_NAME_RE, VALID_ENV_VAR_RE, UPSTREAM_TEMPLATE_RE, MAX_COMMAND_LENGTH, DEFAULT_HOST_STRATEGY, PROFILE_NAME_RE, MAX_PROFILES;
2857
3002
  var init_repo_config = __esm({
2858
3003
  "src/core/repo-config.ts"() {
2859
3004
  "use strict";
2860
3005
  import_node_fs7 = __toESM(require("fs"));
2861
- import_node_path6 = __toESM(require("path"));
3006
+ import_node_path7 = __toESM(require("path"));
2862
3007
  import_yaml2 = __toESM(require("yaml"));
2863
3008
  init_capabilities();
2864
3009
  init_host_routes();
@@ -3035,11 +3180,11 @@ function buildOnboardingPrompt(options = {}) {
3035
3180
  "- Use `devrouter status --repo <REPO_PATH> --json` and `devrouter doctor --repo <REPO_PATH> --json` to inspect managed desired/active resources, fingerprints, transition state, and values-free drift.",
3036
3181
  "",
3037
3182
  "Workspace isolation (parallel git worktrees / agents):",
3038
- `- A "workspace token" lets several worktrees of one repo run in parallel without host/route collisions. Each managed linked worktree has a local Git token plus a durable owner record in the repository's Git common directory spanning the DevPod id, devrouter routes, the \`${WORKSPACE_PLACEHOLDER}\` proxy upstream, and devcontainer aliases.`,
3039
- "- The owner record survives linked-worktree removal and binds the exact path to its DevPod ID. First use reuses an exact-path DevPod or derives a sanitized branch/path identity. Later flags or `DEVROUTER_WORKSPACE` may repeat but cannot rename it. Ambiguous identities fail closed. The primary checkout stays non-namespaced.",
3183
+ `- A "workspace token" lets several worktrees of one repo run in parallel without host/route collisions. Each managed linked worktree has a local Git token plus a durable owner record in the repository's Git common directory spanning the workspace-runtime id, devrouter routes, the \`${WORKSPACE_PLACEHOLDER}\` proxy upstream, and devcontainer aliases.`,
3184
+ "- The owner record survives linked-worktree removal and binds the exact path to its workspace-runtime ID. First use reconciles persisted metadata, the exact-path owner record, and both DevPod and Devsy registries. It preserves an established agreement, uses the readable sanitized identity when free, or claims a deterministic hash-suffixed fallback on collision before provider or route mutation. Later flags or `DEVROUTER_WORKSPACE` may repeat but cannot rename it. Unreadable or conflicting evidence fails closed. The primary checkout stays non-namespaced.",
3040
3185
  `- When a workspace is active: hosts auto-namespace (\`web.localhost\` \u2192 \`web.<ws>.localhost\`), \`${WORKSPACE_PLACEHOLDER}\` in \`upstream\` is substituted with the token, and the docker \`router\` key is suffixed per workspace. Managed ensure rejects every HTTP/TCP upstream outside that exact alias namespace before mutation. The runtime config is computed in memory only \u2014 the committed \`.devrouter.yml\` is never rewritten.`,
3041
3186
  "- 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.",
3042
- "- 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. `workspace up` creates linked worktrees; destructive worktree removal and GC remain ledger-scoped.",
3187
+ "- 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.",
3043
3188
  "- A managed adapter plus `postCreateCommand` requires `waitFor` exactly `postCreateCommand` or `postStartCommand`; managed selective config preserves lifecycle fields and changes only `runServices`.",
3044
3189
  "- Managed process reuse fingerprints command argv, workspace identity, and the exact adapter snapshot. If a non-secret runtime value also affects reuse, set `DEVROUTER_PROCESS_FINGERPRINT_ENV` to its comma-separated environment names; secret-like names are rejected and raw values are never persisted.",
3045
3190
  "- Owner status is `present`, `missing`, `locked`, or `conflict`. Dirty or locked full down fails before side effects. `workspace gc` is a dry run; only `--yes` deletes exact eligible missing resources and records, never Git worktrees, branches, or prune state.",
@@ -3766,7 +3911,7 @@ function readPromptDirectory(promptDirPath) {
3766
3911
  continue;
3767
3912
  }
3768
3913
  const version = normalizeVersion(basename);
3769
- const promptPath = import_node_path7.default.join(promptDirPath, file.name);
3914
+ const promptPath = import_node_path8.default.join(promptDirPath, file.name);
3770
3915
  const prompt = import_node_fs8.default.readFileSync(promptPath, "utf-8").trim();
3771
3916
  releases.push({ version, prompt, promptPath });
3772
3917
  }
@@ -3788,17 +3933,17 @@ function listAvailableUpgradeTargets(currentVersion, releases) {
3788
3933
  }
3789
3934
  function resolvePromptDirectory(explicitPath) {
3790
3935
  if (explicitPath) {
3791
- return import_node_path7.default.resolve(explicitPath);
3936
+ return import_node_path8.default.resolve(explicitPath);
3792
3937
  }
3793
- const entryFile = process.argv[1] ? import_node_path7.default.resolve(process.argv[1]) : __filename;
3794
- const entryDir = import_node_path7.default.dirname(entryFile);
3795
- const resolvedEntryDir = import_node_fs8.default.existsSync(entryFile) ? import_node_path7.default.dirname(import_node_fs8.default.realpathSync(entryFile)) : entryDir;
3938
+ const entryFile = process.argv[1] ? import_node_path8.default.resolve(process.argv[1]) : __filename;
3939
+ const entryDir = import_node_path8.default.dirname(entryFile);
3940
+ const resolvedEntryDir = import_node_fs8.default.existsSync(entryFile) ? import_node_path8.default.dirname(import_node_fs8.default.realpathSync(entryFile)) : entryDir;
3796
3941
  const candidates = [
3797
- import_node_path7.default.resolve(resolvedEntryDir, "..", UPGRADE_PROMPTS_DIR),
3798
- import_node_path7.default.resolve(entryDir, "..", UPGRADE_PROMPTS_DIR),
3799
- import_node_path7.default.resolve(__dirname, "..", UPGRADE_PROMPTS_DIR),
3800
- import_node_path7.default.resolve(__dirname, "..", "..", UPGRADE_PROMPTS_DIR),
3801
- import_node_path7.default.resolve(process.cwd(), UPGRADE_PROMPTS_DIR)
3942
+ import_node_path8.default.resolve(resolvedEntryDir, "..", UPGRADE_PROMPTS_DIR),
3943
+ import_node_path8.default.resolve(entryDir, "..", UPGRADE_PROMPTS_DIR),
3944
+ import_node_path8.default.resolve(__dirname, "..", UPGRADE_PROMPTS_DIR),
3945
+ import_node_path8.default.resolve(__dirname, "..", "..", UPGRADE_PROMPTS_DIR),
3946
+ import_node_path8.default.resolve(process.cwd(), UPGRADE_PROMPTS_DIR)
3802
3947
  ];
3803
3948
  const existing = candidates.find((candidate) => import_node_fs8.default.existsSync(candidate));
3804
3949
  return existing ?? candidates[0];
@@ -3816,12 +3961,12 @@ function loadUpgradeCatalog(options = {}) {
3816
3961
  releases
3817
3962
  };
3818
3963
  }
3819
- var import_node_fs8, import_node_path7, SEMVER_RE, UPGRADE_PROMPTS_DIR;
3964
+ var import_node_fs8, import_node_path8, SEMVER_RE, UPGRADE_PROMPTS_DIR;
3820
3965
  var init_upgrade = __esm({
3821
3966
  "src/core/upgrade.ts"() {
3822
3967
  "use strict";
3823
3968
  import_node_fs8 = __toESM(require("fs"));
3824
- import_node_path7 = __toESM(require("path"));
3969
+ import_node_path8 = __toESM(require("path"));
3825
3970
  init_repo_config();
3826
3971
  SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)$/;
3827
3972
  UPGRADE_PROMPTS_DIR = "upgrade-prompts";
@@ -4033,7 +4178,7 @@ function parseDevcontainerConfig(contents, sourcePath) {
4033
4178
  return parsed;
4034
4179
  }
4035
4180
  function readDevcontainerConfig(repoPath) {
4036
- const sourcePath = import_node_path8.default.join(repoPath, ".devcontainer", "devcontainer.json");
4181
+ const sourcePath = import_node_path9.default.join(repoPath, ".devcontainer", "devcontainer.json");
4037
4182
  if (!import_node_fs9.default.existsSync(sourcePath) || !import_node_fs9.default.lstatSync(sourcePath).isFile()) {
4038
4183
  throw new Error(`Managed Dev Container source config does not exist: ${sourcePath}`);
4039
4184
  }
@@ -4053,12 +4198,12 @@ function assertManagedDevcontainerLifecycle(repoPath) {
4053
4198
  `Managed Dev Container source '${sourcePath}' defines postCreateCommand, but waitFor ${actual}. Set waitFor to 'postCreateCommand' or 'postStartCommand' before retrying devrouter ensure.`
4054
4199
  );
4055
4200
  }
4056
- var import_node_fs9, import_node_path8, import_jsonc_parser;
4201
+ var import_node_fs9, import_node_path9, import_jsonc_parser;
4057
4202
  var init_devcontainer_config = __esm({
4058
4203
  "src/core/devcontainer-config.ts"() {
4059
4204
  "use strict";
4060
4205
  import_node_fs9 = __toESM(require("fs"));
4061
- import_node_path8 = __toESM(require("path"));
4206
+ import_node_path9 = __toESM(require("path"));
4062
4207
  import_jsonc_parser = require("jsonc-parser");
4063
4208
  }
4064
4209
  });
@@ -4068,7 +4213,7 @@ function renderDeliveryScript(targetPath) {
4068
4213
  if (!/^\/tmp\/devrouter\/bin\/[a-zA-Z0-9._-]+$/.test(targetPath)) {
4069
4214
  throw new Error(`Unsafe managed runtime delivery path: ${targetPath}`);
4070
4215
  }
4071
- const temporaryPrefix = import_node_path9.default.posix.basename(targetPath);
4216
+ const temporaryPrefix = import_node_path10.default.posix.basename(targetPath);
4072
4217
  return `set -eu
4073
4218
  umask 077
4074
4219
  runtime_root=/tmp/devrouter
@@ -4110,12 +4255,12 @@ function readRegularFile(filePath) {
4110
4255
  return readRegularFileBytes(filePath)?.toString("utf-8");
4111
4256
  }
4112
4257
  function adapterFingerprint(adapter) {
4113
- return (0, import_node_crypto3.createHash)("sha256").update(adapter).digest("hex");
4258
+ return (0, import_node_crypto4.createHash)("sha256").update(adapter).digest("hex");
4114
4259
  }
4115
4260
  function resolveProcessHelperPath() {
4116
4261
  const candidates = [
4117
- import_node_path9.default.resolve(__dirname, "..", "bin", "devrouter-process"),
4118
- import_node_path9.default.resolve(__dirname, "..", "..", "bin", "devrouter-process")
4262
+ import_node_path10.default.resolve(__dirname, "..", "bin", "devrouter-process"),
4263
+ import_node_path10.default.resolve(__dirname, "..", "..", "bin", "devrouter-process")
4119
4264
  ];
4120
4265
  const helperPath = candidates.find((candidate) => import_node_fs10.default.existsSync(candidate));
4121
4266
  if (!helperPath) {
@@ -4124,11 +4269,11 @@ function resolveProcessHelperPath() {
4124
4269
  return helperPath;
4125
4270
  }
4126
4271
  function resolveManagedPostStartPlan(repoPath) {
4127
- const adapterPath = import_node_path9.default.join(repoPath, MANAGED_ADAPTER_PATH);
4272
+ const adapterPath = import_node_path10.default.join(repoPath, MANAGED_ADAPTER_PATH);
4128
4273
  const adapterBytes = readRegularFileBytes(adapterPath);
4129
4274
  const adapterText = adapterBytes?.toString("utf-8");
4130
- const dockerfilePath = import_node_path9.default.join(repoPath, ".devcontainer", "Dockerfile");
4131
- const devcontainerPath = import_node_path9.default.join(repoPath, ".devcontainer", "devcontainer.json");
4275
+ const dockerfilePath = import_node_path10.default.join(repoPath, ".devcontainer", "Dockerfile");
4276
+ const devcontainerPath = import_node_path10.default.join(repoPath, ".devcontainer", "devcontainer.json");
4132
4277
  const dockerfile = readRegularFile(dockerfilePath) ?? "";
4133
4278
  const devcontainer = readRegularFile(devcontainerPath) ?? "";
4134
4279
  const devrouterPattern = [adapterText, dockerfile, devcontainer].filter((value) => value !== void 0).some(
@@ -4257,14 +4402,14 @@ function runManagedProcessAction(options) {
4257
4402
  }
4258
4403
  return "drifted";
4259
4404
  }
4260
- var import_node_child_process5, import_node_crypto3, import_node_fs10, import_node_path9, MANAGED_MARKER, MANAGED_ADAPTER_PATH, RUNTIME_HELPER_PATH, ADAPTER_WRAPPER;
4405
+ var import_node_child_process5, import_node_crypto4, import_node_fs10, import_node_path10, MANAGED_MARKER, MANAGED_ADAPTER_PATH, RUNTIME_HELPER_PATH, ADAPTER_WRAPPER;
4261
4406
  var init_managed_post_start = __esm({
4262
4407
  "src/core/managed-post-start.ts"() {
4263
4408
  "use strict";
4264
4409
  import_node_child_process5 = require("child_process");
4265
- import_node_crypto3 = require("crypto");
4410
+ import_node_crypto4 = require("crypto");
4266
4411
  import_node_fs10 = __toESM(require("fs"));
4267
- import_node_path9 = __toESM(require("path"));
4412
+ import_node_path10 = __toESM(require("path"));
4268
4413
  init_devcontainer_config();
4269
4414
  MANAGED_MARKER = "devrouter:managed devcontainer";
4270
4415
  MANAGED_ADAPTER_PATH = ".devcontainer/post-start.sh";
@@ -4397,11 +4542,11 @@ function routedProxyApps(config) {
4397
4542
  );
4398
4543
  }
4399
4544
  function buildDevcontainerChecks(repoPath, config, workspace) {
4400
- const devcontainerDir = import_node_path10.default.join(repoPath, ".devcontainer");
4545
+ const devcontainerDir = import_node_path11.default.join(repoPath, ".devcontainer");
4401
4546
  if (!import_node_fs11.default.existsSync(devcontainerDir)) {
4402
4547
  return [];
4403
4548
  }
4404
- const compose = inspectCompose(import_node_path10.default.join(devcontainerDir, "docker-compose.yml"), workspace);
4549
+ const compose = inspectCompose(import_node_path11.default.join(devcontainerDir, "docker-compose.yml"), workspace);
4405
4550
  const checks = [];
4406
4551
  if (compose.parseError) {
4407
4552
  checks.push({
@@ -4428,7 +4573,7 @@ function buildDevcontainerChecks(repoPath, config, workspace) {
4428
4573
  details: compose.publishedPorts.length > 0 ? compose.publishedPorts.join(", ") : void 0,
4429
4574
  suggestion: compose.parseError || compose.publishedPorts.length > 0 ? "Remove published ports and route services through devnet aliases with devrouter proxy apps." : void 0
4430
4575
  });
4431
- const dockerfilePath = import_node_path10.default.join(devcontainerDir, "Dockerfile");
4576
+ const dockerfilePath = import_node_path11.default.join(devcontainerDir, "Dockerfile");
4432
4577
  const dockerfileExists = import_node_fs11.default.existsSync(dockerfilePath);
4433
4578
  const dockerfile = dockerfileExists ? import_node_fs11.default.readFileSync(dockerfilePath, "utf-8") : "";
4434
4579
  const devrouterArtifacts = ["@devrouter/cli", "devrouter-process"].filter(
@@ -4485,12 +4630,12 @@ function buildDevcontainerChecks(repoPath, config, workspace) {
4485
4630
  });
4486
4631
  return checks;
4487
4632
  }
4488
- var import_node_fs11, import_node_path10, import_yaml3;
4633
+ var import_node_fs11, import_node_path11, import_yaml3;
4489
4634
  var init_devcontainer_diagnostics = __esm({
4490
4635
  "src/core/devcontainer-diagnostics.ts"() {
4491
4636
  "use strict";
4492
4637
  import_node_fs11 = __toESM(require("fs"));
4493
- import_node_path10 = __toESM(require("path"));
4638
+ import_node_path11 = __toESM(require("path"));
4494
4639
  import_yaml3 = __toESM(require("yaml"));
4495
4640
  init_managed_post_start();
4496
4641
  }
@@ -4498,18 +4643,18 @@ var init_devcontainer_diagnostics = __esm({
4498
4643
 
4499
4644
  // src/core/paths.ts
4500
4645
  function assertPathWithinRepo(filePath, repoRoot, label) {
4501
- const resolvedRoot = import_node_path11.default.resolve(repoRoot);
4502
- const resolved = import_node_path11.default.resolve(repoRoot, filePath);
4503
- if (resolved !== resolvedRoot && !resolved.startsWith(resolvedRoot + import_node_path11.default.sep)) {
4646
+ const resolvedRoot = import_node_path12.default.resolve(repoRoot);
4647
+ const resolved = import_node_path12.default.resolve(repoRoot, filePath);
4648
+ if (resolved !== resolvedRoot && !resolved.startsWith(resolvedRoot + import_node_path12.default.sep)) {
4504
4649
  throw new Error(`${label} path '${filePath}' escapes the repository root.`);
4505
4650
  }
4506
4651
  return resolved;
4507
4652
  }
4508
- var import_node_path11;
4653
+ var import_node_path12;
4509
4654
  var init_paths = __esm({
4510
4655
  "src/core/paths.ts"() {
4511
4656
  "use strict";
4512
- import_node_path11 = __toESM(require("path"));
4657
+ import_node_path12 = __toESM(require("path"));
4513
4658
  }
4514
4659
  });
4515
4660
 
@@ -4764,10 +4909,10 @@ var init_routes = __esm({
4764
4909
 
4765
4910
  // src/core/managed-runtime-state.ts
4766
4911
  function stateKey(repoPath, workspace) {
4767
- return (0, import_node_crypto4.createHash)("sha256").update(`${repoPath}\0${workspace ?? ""}`, "utf-8").digest("hex");
4912
+ return (0, import_node_crypto5.createHash)("sha256").update(`${repoPath}\0${workspace ?? ""}`, "utf-8").digest("hex");
4768
4913
  }
4769
4914
  function managedRuntimeStatePath(repoPath, workspace) {
4770
- return import_node_path12.default.join(DEVROUTER_HOME, "managed-runtime", `${stateKey(repoPath, workspace)}.json`);
4915
+ return import_node_path13.default.join(DEVROUTER_HOME, "managed-runtime", `${stateKey(repoPath, workspace)}.json`);
4771
4916
  }
4772
4917
  function isStringArray(value) {
4773
4918
  return Array.isArray(value) && value.every((item) => typeof item === "string") && new Set(value).size === value.length;
@@ -4855,13 +5000,13 @@ function markManagedRuntimeDegraded(state, transitionPhase) {
4855
5000
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
4856
5001
  });
4857
5002
  }
4858
- var import_node_crypto4, import_node_fs13, import_node_path12;
5003
+ var import_node_crypto5, import_node_fs13, import_node_path13;
4859
5004
  var init_managed_runtime_state = __esm({
4860
5005
  "src/core/managed-runtime-state.ts"() {
4861
5006
  "use strict";
4862
- import_node_crypto4 = require("crypto");
5007
+ import_node_crypto5 = require("crypto");
4863
5008
  import_node_fs13 = __toESM(require("fs"));
4864
- import_node_path12 = __toESM(require("path"));
5009
+ import_node_path13 = __toESM(require("path"));
4865
5010
  init_atomic_file();
4866
5011
  init_router();
4867
5012
  }
@@ -4912,7 +5057,7 @@ function resolveComposeFiles(source, repoPath, linked) {
4912
5057
  "managedRuntime requires .devcontainer/devcontainer.json to define dockerComposeFile."
4913
5058
  );
4914
5059
  }
4915
- const directory = import_node_path13.default.join(repoPath, ".devcontainer");
5060
+ const directory = import_node_path14.default.join(repoPath, ".devcontainer");
4916
5061
  const files = references.map((reference) => {
4917
5062
  const resolved2 = assertPathWithinRepo(
4918
5063
  resolveComposeReference(reference, linked),
@@ -4992,7 +5137,7 @@ function isWildcard(values) {
4992
5137
  return values?.length === 1 && values[0] === "*";
4993
5138
  }
4994
5139
  function sha256(contents) {
4995
- return (0, import_node_crypto5.createHash)("sha256").update(contents, "utf-8").digest("hex");
5140
+ return (0, import_node_crypto6.createHash)("sha256").update(contents, "utf-8").digest("hex");
4996
5141
  }
4997
5142
  function safeComposeProject(value) {
4998
5143
  if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(value)) {
@@ -5057,7 +5202,7 @@ function inspectManagedDevcontainerConfig(options) {
5057
5202
  const contents = `${MANAGED_DEVCONTAINER_MARKER}
5058
5203
  ${JSON.stringify(effective, null, 2)}
5059
5204
  `;
5060
- const generatedPath = import_node_path13.default.join(options.repoPath, MANAGED_DEVCONTAINER_PATH);
5205
+ const generatedPath = import_node_path14.default.join(options.repoPath, MANAGED_DEVCONTAINER_PATH);
5061
5206
  assertIgnoredGeneratedPath(options.repoPath, generatedPath);
5062
5207
  assertGeneratedPathOwnership(generatedPath);
5063
5208
  return {
@@ -5177,14 +5322,14 @@ function stopExactManagedService(containerId, service) {
5177
5322
  throw new Error(`Could not stop exact managed service '${service}' (${containerId}).`);
5178
5323
  }
5179
5324
  }
5180
- var import_node_child_process6, import_node_crypto5, import_node_fs14, import_node_path13, import_yaml4, MANAGED_DEVCONTAINER_PATH, MANAGED_DEVCONTAINER_MARKER;
5325
+ var import_node_child_process6, import_node_crypto6, import_node_fs14, import_node_path14, import_yaml4, MANAGED_DEVCONTAINER_PATH, MANAGED_DEVCONTAINER_MARKER;
5181
5326
  var init_devcontainer_profile = __esm({
5182
5327
  "src/core/devcontainer-profile.ts"() {
5183
5328
  "use strict";
5184
5329
  import_node_child_process6 = require("child_process");
5185
- import_node_crypto5 = require("crypto");
5330
+ import_node_crypto6 = require("crypto");
5186
5331
  import_node_fs14 = __toESM(require("fs"));
5187
- import_node_path13 = __toESM(require("path"));
5332
+ import_node_path14 = __toESM(require("path"));
5188
5333
  import_yaml4 = __toESM(require("yaml"));
5189
5334
  init_atomic_file();
5190
5335
  init_devcontainer_config();
@@ -5198,9 +5343,17 @@ var init_devcontainer_profile = __esm({
5198
5343
  function inspectWorkspaceContainers(options) {
5199
5344
  let ids = options?.ids;
5200
5345
  if (!ids) {
5201
- const listed = (0, import_node_child_process7.spawnSync)("docker", ["ps", "-a", "--format", "{{.ID}}"], {
5202
- encoding: "utf-8"
5203
- });
5346
+ const listed = (0, import_node_child_process7.spawnSync)(
5347
+ "docker",
5348
+ [
5349
+ "ps",
5350
+ "-a",
5351
+ ...options?.composeProject ? ["--filter", `label=com.docker.compose.project=${options.composeProject}`] : [],
5352
+ "--format",
5353
+ "{{.ID}}"
5354
+ ],
5355
+ { encoding: "utf-8" }
5356
+ );
5204
5357
  if (listed.status !== 0) {
5205
5358
  throw new Error(
5206
5359
  `docker ps failed: ${(listed.stderr || listed.stdout || listed.error?.message || "unknown error").trim()}`
@@ -5225,7 +5378,7 @@ function inspectWorkspaceContainers(options) {
5225
5378
  function workspaceAppContainers(containers, repoPath) {
5226
5379
  return containers.filter((container) => {
5227
5380
  const workingDir = container.labels["com.docker.compose.project.working_dir"];
5228
- return Boolean(workingDir && sameWorkspacePath(workingDir, import_node_path14.default.join(repoPath, ".devcontainer"))) && container.mounts.some(
5381
+ return Boolean(workingDir && sameWorkspacePath(workingDir, import_node_path15.default.join(repoPath, ".devcontainer"))) && container.mounts.some(
5229
5382
  (mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath)
5230
5383
  );
5231
5384
  });
@@ -5233,7 +5386,7 @@ function workspaceAppContainers(containers, repoPath) {
5233
5386
  function hasExactComposeIdentity(container, options) {
5234
5387
  if (options.composeProject !== void 0 && container.labels["com.docker.compose.project"] !== options.composeProject || container.labels["com.docker.compose.service"] !== options.service || !sameWorkspacePath(
5235
5388
  container.labels["com.docker.compose.project.working_dir"] ?? "",
5236
- import_node_path14.default.join(options.repoPath, ".devcontainer")
5389
+ import_node_path15.default.join(options.repoPath, ".devcontainer")
5237
5390
  )) {
5238
5391
  return false;
5239
5392
  }
@@ -5270,12 +5423,12 @@ function resolveRunningWorkspaceContainer(repoPath) {
5270
5423
  }
5271
5424
  return { id: container.id, workspacePath: repoMount.Destination };
5272
5425
  }
5273
- var import_node_child_process7, import_node_path14, SAFE_INSPECT_TEMPLATE, SIZE_INSPECT_TEMPLATE;
5426
+ var import_node_child_process7, import_node_path15, SAFE_INSPECT_TEMPLATE, SIZE_INSPECT_TEMPLATE;
5274
5427
  var init_devpod_environment = __esm({
5275
5428
  "src/core/devpod-environment.ts"() {
5276
5429
  "use strict";
5277
5430
  import_node_child_process7 = require("child_process");
5278
- import_node_path14 = __toESM(require("path"));
5431
+ import_node_path15 = __toESM(require("path"));
5279
5432
  init_workspace();
5280
5433
  SAFE_INSPECT_TEMPLATE = '{"id":{{json .Id}},"state":{"Running":{{json .State.Running}},"Health":{{with (index .State "Health")}}{"Status":{{json .Status}}}{{else}}null{{end}}},"labels":{"com.docker.compose.project":{{json (index .Config.Labels "com.docker.compose.project")}},"com.docker.compose.service":{{json (index .Config.Labels "com.docker.compose.service")}},"com.docker.compose.project.working_dir":{{json (index .Config.Labels "com.docker.compose.project.working_dir")}},"com.docker.compose.project.config_files":{{json (index .Config.Labels "com.docker.compose.project.config_files")}}},"mounts":{{json .Mounts}},"networks":{{json .NetworkSettings.Networks}}}';
5281
5434
  SIZE_INSPECT_TEMPLATE = SAFE_INSPECT_TEMPLATE.replace(
@@ -5920,7 +6073,7 @@ function tlsSetupCommand(repoPath) {
5920
6073
  }
5921
6074
  function getMkcertRootCAPath(options = {}) {
5922
6075
  ensureMkcert();
5923
- const rootCAPath = import_node_path15.default.join(runOrThrow("mkcert", ["-CAROOT"]), "rootCA.pem");
6076
+ const rootCAPath = import_node_path16.default.join(runOrThrow("mkcert", ["-CAROOT"]), "rootCA.pem");
5924
6077
  if (!import_node_fs16.default.existsSync(rootCAPath)) {
5925
6078
  throw new Error(
5926
6079
  `mkcert root CA was not found at '${rootCAPath}'. Run: ${tlsSetupCommand(options.repoPath)}`
@@ -5954,7 +6107,7 @@ function parseDnsHostsFromSubjectAltName(subjectAltName) {
5954
6107
  return normalizeUniqueHosts(names);
5955
6108
  }
5956
6109
  function parseCertificateDnsHosts(pem) {
5957
- const certificate = new import_node_crypto6.X509Certificate(pem);
6110
+ const certificate = new import_node_crypto7.X509Certificate(pem);
5958
6111
  const subjectAltName = certificate.subjectAltName ?? "";
5959
6112
  if (subjectAltName.length === 0) {
5960
6113
  return [];
@@ -6154,20 +6307,20 @@ Run: ${tlsSetupCommand(options.repoPath)}`
6154
6307
  );
6155
6308
  }
6156
6309
  }
6157
- var import_node_child_process8, import_node_crypto6, import_node_fs16, import_node_path15, DEFAULT_TLS_CERT_HOSTS, TLS_CERTIFICATE_LOCK_FILE, TLS_CERTIFICATE_LOCK_WAIT_MS, TLS_CERTIFICATE_WRITE_ATTEMPTS;
6310
+ var import_node_child_process8, import_node_crypto7, import_node_fs16, import_node_path16, DEFAULT_TLS_CERT_HOSTS, TLS_CERTIFICATE_LOCK_FILE, TLS_CERTIFICATE_LOCK_WAIT_MS, TLS_CERTIFICATE_WRITE_ATTEMPTS;
6158
6311
  var init_tls = __esm({
6159
6312
  "src/core/tls.ts"() {
6160
6313
  "use strict";
6161
6314
  import_node_child_process8 = require("child_process");
6162
- import_node_crypto6 = require("crypto");
6315
+ import_node_crypto7 = require("crypto");
6163
6316
  import_node_fs16 = __toESM(require("fs"));
6164
- import_node_path15 = __toESM(require("path"));
6317
+ import_node_path16 = __toESM(require("path"));
6165
6318
  init_docker();
6166
6319
  init_file_lock();
6167
6320
  init_host_routes();
6168
6321
  init_router();
6169
6322
  DEFAULT_TLS_CERT_HOSTS = ["localhost", "*.localhost"];
6170
- TLS_CERTIFICATE_LOCK_FILE = import_node_path15.default.join(DEVROUTER_HOME, "tls-certificate.lock");
6323
+ TLS_CERTIFICATE_LOCK_FILE = import_node_path16.default.join(DEVROUTER_HOME, "tls-certificate.lock");
6171
6324
  TLS_CERTIFICATE_LOCK_WAIT_MS = 6e4;
6172
6325
  TLS_CERTIFICATE_WRITE_ATTEMPTS = 2;
6173
6326
  }
@@ -6526,20 +6679,20 @@ function resetWorkspaceRuntimeCaches() {
6526
6679
  cachedRuntime = void 0;
6527
6680
  cachedSnapshots = void 0;
6528
6681
  }
6529
- var import_node_child_process11, import_node_fs17, import_node_path16, SUPPORTED_RUNTIMES2, RUNTIME_CONFIG_FILE, INACTIVITY_TIMEOUT_PATTERN, cachedRuntime, cachedSnapshots, UnsupportedWorkspaceRuntimeError, WorkspaceRuntimeOwnershipError;
6682
+ var import_node_child_process11, import_node_fs17, import_node_path17, SUPPORTED_RUNTIMES2, RUNTIME_CONFIG_FILE, INACTIVITY_TIMEOUT_PATTERN, cachedRuntime, cachedSnapshots, UnsupportedWorkspaceRuntimeError, WorkspaceRuntimeOwnershipError;
6530
6683
  var init_workspace_runtime = __esm({
6531
6684
  "src/core/workspace-runtime.ts"() {
6532
6685
  "use strict";
6533
6686
  import_node_child_process11 = require("child_process");
6534
6687
  import_node_fs17 = __toESM(require("fs"));
6535
- import_node_path16 = __toESM(require("path"));
6688
+ import_node_path17 = __toESM(require("path"));
6536
6689
  init_atomic_file();
6537
6690
  init_devpod_registry();
6538
6691
  init_devsy_workspaces();
6539
6692
  init_router();
6540
6693
  init_workspace();
6541
6694
  SUPPORTED_RUNTIMES2 = ["devpod", "devsy"];
6542
- RUNTIME_CONFIG_FILE = import_node_path16.default.join(DEVROUTER_HOME, "workspace-runtime.json");
6695
+ RUNTIME_CONFIG_FILE = import_node_path17.default.join(DEVROUTER_HOME, "workspace-runtime.json");
6543
6696
  INACTIVITY_TIMEOUT_PATTERN = /^(?:\d+(?:ms|s|m|h))+$/;
6544
6697
  UnsupportedWorkspaceRuntimeError = class extends Error {
6545
6698
  };
@@ -6617,7 +6770,7 @@ function parseMinimumNodeMajor(value) {
6617
6770
  return Number(match[1]);
6618
6771
  }
6619
6772
  function readPackageJson(repoPath) {
6620
- const packagePath = import_node_path17.default.join(repoPath, "package.json");
6773
+ const packagePath = import_node_path18.default.join(repoPath, "package.json");
6621
6774
  if (!import_node_fs18.default.existsSync(packagePath)) {
6622
6775
  return void 0;
6623
6776
  }
@@ -6762,13 +6915,13 @@ function buildGlobalToolChecks(repoPath) {
6762
6915
  checks.push(nodeToolchainCheck(repoPath));
6763
6916
  return checks;
6764
6917
  }
6765
- var import_node_child_process12, import_node_fs18, import_node_path17;
6918
+ var import_node_child_process12, import_node_fs18, import_node_path18;
6766
6919
  var init_tool_diagnostics = __esm({
6767
6920
  "src/core/tool-diagnostics.ts"() {
6768
6921
  "use strict";
6769
6922
  import_node_child_process12 = require("child_process");
6770
6923
  import_node_fs18 = __toESM(require("fs"));
6771
- import_node_path17 = __toESM(require("path"));
6924
+ import_node_path18 = __toESM(require("path"));
6772
6925
  init_workspace_runtime();
6773
6926
  }
6774
6927
  });
@@ -6898,13 +7051,65 @@ function withMutationLock(activity, target, operation) {
6898
7051
  import_node_fs19.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
6899
7052
  return withFileLockSync(
6900
7053
  DEVSY_MUTATION_LOCK_FILE,
6901
- { activity, target: `'${target}'`, waitMs: DEVSY_MUTATION_WAIT_MS },
7054
+ {
7055
+ activity,
7056
+ target: `'${target}'`,
7057
+ waitMs: DEVSY_MUTATION_WAIT_MS,
7058
+ fair: true,
7059
+ onWait: createStderrWaitReporter(activity, `'${target}'`)
7060
+ },
7061
+ operation
7062
+ );
7063
+ }
7064
+ function withMutationLockAsync(activity, target, operation) {
7065
+ import_node_fs19.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
7066
+ return withFileLock(
7067
+ DEVSY_MUTATION_LOCK_FILE,
7068
+ {
7069
+ activity,
7070
+ target: `'${target}'`,
7071
+ waitMs: DEVSY_MUTATION_WAIT_MS,
7072
+ fair: true,
7073
+ onWait: createStderrWaitReporter(activity, `'${target}'`)
7074
+ },
6902
7075
  operation
6903
7076
  );
6904
7077
  }
6905
7078
  function commandFailure2(result) {
6906
7079
  return [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
6907
7080
  }
7081
+ function runDevsyUp(args, env, quiet) {
7082
+ return new Promise((resolve) => {
7083
+ const child = (0, import_node_child_process14.spawn)("devsy", args, {
7084
+ stdio: ["inherit", quiet ? 2 : "inherit", "pipe"],
7085
+ env
7086
+ });
7087
+ const stderr = child.stderr;
7088
+ if (!stderr) throw new Error("Devsy startup stderr pipe was not created.");
7089
+ let stderrTail = Buffer.alloc(0);
7090
+ let spawnError;
7091
+ stderr.on("data", (chunk) => {
7092
+ const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
7093
+ const writable = process.stderr.write(value);
7094
+ if (!writable) {
7095
+ stderr.pause();
7096
+ process.stderr.once("drain", () => stderr.resume());
7097
+ }
7098
+ if (value.length >= DEVSY_STDERR_TAIL_BYTES) {
7099
+ stderrTail = Buffer.from(value.subarray(value.length - DEVSY_STDERR_TAIL_BYTES));
7100
+ } else {
7101
+ const combined = Buffer.concat([stderrTail, value]);
7102
+ stderrTail = combined.length > DEVSY_STDERR_TAIL_BYTES ? combined.subarray(combined.length - DEVSY_STDERR_TAIL_BYTES) : combined;
7103
+ }
7104
+ });
7105
+ child.once("error", (error) => {
7106
+ spawnError = error;
7107
+ });
7108
+ child.once("close", (status) => {
7109
+ resolve({ status, error: spawnError, stderrTail: stderrTail.toString("utf-8") });
7110
+ });
7111
+ });
7112
+ }
6908
7113
  function runDevsyAction(action2, devsyId, force = false) {
6909
7114
  const args = action2 === "delete" ? ["delete", devsyId, ...force ? ["--force"] : [], "--ignore-not-found"] : ["stop", devsyId];
6910
7115
  const result = (0, import_node_child_process14.spawnSync)("devsy", ["workspace", ...args], { encoding: "utf-8" });
@@ -6969,7 +7174,7 @@ function assertDevsyTarget(devsyId, repoPath) {
6969
7174
  }
6970
7175
  function startDevsyWorkspace(options) {
6971
7176
  const activity = options.recreate ? "Devsy recreate" : "Devsy start";
6972
- return withMutationLock(activity, options.repoPath, () => {
7177
+ return withMutationLockAsync(activity, options.repoPath, async () => {
6973
7178
  let devsyId = assertDevsyTarget(options.devsyId, options.repoPath);
6974
7179
  if (devsyId && options.recreate) {
6975
7180
  const attached = listDevsyWorkspaces();
@@ -7009,12 +7214,13 @@ function startDevsyWorkspace(options) {
7009
7214
  delete env.DEVROUTER_GIT_COMMON_DIR;
7010
7215
  delete env.DEVCONTAINER_COMPOSE_OVERLAY;
7011
7216
  }
7012
- const result = (0, import_node_child_process14.spawnSync)("devsy", args, {
7013
- stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
7014
- env
7015
- });
7217
+ const result = await runDevsyUp(args, env, options.quiet ?? false);
7016
7218
  if (result.status !== 0) {
7017
- const message = `devsy workspace up failed for '${devsyId ?? options.repoPath}'.`;
7219
+ let message = `devsy workspace up failed for '${devsyId ?? options.repoPath}'.`;
7220
+ if (result.error?.message) message += ` ${result.error.message}`;
7221
+ if (AGENT_ACQUISITION_RE.test(result.stderrTail)) {
7222
+ message += " Devsy could not obtain its agent binary; allow Devsy to download it or set DEVSY_AGENT_BINARY to a verified official Devsy agent binary matching this platform and Devsy version.";
7223
+ }
7018
7224
  if (failedStartMayHaveAttached(devsyId, options.repoPath)) {
7019
7225
  throw new DevsyStartPostconditionError(message);
7020
7226
  }
@@ -7040,20 +7246,22 @@ function startDevsyWorkspace(options) {
7040
7246
  }
7041
7247
  });
7042
7248
  }
7043
- var import_node_child_process14, import_node_fs19, import_node_path18, DEVSY_MUTATION_LOCK_FILE, DEVSY_MUTATION_WAIT_MS, DevsyStartPostconditionError;
7249
+ var import_node_child_process14, import_node_fs19, import_node_path19, DEVSY_MUTATION_LOCK_FILE, DEVSY_MUTATION_WAIT_MS, DevsyStartPostconditionError, AGENT_ACQUISITION_RE, DEVSY_STDERR_TAIL_BYTES;
7044
7250
  var init_devsy_mutation = __esm({
7045
7251
  "src/core/devsy-mutation.ts"() {
7046
7252
  "use strict";
7047
7253
  import_node_child_process14 = require("child_process");
7048
7254
  import_node_fs19 = __toESM(require("fs"));
7049
- import_node_path18 = __toESM(require("path"));
7255
+ import_node_path19 = __toESM(require("path"));
7050
7256
  init_devsy_workspaces();
7051
7257
  init_file_lock();
7052
7258
  init_router();
7053
- DEVSY_MUTATION_LOCK_FILE = import_node_path18.default.join(DEVROUTER_HOME, "devsy-mutation.lock");
7054
- DEVSY_MUTATION_WAIT_MS = 6e4;
7259
+ DEVSY_MUTATION_LOCK_FILE = import_node_path19.default.join(DEVROUTER_HOME, "devsy-mutation.lock");
7260
+ DEVSY_MUTATION_WAIT_MS = 18e5;
7055
7261
  DevsyStartPostconditionError = class extends Error {
7056
7262
  };
7263
+ AGENT_ACQUISITION_RE = /inject agent.*agent binary not found/i;
7264
+ DEVSY_STDERR_TAIL_BYTES = 8192;
7057
7265
  }
7058
7266
  });
7059
7267
 
@@ -7062,7 +7270,13 @@ function withMutationLock2(activity, target, operation) {
7062
7270
  import_node_fs20.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
7063
7271
  return withFileLockSync(
7064
7272
  DEVPOD_MUTATION_LOCK_FILE,
7065
- { activity, target: `'${target}'`, waitMs: DEVPOD_MUTATION_WAIT_MS },
7273
+ {
7274
+ activity,
7275
+ target: `'${target}'`,
7276
+ waitMs: DEVPOD_MUTATION_WAIT_MS,
7277
+ fair: true,
7278
+ onWait: createStderrWaitReporter(activity, `'${target}'`)
7279
+ },
7066
7280
  operation
7067
7281
  );
7068
7282
  }
@@ -7137,10 +7351,10 @@ function deleteOwnedDevpodWorkspace(devpodId, worktreePath) {
7137
7351
  resetWorkspaceRuntimeCaches();
7138
7352
  return result;
7139
7353
  }
7140
- function startDevpodWorkspace(options) {
7354
+ async function startDevpodWorkspace(options) {
7141
7355
  if (resolveWorkspaceRuntimeOrDefault(options.repoPath) === "devsy") {
7142
7356
  try {
7143
- const result = startDevsyWorkspace({
7357
+ const result = await startDevsyWorkspace({
7144
7358
  repoPath: options.repoPath,
7145
7359
  devsyId: options.devpodId,
7146
7360
  devcontainerPath: options.devcontainerPath,
@@ -7227,20 +7441,20 @@ function startDevpodWorkspace(options) {
7227
7441
  }
7228
7442
  });
7229
7443
  }
7230
- var import_node_child_process15, import_node_fs20, import_node_path19, DEVPOD_MUTATION_LOCK_FILE, DEVPOD_MUTATION_WAIT_MS, DevpodStartPostconditionError;
7444
+ var import_node_child_process15, import_node_fs20, import_node_path20, DEVPOD_MUTATION_LOCK_FILE, DEVPOD_MUTATION_WAIT_MS, DevpodStartPostconditionError;
7231
7445
  var init_devpod_mutation = __esm({
7232
7446
  "src/core/devpod-mutation.ts"() {
7233
7447
  "use strict";
7234
7448
  import_node_child_process15 = require("child_process");
7235
7449
  import_node_fs20 = __toESM(require("fs"));
7236
- import_node_path19 = __toESM(require("path"));
7450
+ import_node_path20 = __toESM(require("path"));
7237
7451
  init_devpod_workspaces();
7238
7452
  init_devsy_mutation();
7239
7453
  init_file_lock();
7240
7454
  init_router();
7241
7455
  init_workspace_runtime();
7242
- DEVPOD_MUTATION_LOCK_FILE = import_node_path19.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
7243
- DEVPOD_MUTATION_WAIT_MS = 6e4;
7456
+ DEVPOD_MUTATION_LOCK_FILE = import_node_path20.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
7457
+ DEVPOD_MUTATION_WAIT_MS = 18e5;
7244
7458
  DevpodStartPostconditionError = class extends Error {
7245
7459
  };
7246
7460
  }
@@ -7261,7 +7475,7 @@ function resolveGitCommonDir(repoPath) {
7261
7475
  if (result.status !== 0 || !output2) {
7262
7476
  throw commandError("Could not resolve the Git common directory", repoPath, result.stderr);
7263
7477
  }
7264
- return comparableWorkspacePath(import_node_path20.default.isAbsolute(output2) ? output2 : import_node_path20.default.resolve(repoPath, output2));
7478
+ return comparableWorkspacePath(import_node_path21.default.isAbsolute(output2) ? output2 : import_node_path21.default.resolve(repoPath, output2));
7265
7479
  }
7266
7480
  function resolveGitTopLevel(repoPath) {
7267
7481
  const result = (0, import_node_child_process16.spawnSync)("git", ["-C", repoPath, "rev-parse", "--show-toplevel"], {
@@ -7312,7 +7526,7 @@ function listGitWorktrees(repoPath) {
7312
7526
  return worktrees;
7313
7527
  }
7314
7528
  function ownershipDirectory(repoPath) {
7315
- return import_node_path20.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
7529
+ return import_node_path21.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
7316
7530
  }
7317
7531
  function validateWorkspace(value, label) {
7318
7532
  if (typeof value !== "string" || wsFromBranch(value) !== value) {
@@ -7340,7 +7554,7 @@ function validateRecord(value, expectedWorkspace) {
7340
7554
  `workspace ownership file '${expectedWorkspace}' contains identity '${workspace}'`
7341
7555
  );
7342
7556
  }
7343
- if (typeof candidate.worktreePath !== "string" || !import_node_path20.default.isAbsolute(candidate.worktreePath)) {
7557
+ if (typeof candidate.worktreePath !== "string" || !import_node_path21.default.isAbsolute(candidate.worktreePath)) {
7344
7558
  throw new Error("invalid workspace ownership worktreePath");
7345
7559
  }
7346
7560
  if (candidate.branch !== null && typeof candidate.branch !== "string") {
@@ -7384,14 +7598,14 @@ function listWorkspaceOwnershipInDirectory(directory) {
7384
7598
  return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
7385
7599
  const workspace = entry.name.slice(0, -".json".length);
7386
7600
  validateWorkspace(workspace, "filename");
7387
- return readRecordFile(import_node_path20.default.join(directory, entry.name), workspace);
7601
+ return readRecordFile(import_node_path21.default.join(directory, entry.name), workspace);
7388
7602
  });
7389
7603
  }
7390
7604
  function writeWorkspaceOwnershipInDirectory(directory, input2) {
7391
7605
  const workspace = validateWorkspace(input2.workspace, "workspace");
7392
7606
  const devpodId = validateWorkspace(input2.devpodId, "devpodId");
7393
7607
  const worktreePath = comparableWorkspacePath(input2.worktreePath);
7394
- const filePath = import_node_path20.default.join(directory, `${workspace}.json`);
7608
+ const filePath = import_node_path21.default.join(directory, `${workspace}.json`);
7395
7609
  const now = (/* @__PURE__ */ new Date()).toISOString();
7396
7610
  const records = listWorkspaceOwnershipInDirectory(directory);
7397
7611
  const existing = records.find((record2) => record2.workspace === workspace);
@@ -7427,7 +7641,7 @@ function writeWorkspaceOwnershipInDirectory(directory, input2) {
7427
7641
  return record;
7428
7642
  }
7429
7643
  function removeWorkspaceOwnershipInDirectory(directory, workspace) {
7430
- const filePath = import_node_path20.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
7644
+ const filePath = import_node_path21.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
7431
7645
  try {
7432
7646
  import_node_fs21.default.rmSync(filePath);
7433
7647
  return true;
@@ -7440,7 +7654,7 @@ function sameOwnershipRecord(left, right) {
7440
7654
  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;
7441
7655
  }
7442
7656
  function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
7443
- const filePath = import_node_path20.default.join(
7657
+ const filePath = import_node_path21.default.join(
7444
7658
  directory,
7445
7659
  `${validateWorkspace(expected.workspace, "workspace")}.json`
7446
7660
  );
@@ -7455,12 +7669,16 @@ function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
7455
7669
  import_node_fs21.default.rmSync(filePath);
7456
7670
  return "removed";
7457
7671
  }
7458
- function withWorkspaceOwnershipTransaction(repoPath, operation) {
7672
+ function withWorkspaceOwnershipTransaction(repoPath, operation, options = {}) {
7459
7673
  const directory = ownershipDirectory(repoPath);
7460
7674
  import_node_fs21.default.mkdirSync(directory, { recursive: true });
7461
7675
  return withFileLockSync(
7462
- import_node_path20.default.join(directory, ".lock"),
7463
- { activity: "workspace ownership transaction", target: `'${repoPath}'`, waitMs: 5e3 },
7676
+ import_node_path21.default.join(directory, ".lock"),
7677
+ {
7678
+ activity: "workspace ownership transaction",
7679
+ target: `'${repoPath}'`,
7680
+ waitMs: options.waitMs ?? 5e3
7681
+ },
7464
7682
  () => operation({
7465
7683
  list: () => listWorkspaceOwnershipInDirectory(directory),
7466
7684
  write: (input2) => writeWorkspaceOwnershipInDirectory(directory, input2),
@@ -7469,8 +7687,169 @@ function withWorkspaceOwnershipTransaction(repoPath, operation) {
7469
7687
  })
7470
7688
  );
7471
7689
  }
7472
- function writeWorkspaceOwnership(repoPath, input2) {
7473
- return withWorkspaceOwnershipTransaction(repoPath, (transaction) => transaction.write(input2));
7690
+ function providerPathOwner(providerWorkspaces, worktreePath) {
7691
+ const owners = providerWorkspaces.filter(
7692
+ (workspace) => sameWorkspacePath(workspace.source.localFolder, worktreePath)
7693
+ );
7694
+ if (owners.length > 1) {
7695
+ throw new Error(
7696
+ `Worktree '${worktreePath}' is registered by multiple workspace runtimes; no identity was claimed.`
7697
+ );
7698
+ }
7699
+ return owners[0];
7700
+ }
7701
+ function persistedWorkspaceOwners(repoPath, worktreePath) {
7702
+ const owners = /* @__PURE__ */ new Map();
7703
+ for (const worktree of listGitWorktrees(repoPath)) {
7704
+ if (sameWorkspacePath(worktree.path, worktreePath)) continue;
7705
+ let workspace;
7706
+ try {
7707
+ workspace = readPersistedWorkspace(worktree.path);
7708
+ } catch (error) {
7709
+ if (!import_node_fs21.default.existsSync(worktree.path)) continue;
7710
+ throw error;
7711
+ }
7712
+ if (!workspace) continue;
7713
+ const existing = owners.get(workspace);
7714
+ if (existing && !sameWorkspacePath(existing, worktree.path)) {
7715
+ throw new Error(`Persisted workspace identity '${workspace}' belongs to multiple worktrees.`);
7716
+ }
7717
+ owners.set(workspace, worktree.path);
7718
+ }
7719
+ return owners;
7720
+ }
7721
+ function claimConflict(workspace, devpodId, worktreePath, records, providerWorkspaces, persistedOwners, exactRecord) {
7722
+ const recordOwner = records.find(
7723
+ (record) => record !== exactRecord && (record.workspace === workspace || record.devpodId === devpodId)
7724
+ );
7725
+ if (recordOwner) {
7726
+ return `workspace owner record '${recordOwner.workspace}' for '${recordOwner.worktreePath}'`;
7727
+ }
7728
+ const providerOwner = providerWorkspaces.find(
7729
+ (providerWorkspace) => providerWorkspace.id === devpodId && !sameWorkspacePath(providerWorkspace.source.localFolder, worktreePath)
7730
+ );
7731
+ if (providerOwner) {
7732
+ return `workspace runtime identity '${providerOwner.id}' already belongs to '${providerOwner.source.localFolder}'`;
7733
+ }
7734
+ const persistedOwner = persistedOwners.get(workspace);
7735
+ if (persistedOwner) {
7736
+ return `persisted checkout metadata for '${persistedOwner}'`;
7737
+ }
7738
+ return void 0;
7739
+ }
7740
+ function claimWorkspaceIdentity(repoPath, input2) {
7741
+ const worktreePath = comparableWorkspacePath(repoPath);
7742
+ const exactProvider = providerPathOwner(input2.providerWorkspaces, worktreePath);
7743
+ return withWorkspaceOwnershipTransaction(repoPath, (transaction) => {
7744
+ const records = transaction.list();
7745
+ const exactRecords = records.filter(
7746
+ (record) => sameWorkspacePath(record.worktreePath, worktreePath)
7747
+ );
7748
+ if (exactRecords.length > 1) {
7749
+ throw new Error(
7750
+ `Worktree '${worktreePath}' has multiple workspace owner records; no identity was claimed.`
7751
+ );
7752
+ }
7753
+ const exactRecord = exactRecords[0];
7754
+ const persisted = readPersistedWorkspace(worktreePath);
7755
+ const persistedOwners = persistedWorkspaceOwners(repoPath, worktreePath);
7756
+ if (exactRecord) {
7757
+ if (persisted && persisted !== exactRecord.workspace) {
7758
+ throw new Error(
7759
+ `Persisted workspace identity '${persisted}' disagrees with owner record '${exactRecord.workspace}'.`
7760
+ );
7761
+ }
7762
+ if (exactProvider && exactProvider.id !== exactRecord.devpodId) {
7763
+ throw new Error(
7764
+ `Workspace runtime '${exactProvider.id}' disagrees with owner record '${exactRecord.devpodId}'.`
7765
+ );
7766
+ }
7767
+ const conflict2 = claimConflict(
7768
+ exactRecord.workspace,
7769
+ exactRecord.devpodId,
7770
+ worktreePath,
7771
+ records,
7772
+ input2.providerWorkspaces,
7773
+ persistedOwners,
7774
+ exactRecord
7775
+ );
7776
+ if (conflict2) {
7777
+ throw new Error(
7778
+ `Workspace '${exactRecord.workspace}' conflicts with ${conflict2}; no identity was claimed.`
7779
+ );
7780
+ }
7781
+ if (!persisted) persistWorkspace(worktreePath, exactRecord.workspace);
7782
+ return transaction.write({
7783
+ workspace: exactRecord.workspace,
7784
+ worktreePath,
7785
+ branch: input2.branch ?? null,
7786
+ devpodId: exactRecord.devpodId
7787
+ });
7788
+ }
7789
+ if (persisted && exactProvider && persisted !== exactProvider.id) {
7790
+ throw new Error(
7791
+ `Persisted workspace identity '${persisted}' disagrees with workspace runtime '${exactProvider.id}'.`
7792
+ );
7793
+ }
7794
+ let workspace = persisted ?? exactProvider?.id;
7795
+ let devpodId = exactProvider?.id ?? persisted;
7796
+ if (!workspace || !devpodId) {
7797
+ if (input2.unavailableRuntimes.length > 0) {
7798
+ throw new Error(
7799
+ `Cannot claim a new workspace identity because these runtime registries are unavailable: ${input2.unavailableRuntimes.join(", ")}.`
7800
+ );
7801
+ }
7802
+ const candidate = workspaceIdentityCandidates(input2.source).find(
7803
+ (next) => !claimConflict(
7804
+ next,
7805
+ next,
7806
+ worktreePath,
7807
+ records,
7808
+ input2.providerWorkspaces,
7809
+ persistedOwners
7810
+ )
7811
+ );
7812
+ if (!candidate) {
7813
+ throw new Error(
7814
+ `Could not allocate a collision-safe workspace identity for '${worktreePath}'.`
7815
+ );
7816
+ }
7817
+ workspace = candidate;
7818
+ devpodId = candidate;
7819
+ }
7820
+ const conflict = claimConflict(
7821
+ workspace,
7822
+ devpodId,
7823
+ worktreePath,
7824
+ records,
7825
+ input2.providerWorkspaces,
7826
+ persistedOwners
7827
+ );
7828
+ if (conflict) {
7829
+ throw new Error(
7830
+ `Workspace '${workspace}' conflicts with ${conflict}; no identity was claimed.`
7831
+ );
7832
+ }
7833
+ const written = transaction.write({
7834
+ workspace,
7835
+ worktreePath,
7836
+ branch: input2.branch ?? null,
7837
+ devpodId
7838
+ });
7839
+ try {
7840
+ persistWorkspace(worktreePath, workspace);
7841
+ } catch (error) {
7842
+ const cleanup = transaction.removeIfMatches(written);
7843
+ if (cleanup !== "removed") {
7844
+ const detail = error instanceof Error ? error.message : String(error);
7845
+ throw new Error(
7846
+ `Could not persist workspace identity and owner-record rollback was '${cleanup}': ${detail}`
7847
+ );
7848
+ }
7849
+ throw error;
7850
+ }
7851
+ return written;
7852
+ });
7474
7853
  }
7475
7854
  function removeWorkspaceOwnership(repoPath, workspace) {
7476
7855
  return withWorkspaceOwnershipTransaction(
@@ -7514,20 +7893,20 @@ function listMissingWorkspaceOwnership(repoPath) {
7514
7893
  (record) => inspectWorkspaceOwnership(record, worktrees, void 0).ownerStatus === "missing"
7515
7894
  );
7516
7895
  }
7517
- var import_node_child_process16, import_node_fs21, import_node_path20, READ_ONLY_GIT_ENV, OWNERSHIP_VERSION, OWNERSHIP_DIR;
7896
+ var import_node_child_process16, import_node_fs21, import_node_path21, READ_ONLY_GIT_ENV, OWNERSHIP_VERSION, OWNERSHIP_DIR;
7518
7897
  var init_workspace_ownership = __esm({
7519
7898
  "src/core/workspace-ownership.ts"() {
7520
7899
  "use strict";
7521
7900
  import_node_child_process16 = require("child_process");
7522
7901
  import_node_fs21 = __toESM(require("fs"));
7523
- import_node_path20 = __toESM(require("path"));
7902
+ import_node_path21 = __toESM(require("path"));
7524
7903
  init_atomic_file();
7525
7904
  init_devpod_workspaces();
7526
7905
  init_file_lock();
7527
7906
  init_workspace();
7528
7907
  READ_ONLY_GIT_ENV = { ...process.env, GIT_OPTIONAL_LOCKS: "0" };
7529
7908
  OWNERSHIP_VERSION = 1;
7530
- OWNERSHIP_DIR = import_node_path20.default.join("devrouter", "workspaces");
7909
+ OWNERSHIP_DIR = import_node_path21.default.join("devrouter", "workspaces");
7531
7910
  }
7532
7911
  });
7533
7912
 
@@ -7541,10 +7920,10 @@ function inRepositoryWorkspaceScope(repoPath, worktreePath, livePaths) {
7541
7920
  if (livePaths.some((candidate) => sameWorkspacePath(candidate, worktreePath))) return true;
7542
7921
  const comparableRepo = comparableWorkspacePath(repoPath);
7543
7922
  const comparableWorktree = comparableWorkspacePath(worktreePath);
7544
- const localRoot = import_node_path21.default.join(comparableRepo, "trees") + import_node_path21.default.sep;
7923
+ const localRoot = import_node_path22.default.join(comparableRepo, "trees") + import_node_path22.default.sep;
7545
7924
  if (comparableWorktree.startsWith(localRoot)) return true;
7546
- const legacyPrefix = `${import_node_path21.default.basename(comparableRepo)}-`;
7547
- return import_node_path21.default.dirname(comparableWorktree) === import_node_path21.default.dirname(comparableRepo) && import_node_path21.default.basename(comparableWorktree).startsWith(legacyPrefix);
7925
+ const legacyPrefix = `${import_node_path22.default.basename(comparableRepo)}-`;
7926
+ return import_node_path22.default.dirname(comparableWorktree) === import_node_path22.default.dirname(comparableRepo) && import_node_path22.default.basename(comparableWorktree).startsWith(legacyPrefix);
7548
7927
  }
7549
7928
  function previewActions(devpodStatus, routeCount, includeRecord) {
7550
7929
  const actions = [
@@ -7796,11 +8175,11 @@ function applyWorkspaceGc(plan) {
7796
8175
  candidates
7797
8176
  };
7798
8177
  }
7799
- var import_node_path21;
8178
+ var import_node_path22;
7800
8179
  var init_workspace_gc = __esm({
7801
8180
  "src/core/workspace-gc.ts"() {
7802
8181
  "use strict";
7803
- import_node_path21 = __toESM(require("path"));
8182
+ import_node_path22 = __toESM(require("path"));
7804
8183
  init_devpod_mutation();
7805
8184
  init_devpod_workspaces();
7806
8185
  init_host_routes();
@@ -8153,7 +8532,7 @@ async function buildDoctorReport(options = {}) {
8153
8532
  const config = runtimeConfig.config;
8154
8533
  loadedConfig = config;
8155
8534
  loadedWorkspace = runtimeConfig.workspace;
8156
- const cliVersion = true ? "0.0.44" : "0.0.0-dev";
8535
+ const cliVersion = true ? "0.0.46" : "0.0.0-dev";
8157
8536
  const configVersion = config.devrouter?.version;
8158
8537
  if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
8159
8538
  addCheck(checks, {
@@ -8183,7 +8562,7 @@ async function buildDoctorReport(options = {}) {
8183
8562
  (app) => app.docker.composeFiles.map((filePath) => ({
8184
8563
  app: app.name,
8185
8564
  filePath,
8186
- absolutePath: import_node_path22.default.resolve(repo.path, filePath)
8565
+ absolutePath: import_node_path23.default.resolve(repo.path, filePath)
8187
8566
  }))
8188
8567
  ).filter((entry) => !import_node_fs22.default.existsSync(entry.absolutePath));
8189
8568
  addCheck(checks, {
@@ -8228,7 +8607,7 @@ async function buildDoctorReport(options = {}) {
8228
8607
  const missingHostCwds = config.apps.filter((app) => app.runtime === "host").map((app) => ({
8229
8608
  app: app.name,
8230
8609
  cwd: app.hostRun.cwd,
8231
- absolutePath: import_node_path22.default.resolve(repo.path, app.hostRun.cwd)
8610
+ absolutePath: import_node_path23.default.resolve(repo.path, app.hostRun.cwd)
8232
8611
  })).filter((entry) => !import_node_fs22.default.existsSync(entry.absolutePath));
8233
8612
  addCheck(checks, {
8234
8613
  id: "repo.host-cwd",
@@ -8373,12 +8752,12 @@ async function buildDoctorReport(options = {}) {
8373
8752
  nextSteps
8374
8753
  };
8375
8754
  }
8376
- var import_node_fs22, import_node_path22, import_yaml5, POSTGRES_DEFAULTS;
8755
+ var import_node_fs22, import_node_path23, import_yaml5, POSTGRES_DEFAULTS;
8377
8756
  var init_doctor = __esm({
8378
8757
  "src/core/doctor.ts"() {
8379
8758
  "use strict";
8380
8759
  import_node_fs22 = __toESM(require("fs"));
8381
- import_node_path22 = __toESM(require("path"));
8760
+ import_node_path23 = __toESM(require("path"));
8382
8761
  import_yaml5 = __toESM(require("yaml"));
8383
8762
  init_docker();
8384
8763
  init_host_routes();
@@ -8873,11 +9252,11 @@ var init_route_publication = __esm({
8873
9252
  // src/core/workspace-ensure.ts
8874
9253
  function assertOverlay(container, repoPath) {
8875
9254
  const workingDir = container.labels["com.docker.compose.project.working_dir"];
8876
- if (!workingDir || !sameWorkspacePath(workingDir, import_node_path23.default.join(repoPath, ".devcontainer"))) {
9255
+ if (!workingDir || !sameWorkspacePath(workingDir, import_node_path24.default.join(repoPath, ".devcontainer"))) {
8877
9256
  throw new Error(`Container '${container.id}' does not belong to the exact worktree.`);
8878
9257
  }
8879
9258
  const configFiles = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").filter(Boolean);
8880
- const expectedOverlay = import_node_path23.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
9259
+ const expectedOverlay = import_node_path24.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
8881
9260
  if (!configFiles.some((configFile) => sameWorkspacePath(configFile, expectedOverlay))) {
8882
9261
  throw new Error(`Container '${container.id}' was not started with ${DEVCONTAINER_OVERLAY}.`);
8883
9262
  }
@@ -9053,6 +9432,20 @@ function isWarmWorkspaceActive(repoPath) {
9053
9432
  return true;
9054
9433
  }
9055
9434
  }
9435
+ function hasExactManagedComposeProject(repoPath, state) {
9436
+ try {
9437
+ return inspectWorkspaceContainers({
9438
+ composeProject: state.composeProject
9439
+ }).some((container) => {
9440
+ const workingDir = container.labels["com.docker.compose.project.working_dir"];
9441
+ return Boolean(
9442
+ workingDir && sameWorkspacePath(workingDir, import_node_path24.default.join(repoPath, ".devcontainer"))
9443
+ );
9444
+ });
9445
+ } catch {
9446
+ return true;
9447
+ }
9448
+ }
9056
9449
  function captureFirstTransitionBaseline(options) {
9057
9450
  const containers = inspectWorkspaceContainers();
9058
9451
  const runningPrimary = workspaceAppContainers(containers, options.repoPath).filter(
@@ -9229,25 +9622,30 @@ async function waitForHttpRoutes(repoPath, apps, timeoutMs) {
9229
9622
  );
9230
9623
  }
9231
9624
  function resolveLinkedTarget(repoPath) {
9232
- const devpods = listDevpodWorkspaces(repoPath);
9233
- const existingDevpod = selectDevpodWorkspace(devpods, repoPath);
9234
- const persisted = readPersistedWorkspace(repoPath);
9235
- const candidate = existingDevpod?.id ?? persisted ?? resolveWorktreeWorkspace(repoPath);
9236
- if (!candidate) {
9237
- throw new Error(`Could not resolve a workspace identity for '${repoPath}'.`);
9238
- }
9239
- const otherOwner = devpods.find(
9240
- (devpod) => devpod.id === candidate && !sameWorkspacePath(devpod.source.localFolder, repoPath)
9625
+ const snapshots = getWorkspaceRegistrySnapshots();
9626
+ const providerWorkspaces = [
9627
+ ...snapshots.devpod ?? [],
9628
+ ...snapshots.devsy?.map((workspace) => ({
9629
+ id: workspace.id,
9630
+ source: workspace.source,
9631
+ ...workspace.lastUsed ? { lastUsed: workspace.lastUsed } : {},
9632
+ ...workspace.lastUsedMalformed ? { lastUsedMalformed: true } : {}
9633
+ })) ?? []
9634
+ ];
9635
+ const branch = currentBranch(repoPath);
9636
+ const claim = claimWorkspaceIdentity(repoPath, {
9637
+ source: branch ?? repoPath,
9638
+ branch: branch ?? null,
9639
+ providerWorkspaces,
9640
+ unavailableRuntimes: snapshots.unavailable
9641
+ });
9642
+ const existingDevpod = providerWorkspaces.find(
9643
+ (workspace) => workspace.id === claim.devpodId && sameWorkspacePath(workspace.source.localFolder, repoPath)
9241
9644
  );
9242
- if (otherOwner) {
9243
- throw new Error(
9244
- `DevPod identity '${candidate}' already belongs to '${otherOwner.source.localFolder}'.`
9245
- );
9246
- }
9247
9645
  return {
9248
9646
  kind: "linked",
9249
- workspace: persistWorkspace(repoPath, candidate),
9250
- devpodId: candidate,
9647
+ workspace: claim.workspace,
9648
+ devpodId: claim.devpodId,
9251
9649
  hadExactDevpod: Boolean(existingDevpod),
9252
9650
  gitCommonDir: resolveGitCommonDir(repoPath)
9253
9651
  };
@@ -9262,7 +9660,7 @@ function resolvePrimaryTarget(repoPath) {
9262
9660
  }
9263
9661
  function isPrimaryCheckout(repoPath) {
9264
9662
  try {
9265
- return import_node_fs23.default.statSync(import_node_path23.default.join(repoPath, ".git")).isDirectory();
9663
+ return import_node_fs23.default.statSync(import_node_path24.default.join(repoPath, ".git")).isDirectory();
9266
9664
  } catch {
9267
9665
  return false;
9268
9666
  }
@@ -9312,7 +9710,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
9312
9710
  const target = linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
9313
9711
  let devpodId = target.devpodId;
9314
9712
  if (target.kind === "linked") {
9315
- const overlayPath = import_node_path23.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
9713
+ const overlayPath = import_node_path24.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
9316
9714
  if (!import_node_fs23.default.existsSync(overlayPath)) {
9317
9715
  throw new Error(`Missing required DevPod compose overlay: ${overlayPath}`);
9318
9716
  }
@@ -9330,6 +9728,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
9330
9728
  const desiredProcesses = managedRuntime ? desiredManagedProcesses(managedRuntime, runtime.resolvedProfile) : [];
9331
9729
  if (managedRuntime) {
9332
9730
  previousManagedState = readManagedRuntimeState(repoPath, target.workspace);
9731
+ if (previousManagedState && !hasExactManagedComposeProject(repoPath, previousManagedState)) {
9732
+ previousManagedState = void 0;
9733
+ }
9333
9734
  if (previousManagedState?.status === "degraded") {
9334
9735
  throw new Error(
9335
9736
  "Managed runtime state is degraded; refusing a new profile transition until drift is repaired."
@@ -9380,7 +9781,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
9380
9781
  }
9381
9782
  const apps = proxyAppsFromConfig(runtime.config);
9382
9783
  const parsedUpstreams = apps.map((app) => parseUpstream(app.upstream));
9383
- const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path23.default.basename(repoPath)) ?? "app";
9784
+ const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path24.default.basename(repoPath)) ?? "app";
9384
9785
  for (const [index, app] of apps.entries()) {
9385
9786
  if (!parsedUpstreams[index].host.startsWith(`${aliasPrefix}-`)) {
9386
9787
  const owner = target.kind === "linked" ? "workspace" : "checkout";
@@ -9394,20 +9795,11 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
9394
9795
  managedConfigWritten = true;
9395
9796
  }
9396
9797
  const upstreamHosts = parsedUpstreams.map((upstream) => upstream.host);
9397
- const ownership = target.kind === "linked" ? {
9398
- workspace: target.workspace,
9399
- worktreePath: repoPath,
9400
- branch: currentBranch(repoPath),
9401
- devpodId: target.devpodId
9402
- } : void 0;
9403
- if (ownership) {
9404
- writeWorkspaceOwnership(repoPath, ownership);
9405
- }
9406
9798
  const currentTarget = () => target.kind === "linked" ? target : { ...target, devpodId };
9407
- const startAndProveAttachment = (recreate = false) => {
9799
+ const startAndProveAttachment = async (recreate = false) => {
9408
9800
  const requestedTarget = currentTarget();
9409
9801
  try {
9410
- devpodId = startDevpodWorkspace({
9802
+ devpodId = await startDevpodWorkspace({
9411
9803
  repoPath,
9412
9804
  devpodId: requestedTarget.devpodId,
9413
9805
  devcontainerPath: managedPlan?.generatedRelativePath,
@@ -9425,19 +9817,16 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
9425
9817
  if (error instanceof DevpodStartPostconditionError) environmentStarted = true;
9426
9818
  throw error;
9427
9819
  }
9428
- if (ownership) {
9429
- writeWorkspaceOwnership(repoPath, ownership);
9430
- }
9431
9820
  };
9432
9821
  const preflight = (timeoutMs) => waitForContainerPreflight(repoPath, currentTarget(), upstreamHosts, timeoutMs);
9433
9822
  const recreateAndPreflight = async () => {
9434
- startAndProveAttachment(true);
9823
+ await startAndProveAttachment(true);
9435
9824
  return preflight(options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS);
9436
9825
  };
9437
9826
  let recreated = false;
9438
9827
  let container;
9439
9828
  try {
9440
- startAndProveAttachment();
9829
+ await startAndProveAttachment();
9441
9830
  } catch (error) {
9442
9831
  if (managedPlan || !target.hadExactDevpod) {
9443
9832
  throw error;
@@ -9824,13 +10213,13 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
9824
10213
  }
9825
10214
  });
9826
10215
  }
9827
- var import_node_child_process19, import_node_fs23, import_node_path23, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
10216
+ var import_node_child_process19, import_node_fs23, import_node_path24, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
9828
10217
  var init_workspace_ensure = __esm({
9829
10218
  "src/core/workspace-ensure.ts"() {
9830
10219
  "use strict";
9831
10220
  import_node_child_process19 = require("child_process");
9832
10221
  import_node_fs23 = __toESM(require("fs"));
9833
- import_node_path23 = __toESM(require("path"));
10222
+ import_node_path24 = __toESM(require("path"));
9834
10223
  init_devcontainer_profile();
9835
10224
  init_devpod_environment();
9836
10225
  init_devpod_mutation();
@@ -9845,6 +10234,7 @@ var init_workspace_ensure = __esm({
9845
10234
  init_router();
9846
10235
  init_workspace();
9847
10236
  init_workspace_ownership();
10237
+ init_workspace_runtime();
9848
10238
  DEVCONTAINER_OVERLAY = "docker-compose.devrouter.yml";
9849
10239
  DEFAULT_READINESS_TIMEOUT_MS = 12e4;
9850
10240
  POLL_INTERVAL_MS = 1e3;
@@ -9901,6 +10291,11 @@ var init_ensure = __esm({
9901
10291
  });
9902
10292
 
9903
10293
  // src/core/workspace-lifecycle.ts
10294
+ function withWorkspaceAllocationTransaction(repoPath, operation) {
10295
+ return withWorkspaceOwnershipTransaction(repoPath, operation, {
10296
+ waitMs: WORKSPACE_ALLOCATION_LOCK_WAIT_MS
10297
+ });
10298
+ }
9904
10299
  function warnMissingWorkspaceOwnership(repoPath) {
9905
10300
  const missing = listMissingWorkspaceOwnership(repoPath);
9906
10301
  if (missing.length === 0) return;
@@ -9910,7 +10305,7 @@ function warnMissingWorkspaceOwnership(repoPath) {
9910
10305
  );
9911
10306
  }
9912
10307
  function defaultWorktreePath(mainRepo, ws) {
9913
- return import_node_path24.default.join(mainRepo, "trees", ws);
10308
+ return import_node_path25.default.join(mainRepo, "trees", ws);
9914
10309
  }
9915
10310
  function assertDefaultWorktreeRootIgnored(mainRepo) {
9916
10311
  const ignored = (0, import_node_child_process20.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
@@ -9918,12 +10313,12 @@ function assertDefaultWorktreeRootIgnored(mainRepo) {
9918
10313
  });
9919
10314
  if (ignored.status !== 0) {
9920
10315
  throw new Error(
9921
- `Default worktree root '${import_node_path24.default.join(mainRepo, "trees")}' is not ignored. Add 'trees/' to '${import_node_path24.default.join(mainRepo, ".gitignore")}' or use --path.`
10316
+ `Default worktree root '${import_node_path25.default.join(mainRepo, "trees")}' is not ignored. Add 'trees/' to '${import_node_path25.default.join(mainRepo, ".gitignore")}' or use --path.`
9922
10317
  );
9923
10318
  }
9924
10319
  }
9925
10320
  function legacyDefaultWorktreePath(mainRepo, ws) {
9926
- return import_node_path24.default.join(import_node_path24.default.dirname(mainRepo), `${import_node_path24.default.basename(mainRepo)}-${ws}`);
10321
+ return import_node_path25.default.join(import_node_path25.default.dirname(mainRepo), `${import_node_path25.default.basename(mainRepo)}-${ws}`);
9927
10322
  }
9928
10323
  function teardownFallbackPath(mainRepo, workspace) {
9929
10324
  const candidates = [
@@ -10062,44 +10457,67 @@ async function workspaceUp(branch, opts = {}) {
10062
10457
  if (!ws) {
10063
10458
  throw new Error(`Branch '${branch}' does not yield a valid workspace token.`);
10064
10459
  }
10065
- const worktreePath = opts.path ? import_node_path24.default.resolve(opts.path) : defaultWorktreePath(mainRepo, ws);
10066
- if (import_node_fs24.default.existsSync(worktreePath)) {
10067
- const registered = listGitWorktrees(mainRepo).find(
10068
- (worktree) => sameWorkspacePath(worktree.path, worktreePath)
10460
+ const worktreePath = withWorkspaceAllocationTransaction(mainRepo, () => {
10461
+ const worktrees = listGitWorktrees(mainRepo);
10462
+ const branchWorktrees = worktrees.filter((worktree) => worktree.branch === branch);
10463
+ if (branchWorktrees.length > 1) {
10464
+ throw new Error(
10465
+ `Branch '${branch}' is checked out in multiple worktrees: ${branchWorktrees.map((worktree) => worktree.path).join(", ")}`
10466
+ );
10467
+ }
10468
+ const existingBranch = branchWorktrees[0];
10469
+ const candidatePaths = workspaceIdentityCandidates(branch).map(
10470
+ (candidate) => defaultWorktreePath(mainRepo, candidate)
10471
+ );
10472
+ const generatedPath = candidatePaths.find(
10473
+ (candidate) => !import_node_fs24.default.existsSync(candidate) && !worktrees.some((worktree) => sameWorkspacePath(worktree.path, candidate))
10069
10474
  );
10070
- if (!registered || sameWorkspacePath(registered.path, mainRepo)) {
10071
- throw new Error(`Existing path '${worktreePath}' is not a linked worktree of '${mainRepo}'.`);
10475
+ const selectedPath = opts.path ? import_node_path25.default.resolve(opts.path) : existingBranch?.path ?? generatedPath;
10476
+ if (!selectedPath) {
10477
+ throw new Error(`Could not allocate a collision-safe worktree path for branch '${branch}'.`);
10072
10478
  }
10073
- if (registered.branch !== branch) {
10479
+ if (existingBranch && !sameWorkspacePath(existingBranch.path, selectedPath)) {
10074
10480
  throw new Error(
10075
- `Existing worktree '${worktreePath}' uses branch '${registered.branch ?? "detached"}', not '${branch}'.`
10481
+ `Branch '${branch}' already uses worktree '${existingBranch.path}', not '${selectedPath}'.`
10076
10482
  );
10077
10483
  }
10078
- process.stdout.write(`Worktree already exists: ${worktreePath}
10484
+ if (import_node_fs24.default.existsSync(selectedPath)) {
10485
+ const registered = worktrees.find(
10486
+ (worktree) => sameWorkspacePath(worktree.path, selectedPath)
10487
+ );
10488
+ if (!registered || sameWorkspacePath(registered.path, mainRepo)) {
10489
+ throw new Error(
10490
+ `Existing path '${selectedPath}' is not a linked worktree of '${mainRepo}'.`
10491
+ );
10492
+ }
10493
+ if (registered.branch !== branch) {
10494
+ throw new Error(
10495
+ `Existing worktree '${selectedPath}' uses branch '${registered.branch ?? "detached"}', not '${branch}'.`
10496
+ );
10497
+ }
10498
+ process.stdout.write(`Worktree already exists: ${selectedPath}
10079
10499
  `);
10080
- } else {
10081
- if (!opts.path) {
10082
- assertDefaultWorktreeRootIgnored(mainRepo);
10500
+ return selectedPath;
10083
10501
  }
10084
- const add = (0, import_node_child_process20.spawnSync)("git", ["-C", mainRepo, "worktree", "add", worktreePath, branch], {
10502
+ if (!opts.path) assertDefaultWorktreeRootIgnored(mainRepo);
10503
+ const add = (0, import_node_child_process20.spawnSync)("git", ["-C", mainRepo, "worktree", "add", selectedPath, branch], {
10085
10504
  encoding: "utf-8"
10086
10505
  });
10087
10506
  if (add.status !== 0) {
10088
10507
  const addNew = (0, import_node_child_process20.spawnSync)(
10089
10508
  "git",
10090
- ["-C", mainRepo, "worktree", "add", "-b", branch, worktreePath],
10091
- {
10092
- encoding: "utf-8"
10093
- }
10509
+ ["-C", mainRepo, "worktree", "add", "-b", branch, selectedPath],
10510
+ { encoding: "utf-8" }
10094
10511
  );
10095
10512
  if (addNew.status !== 0) {
10096
- const detail = [add.stderr, addNew.stderr].map((s) => s?.trim()).filter(Boolean).join("; ");
10513
+ const detail = [add.stderr, addNew.stderr].map((stderr) => stderr?.trim()).filter(Boolean).join("; ");
10097
10514
  throw new Error(`git worktree add failed: ${detail || "unknown error"}`);
10098
10515
  }
10099
10516
  }
10100
- process.stdout.write(`Created worktree ${worktreePath} (workspace '${ws}')
10517
+ process.stdout.write(`Created worktree ${selectedPath}
10101
10518
  `);
10102
- }
10519
+ return selectedPath;
10520
+ });
10103
10521
  if (opts.noDevpod) {
10104
10522
  warnMissingWorkspaceOwnership(mainRepo);
10105
10523
  process.stdout.write("Skipped environment startup; no routes were changed.\n");
@@ -10242,13 +10660,13 @@ async function workspaceStop(target, opts = {}) {
10242
10660
  async function workspaceDown(target, opts = {}) {
10243
10661
  return runWorkspaceLifecycle("down", target, opts);
10244
10662
  }
10245
- var import_node_child_process20, import_node_fs24, import_node_path24;
10663
+ var import_node_child_process20, import_node_fs24, import_node_path25, WORKSPACE_ALLOCATION_LOCK_WAIT_MS;
10246
10664
  var init_workspace_lifecycle = __esm({
10247
10665
  "src/core/workspace-lifecycle.ts"() {
10248
10666
  "use strict";
10249
10667
  import_node_child_process20 = require("child_process");
10250
10668
  import_node_fs24 = __toESM(require("fs"));
10251
- import_node_path24 = __toESM(require("path"));
10669
+ import_node_path25 = __toESM(require("path"));
10252
10670
  init_devpod_mutation();
10253
10671
  init_devpod_workspaces();
10254
10672
  init_repo_config();
@@ -10256,6 +10674,7 @@ var init_workspace_lifecycle = __esm({
10256
10674
  init_workspace();
10257
10675
  init_workspace_ensure();
10258
10676
  init_workspace_ownership();
10677
+ WORKSPACE_ALLOCATION_LOCK_WAIT_MS = 6e4;
10259
10678
  }
10260
10679
  });
10261
10680
 
@@ -10449,7 +10868,7 @@ async function devpodExec(repoPath, command) {
10449
10868
  }
10450
10869
  assertRunningDevpod(devpod.id, repoPath);
10451
10870
  const workspaceDirectory = resolveWorkspaceDirectory(repoPath);
10452
- const statusMarker = `__DEVROUTER_EXIT_${(0, import_node_crypto7.randomUUID)()}__:`;
10871
+ const statusMarker = `__DEVROUTER_EXIT_${(0, import_node_crypto8.randomUUID)()}__:`;
10453
10872
  const statusMarkerBytes = Buffer.from(statusMarker, "ascii");
10454
10873
  const literalCommand = command.map(quotePosixArg).join(" ");
10455
10874
  const wrappedCommand = `${literalCommand}; __devrouter_status=$?; printf '${statusMarker}%s\\n' "$__devrouter_status" >&2; exit 0`;
@@ -10526,12 +10945,12 @@ async function devpodExec(repoPath, command) {
10526
10945
  });
10527
10946
  });
10528
10947
  }
10529
- var import_node_child_process22, import_node_crypto7, DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC;
10948
+ var import_node_child_process22, import_node_crypto8, DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC;
10530
10949
  var init_devpod_exec = __esm({
10531
10950
  "src/core/devpod-exec.ts"() {
10532
10951
  "use strict";
10533
10952
  import_node_child_process22 = require("child_process");
10534
- import_node_crypto7 = require("crypto");
10953
+ import_node_crypto8 = require("crypto");
10535
10954
  init_devpod_environment();
10536
10955
  init_devpod_workspaces();
10537
10956
  init_devsy_exec();
@@ -10826,7 +11245,7 @@ function readJson(filePath) {
10826
11245
  }
10827
11246
  }
10828
11247
  function relative(repoPath, filePath) {
10829
- return import_node_path25.default.relative(repoPath, filePath) || ".";
11248
+ return import_node_path26.default.relative(repoPath, filePath) || ".";
10830
11249
  }
10831
11250
  function redactEnvAssignments(value) {
10832
11251
  return value.replace(
@@ -10867,7 +11286,7 @@ function inspectPackageManager(repoPath, pkg) {
10867
11286
  ["bun.lock", "bun"]
10868
11287
  ];
10869
11288
  for (const [fileName, name] of lockfiles) {
10870
- if (import_node_fs25.default.existsSync(import_node_path25.default.join(repoPath, fileName))) {
11289
+ if (import_node_fs25.default.existsSync(import_node_path26.default.join(repoPath, fileName))) {
10871
11290
  return { name, source: fileName };
10872
11291
  }
10873
11292
  }
@@ -10882,7 +11301,7 @@ function inspectNode(repoPath, pkg) {
10882
11301
  if (typeof engines?.node === "string") {
10883
11302
  return { version: engines.node, source: "package.json:engines.node" };
10884
11303
  }
10885
- const nvmrc = import_node_path25.default.join(repoPath, ".nvmrc");
11304
+ const nvmrc = import_node_path26.default.join(repoPath, ".nvmrc");
10886
11305
  if (import_node_fs25.default.existsSync(nvmrc)) {
10887
11306
  const version = import_node_fs25.default.readFileSync(nvmrc, "utf-8").trim();
10888
11307
  return { version, source: ".nvmrc" };
@@ -10945,7 +11364,7 @@ function configuredComposeFiles(repoPath) {
10945
11364
  const files = config.apps.filter(
10946
11365
  (app) => app.runtime === "docker"
10947
11366
  ).flatMap((app) => app.docker.composeFiles).filter(
10948
- (fileName) => !import_node_path25.default.isAbsolute(fileName) && !import_node_path25.default.normalize(fileName).startsWith("..")
11367
+ (fileName) => !import_node_path26.default.isAbsolute(fileName) && !import_node_path26.default.normalize(fileName).startsWith("..")
10949
11368
  );
10950
11369
  return Array.from(new Set(files));
10951
11370
  } catch {
@@ -10963,7 +11382,7 @@ function composeFiles(repoPath) {
10963
11382
  ...configuredComposeFiles(repoPath)
10964
11383
  ];
10965
11384
  return Array.from(new Set(candidates)).filter(
10966
- (fileName) => import_node_fs25.default.existsSync(import_node_path25.default.join(repoPath, fileName))
11385
+ (fileName) => import_node_fs25.default.existsSync(import_node_path26.default.join(repoPath, fileName))
10967
11386
  );
10968
11387
  }
10969
11388
  function stringArray2(value) {
@@ -11001,7 +11420,7 @@ function inspectServices(repoPath) {
11001
11420
  const services = [];
11002
11421
  for (const fileName of composeFiles(repoPath)) {
11003
11422
  try {
11004
- const parsed = import_yaml6.default.parse(import_node_fs25.default.readFileSync(import_node_path25.default.join(repoPath, fileName), "utf-8"));
11423
+ const parsed = import_yaml6.default.parse(import_node_fs25.default.readFileSync(import_node_path26.default.join(repoPath, fileName), "utf-8"));
11005
11424
  const serviceMap = asRecord3(asRecord3(parsed)?.services);
11006
11425
  for (const [name, value] of Object.entries(serviceMap ?? {})) {
11007
11426
  const service = asRecord3(value);
@@ -11037,7 +11456,7 @@ function inspectServices(repoPath) {
11037
11456
  }
11038
11457
  function inspectEnvFiles(repoPath) {
11039
11458
  const files = import_node_fs25.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
11040
- const content = import_node_fs25.default.readFileSync(import_node_path25.default.join(repoPath, fileName), "utf-8");
11459
+ const content = import_node_fs25.default.readFileSync(import_node_path26.default.join(repoPath, fileName), "utf-8");
11041
11460
  const names = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#") && line.includes("=")).map((line) => line.split("=")[0]?.trim()).filter((name) => Boolean(name)).sort();
11042
11461
  return { path: fileName, names };
11043
11462
  });
@@ -11051,13 +11470,13 @@ function inspectEnvFiles(repoPath) {
11051
11470
  };
11052
11471
  }
11053
11472
  function inspectDevcontainer(repoPath) {
11054
- const dir = import_node_path25.default.join(repoPath, ".devcontainer");
11473
+ const dir = import_node_path26.default.join(repoPath, ".devcontainer");
11055
11474
  if (!import_node_fs25.default.existsSync(dir)) {
11056
11475
  return { exists: false, files: [] };
11057
11476
  }
11058
11477
  return {
11059
11478
  exists: true,
11060
- files: import_node_fs25.default.readdirSync(dir).filter((fileName) => import_node_fs25.default.statSync(import_node_path25.default.join(dir, fileName)).isFile()).sort().map((fileName) => `.devcontainer/${fileName}`)
11479
+ files: import_node_fs25.default.readdirSync(dir).filter((fileName) => import_node_fs25.default.statSync(import_node_path26.default.join(dir, fileName)).isFile()).sort().map((fileName) => `.devcontainer/${fileName}`)
11061
11480
  };
11062
11481
  }
11063
11482
  function inspectDevrouter(repoPath) {
@@ -11106,14 +11525,14 @@ function inspectAgentGuidance(repoPath) {
11106
11525
  ["AGENTS.md", "agents"],
11107
11526
  ["CLAUDE.md", "claude"]
11108
11527
  ]) {
11109
- if (import_node_fs25.default.existsSync(import_node_path25.default.join(repoPath, fileName))) {
11528
+ if (import_node_fs25.default.existsSync(import_node_path26.default.join(repoPath, fileName))) {
11110
11529
  results.push({ path: fileName, kind });
11111
11530
  }
11112
11531
  }
11113
- const skillsDir = import_node_path25.default.join(repoPath, ".agents", "skills");
11532
+ const skillsDir = import_node_path26.default.join(repoPath, ".agents", "skills");
11114
11533
  if (import_node_fs25.default.existsSync(skillsDir)) {
11115
11534
  for (const name of import_node_fs25.default.readdirSync(skillsDir).sort()) {
11116
- const skillPath = import_node_path25.default.join(skillsDir, name, "SKILL.md");
11535
+ const skillPath = import_node_path26.default.join(skillsDir, name, "SKILL.md");
11117
11536
  if (import_node_fs25.default.existsSync(skillPath)) {
11118
11537
  results.push({ path: relative(repoPath, skillPath), kind: "skill" });
11119
11538
  }
@@ -11157,7 +11576,7 @@ function buildIssues(report) {
11157
11576
  }
11158
11577
  function inspectRepo(options = {}) {
11159
11578
  const repoPath = resolveRepoPath(options.repo);
11160
- const pkg = readJson(import_node_path25.default.join(repoPath, "package.json"));
11579
+ const pkg = readJson(import_node_path26.default.join(repoPath, "package.json"));
11161
11580
  const scripts = inspectScripts(pkg);
11162
11581
  const reportWithoutIssues = {
11163
11582
  repoPath,
@@ -11176,12 +11595,12 @@ function inspectRepo(options = {}) {
11176
11595
  issues: buildIssues(reportWithoutIssues)
11177
11596
  };
11178
11597
  }
11179
- var import_node_fs25, import_node_path25, import_yaml6;
11598
+ var import_node_fs25, import_node_path26, import_yaml6;
11180
11599
  var init_repo_inspect = __esm({
11181
11600
  "src/core/repo-inspect.ts"() {
11182
11601
  "use strict";
11183
11602
  import_node_fs25 = __toESM(require("fs"));
11184
- import_node_path25 = __toESM(require("path"));
11603
+ import_node_path26 = __toESM(require("path"));
11185
11604
  import_yaml6 = __toESM(require("yaml"));
11186
11605
  init_repo_config();
11187
11606
  }
@@ -11286,7 +11705,7 @@ function requiredFileChecks(repoPath) {
11286
11705
  ".devcontainer/docker-compose.yml",
11287
11706
  ".devrouter.yml"
11288
11707
  ];
11289
- const missing = required.filter((fileName) => !import_node_fs26.default.existsSync(import_node_path26.default.join(repoPath, fileName)));
11708
+ const missing = required.filter((fileName) => !import_node_fs26.default.existsSync(import_node_path27.default.join(repoPath, fileName)));
11290
11709
  return {
11291
11710
  id: "repo.devcontainer.verify-files",
11292
11711
  level: missing.length === 0 ? "ok" : "error",
@@ -11505,12 +11924,12 @@ async function verifyDevcontainer(options = {}) {
11505
11924
  nextSteps: collectNextSteps3(checks)
11506
11925
  };
11507
11926
  }
11508
- var import_node_fs26, import_node_path26;
11927
+ var import_node_fs26, import_node_path27;
11509
11928
  var init_devcontainer_verify = __esm({
11510
11929
  "src/core/devcontainer-verify.ts"() {
11511
11930
  "use strict";
11512
11931
  import_node_fs26 = __toESM(require("fs"));
11513
- import_node_path26 = __toESM(require("path"));
11932
+ import_node_path27 = __toESM(require("path"));
11514
11933
  init_capabilities();
11515
11934
  init_doctor();
11516
11935
  init_host_routes();
@@ -11807,7 +12226,7 @@ function packageManagerIssues(repo) {
11807
12226
  }
11808
12227
  function plannedFiles(repoPath, version) {
11809
12228
  const repo = inspectRepo({ repo: repoPath });
11810
- const projectName = sanitizeProjectName(import_node_path27.default.basename(repo.repoPath));
12229
+ const projectName = sanitizeProjectName(import_node_path28.default.basename(repo.repoPath));
11811
12230
  const nodeMajor = majorVersion(repo.node?.version, "24");
11812
12231
  const pnpmVersion = repo.packageManager?.name === "pnpm" && repo.packageManager.version ? repo.packageManager.version : DEFAULT_PNPM_VERSION;
11813
12232
  const port = inferPort2(repo);
@@ -11852,7 +12271,7 @@ function plannedFiles(repoPath, version) {
11852
12271
  };
11853
12272
  }
11854
12273
  function classifyFile(repoPath, file) {
11855
- const absolutePath = import_node_path27.default.join(repoPath, file.relativePath);
12274
+ const absolutePath = import_node_path28.default.join(repoPath, file.relativePath);
11856
12275
  if (!import_node_fs27.default.existsSync(absolutePath)) {
11857
12276
  return {
11858
12277
  path: file.relativePath,
@@ -11918,8 +12337,8 @@ function buildPlan(repoPath, dryRun, version) {
11918
12337
  };
11919
12338
  }
11920
12339
  function writeFile(repoPath, file) {
11921
- const absolutePath = import_node_path27.default.join(repoPath, file.relativePath);
11922
- import_node_fs27.default.mkdirSync(import_node_path27.default.dirname(absolutePath), { recursive: true });
12340
+ const absolutePath = import_node_path28.default.join(repoPath, file.relativePath);
12341
+ import_node_fs27.default.mkdirSync(import_node_path28.default.dirname(absolutePath), { recursive: true });
11923
12342
  import_node_fs27.default.writeFileSync(absolutePath, file.content, "utf-8");
11924
12343
  if (file.executable) {
11925
12344
  import_node_fs27.default.chmodSync(absolutePath, 493);
@@ -11960,12 +12379,12 @@ function writeDevcontainer(options = {}) {
11960
12379
  nextSteps: postWriteNextSteps(repoPath)
11961
12380
  };
11962
12381
  }
11963
- var import_node_fs27, import_node_path27, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
12382
+ var import_node_fs27, import_node_path28, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
11964
12383
  var init_devcontainer_write = __esm({
11965
12384
  "src/core/devcontainer-write.ts"() {
11966
12385
  "use strict";
11967
12386
  import_node_fs27 = __toESM(require("fs"));
11968
- import_node_path27 = __toESM(require("path"));
12387
+ import_node_path28 = __toESM(require("path"));
11969
12388
  init_repo_config();
11970
12389
  init_repo_inspect();
11971
12390
  MANAGED_MARKER2 = "devrouter:managed devcontainer";
@@ -12356,7 +12775,7 @@ function sanitizeRouterId(value) {
12356
12775
  return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
12357
12776
  }
12358
12777
  function repoHash(repoPath) {
12359
- return (0, import_node_crypto8.createHash)("sha1").update(import_node_path28.default.resolve(repoPath)).digest("hex").slice(0, 12);
12778
+ return (0, import_node_crypto9.createHash)("sha1").update(import_node_path29.default.resolve(repoPath)).digest("hex").slice(0, 12);
12360
12779
  }
12361
12780
  function asDockerApp(app) {
12362
12781
  return app.runtime === "docker";
@@ -12435,9 +12854,9 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
12435
12854
  if (dockerApps.length === 0) {
12436
12855
  throw new Error("No docker apps selected to prepare compose overlay.");
12437
12856
  }
12438
- const cachePath = import_node_path28.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
12857
+ const cachePath = import_node_path29.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
12439
12858
  import_node_fs28.default.mkdirSync(cachePath, { recursive: true });
12440
- const overlayPath = import_node_path28.default.join(cachePath, "compose.devrouter.yml");
12859
+ const overlayPath = import_node_path29.default.join(cachePath, "compose.devrouter.yml");
12441
12860
  const overlayDocument = buildOverlayDocument(dockerApps, publishTcpPorts);
12442
12861
  import_node_fs28.default.writeFileSync(overlayPath, import_yaml7.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
12443
12862
  return {
@@ -12559,14 +12978,14 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
12559
12978
  const port = Number(match[1]);
12560
12979
  return Number.isInteger(port) && port > 0 ? port : void 0;
12561
12980
  }
12562
- var import_node_child_process25, import_node_crypto8, import_node_fs28, import_node_path28, import_yaml7;
12981
+ var import_node_child_process25, import_node_crypto9, import_node_fs28, import_node_path29, import_yaml7;
12563
12982
  var init_docker_run = __esm({
12564
12983
  "src/core/docker-run.ts"() {
12565
12984
  "use strict";
12566
12985
  import_node_child_process25 = require("child_process");
12567
- import_node_crypto8 = require("crypto");
12986
+ import_node_crypto9 = require("crypto");
12568
12987
  import_node_fs28 = __toESM(require("fs"));
12569
- import_node_path28 = __toESM(require("path"));
12988
+ import_node_path29 = __toESM(require("path"));
12570
12989
  import_yaml7 = __toESM(require("yaml"));
12571
12990
  init_docker_error_guidance();
12572
12991
  init_paths();
@@ -13420,7 +13839,7 @@ function measureWorktreeConsumption(worktreePath, options) {
13420
13839
  timedOut = true;
13421
13840
  break;
13422
13841
  }
13423
- const entryPath = import_node_path29.default.join(dirPath, entry.name);
13842
+ const entryPath = import_node_path30.default.join(dirPath, entry.name);
13424
13843
  let entryStat;
13425
13844
  try {
13426
13845
  entryStat = import_node_fs29.default.lstatSync(entryPath);
@@ -13497,12 +13916,12 @@ function describeError(error, worktreePath) {
13497
13916
  }
13498
13917
  return `could not stat worktree path '${worktreePath}': ${error?.message ?? String(error)}`;
13499
13918
  }
13500
- var import_node_fs29, import_node_path29, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
13919
+ var import_node_fs29, import_node_path30, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
13501
13920
  var init_workspace_consumption = __esm({
13502
13921
  "src/core/workspace-consumption.ts"() {
13503
13922
  "use strict";
13504
13923
  import_node_fs29 = __toESM(require("fs"));
13505
- import_node_path29 = __toESM(require("path"));
13924
+ import_node_path30 = __toESM(require("path"));
13506
13925
  init_devpod_environment();
13507
13926
  DEFAULT_DEADLINE_MS = 1e4;
13508
13927
  BLOCK_SIZE_BYTES = 512;
@@ -14128,7 +14547,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
14128
14547
  containers = measureContainersFn(worktreePaths);
14129
14548
  } catch (error) {
14130
14549
  const reason = `container measurement failed: ${describeCause(error)}`;
14131
- containers = new Map(worktreePaths.map((path29) => [path29, unknownContainers(reason)]));
14550
+ containers = new Map(worktreePaths.map((path30) => [path30, unknownContainers(reason)]));
14132
14551
  }
14133
14552
  }
14134
14553
  const rows = records.map((record) => {
@@ -14359,7 +14778,7 @@ var init_version = __esm({
14359
14778
 
14360
14779
  // src/cli.ts
14361
14780
  var import_commander = require("commander");
14362
- var CLI_VERSION = true ? "0.0.44" : "0.0.0-dev";
14781
+ var CLI_VERSION = true ? "0.0.46" : "0.0.0-dev";
14363
14782
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
14364
14783
  function withErrorHandling(action2) {
14365
14784
  return async (...args) => {