@devrouter/cli 0.0.45 → 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
@@ -322,7 +322,7 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
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 {
@@ -1073,7 +1197,7 @@ function workspaceIdentityCandidates(source) {
1073
1197
  return candidates;
1074
1198
  }
1075
1199
  function isLinkedWorktree(repoPath) {
1076
- const gitPath = import_node_path4.default.join(repoPath, ".git");
1200
+ const gitPath = import_node_path5.default.join(repoPath, ".git");
1077
1201
  let stat;
1078
1202
  try {
1079
1203
  stat = import_node_fs5.default.statSync(gitPath);
@@ -1097,7 +1221,7 @@ function isLinkedWorktree(repoPath) {
1097
1221
  return /(^|\/)worktrees\//.test(gitdir);
1098
1222
  }
1099
1223
  function gitMetadataDir(repoPath) {
1100
- const gitPath = import_node_path4.default.join(repoPath, ".git");
1224
+ const gitPath = import_node_path5.default.join(repoPath, ".git");
1101
1225
  let stat;
1102
1226
  try {
1103
1227
  stat = import_node_fs5.default.statSync(gitPath);
@@ -1114,7 +1238,7 @@ function gitMetadataDir(repoPath) {
1114
1238
  if (!match) {
1115
1239
  return void 0;
1116
1240
  }
1117
- return import_node_path4.default.resolve(repoPath, match[1].trim());
1241
+ return import_node_path5.default.resolve(repoPath, match[1].trim());
1118
1242
  }
1119
1243
  function validatePersistedWorkspace(value) {
1120
1244
  const workspace = value.trim();
@@ -1128,7 +1252,7 @@ function readPersistedWorkspace(repoPath) {
1128
1252
  if (!gitDir) {
1129
1253
  return void 0;
1130
1254
  }
1131
- const metadataPath = import_node_path4.default.join(gitDir, WORKSPACE_METADATA_FILE);
1255
+ const metadataPath = import_node_path5.default.join(gitDir, WORKSPACE_METADATA_FILE);
1132
1256
  try {
1133
1257
  return validatePersistedWorkspace(import_node_fs5.default.readFileSync(metadataPath, "utf-8"));
1134
1258
  } catch (error) {
@@ -1153,7 +1277,7 @@ function persistWorkspace(repoPath, value) {
1153
1277
  if (!gitDir) {
1154
1278
  throw new Error(`cannot persist workspace identity: '${repoPath}' is not a Git checkout`);
1155
1279
  }
1156
- writeFileAtomically(import_node_path4.default.join(gitDir, WORKSPACE_METADATA_FILE), `${workspace}
1280
+ writeFileAtomically(import_node_path5.default.join(gitDir, WORKSPACE_METADATA_FILE), `${workspace}
1157
1281
  `);
1158
1282
  return workspace;
1159
1283
  }
@@ -1162,7 +1286,7 @@ async function withWorkspaceLifecycleLock(repoPath, operation) {
1162
1286
  if (!gitDir) {
1163
1287
  throw new Error(`cannot lock workspace lifecycle: '${repoPath}' is not a Git checkout`);
1164
1288
  }
1165
- const lockPath = import_node_path4.default.join(gitDir, WORKSPACE_LOCK_FILE);
1289
+ const lockPath = import_node_path5.default.join(gitDir, WORKSPACE_LOCK_FILE);
1166
1290
  return withFileLock(
1167
1291
  lockPath,
1168
1292
  { activity: "workspace lifecycle", target: `'${repoPath}'` },
@@ -1190,7 +1314,7 @@ function deriveLinkedWorktreeWorkspace(repoPath, linkedWorktreeBranch) {
1190
1314
  return void 0;
1191
1315
  }
1192
1316
  const branch = currentBranch(repoPath);
1193
- 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)));
1194
1318
  }
1195
1319
  function resolveWorktreeWorkspace(repoPath, linkedWorktreeBranch) {
1196
1320
  return readPersistedWorkspace(repoPath) ?? deriveLinkedWorktreeWorkspace(repoPath, linkedWorktreeBranch);
@@ -1213,14 +1337,14 @@ function resolveWorkspace(repoPath, override) {
1213
1337
  }
1214
1338
  return deriveLinkedWorktreeWorkspace(repoPath);
1215
1339
  }
1216
- var import_node_child_process3, import_node_crypto3, import_node_fs5, import_node_path4, 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;
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;
1217
1341
  var init_workspace = __esm({
1218
1342
  "src/core/workspace.ts"() {
1219
1343
  "use strict";
1220
1344
  import_node_child_process3 = require("child_process");
1221
1345
  import_node_crypto3 = require("crypto");
1222
1346
  import_node_fs5 = __toESM(require("fs"));
1223
- import_node_path4 = __toESM(require("path"));
1347
+ import_node_path5 = __toESM(require("path"));
1224
1348
  init_atomic_file();
1225
1349
  init_file_lock();
1226
1350
  MAX_WORKSPACE_LENGTH = 32;
@@ -1644,7 +1768,7 @@ function listHostRoutes(tlsEnabled) {
1644
1768
  protocol: isTcp ? `tcp/${tcpProtocol}` : "http",
1645
1769
  appName: route.name,
1646
1770
  serviceName: route.name,
1647
- projectName: import_node_path5.default.basename(route.repoPath),
1771
+ projectName: import_node_path6.default.basename(route.repoPath),
1648
1772
  hosts: [route.host],
1649
1773
  // TCP routes are reached via an SNI-aware client on the shared protocol
1650
1774
  // port; surface a protocol-scheme URL rather than http(s)://.
@@ -1659,12 +1783,12 @@ function listHostRoutes(tlsEnabled) {
1659
1783
  };
1660
1784
  });
1661
1785
  }
1662
- 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;
1663
1787
  var init_host_routes = __esm({
1664
1788
  "src/core/host-routes.ts"() {
1665
1789
  "use strict";
1666
1790
  import_node_fs6 = __toESM(require("fs"));
1667
- import_node_path5 = __toESM(require("path"));
1791
+ import_node_path6 = __toESM(require("path"));
1668
1792
  import_node_util = require("util");
1669
1793
  import_yaml = __toESM(require("yaml"));
1670
1794
  init_atomic_file();
@@ -2512,10 +2636,10 @@ function appToNodeValue(app) {
2512
2636
  return value;
2513
2637
  }
2514
2638
  function resolveRepoPath(repoPath) {
2515
- return import_node_path6.default.resolve(repoPath ?? process.cwd());
2639
+ return import_node_path7.default.resolve(repoPath ?? process.cwd());
2516
2640
  }
2517
2641
  function getRepoConfigPath(repoPath) {
2518
- return import_node_path6.default.join(resolveRepoPath(repoPath), CONFIG_FILE_NAME);
2642
+ return import_node_path7.default.join(resolveRepoPath(repoPath), CONFIG_FILE_NAME);
2519
2643
  }
2520
2644
  function loadRepoConfig(repoPath) {
2521
2645
  const resolvedRepoPath = resolveRepoPath(repoPath);
@@ -2530,7 +2654,7 @@ function loadRepoConfig(repoPath) {
2530
2654
  const config = parseConfig(parsed ?? {}, configPath);
2531
2655
  const requiredVersion = config.devrouter?.version;
2532
2656
  if (requiredVersion && !hasWarnedVersionMismatch) {
2533
- const cliVersion = true ? "0.0.45" : "0.0.0-dev";
2657
+ const cliVersion = true ? "0.0.46" : "0.0.0-dev";
2534
2658
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
2535
2659
  hasWarnedVersionMismatch = true;
2536
2660
  process.stderr.write(
@@ -2563,7 +2687,7 @@ function initRepoConfig(repoPath, options = {}) {
2563
2687
  }
2564
2688
  } : {},
2565
2689
  project: {
2566
- name: import_node_path6.default.basename(resolvedRepoPath)
2690
+ name: import_node_path7.default.basename(resolvedRepoPath)
2567
2691
  },
2568
2692
  apps: []
2569
2693
  };
@@ -2792,7 +2916,7 @@ function namespaceHost(host, workspace) {
2792
2916
  return `${base}.${workspace}${suffix}`;
2793
2917
  }
2794
2918
  function applyWorkspace(config, workspace, repoPath) {
2795
- 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";
2796
2920
  const substitutionToken = workspace ?? defaultToken;
2797
2921
  const next = structuredClone(config);
2798
2922
  for (const app of next.apps) {
@@ -2874,12 +2998,12 @@ function resolveAppDependencies(config, app) {
2874
2998
  }
2875
2999
  return results;
2876
3000
  }
2877
- 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;
2878
3002
  var init_repo_config = __esm({
2879
3003
  "src/core/repo-config.ts"() {
2880
3004
  "use strict";
2881
3005
  import_node_fs7 = __toESM(require("fs"));
2882
- import_node_path6 = __toESM(require("path"));
3006
+ import_node_path7 = __toESM(require("path"));
2883
3007
  import_yaml2 = __toESM(require("yaml"));
2884
3008
  init_capabilities();
2885
3009
  init_host_routes();
@@ -3060,7 +3184,7 @@ function buildOnboardingPrompt(options = {}) {
3060
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.",
3061
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.`,
3062
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.",
3063
- "- 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.",
3064
3188
  "- A managed adapter plus `postCreateCommand` requires `waitFor` exactly `postCreateCommand` or `postStartCommand`; managed selective config preserves lifecycle fields and changes only `runServices`.",
3065
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.",
3066
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.",
@@ -3787,7 +3911,7 @@ function readPromptDirectory(promptDirPath) {
3787
3911
  continue;
3788
3912
  }
3789
3913
  const version = normalizeVersion(basename);
3790
- const promptPath = import_node_path7.default.join(promptDirPath, file.name);
3914
+ const promptPath = import_node_path8.default.join(promptDirPath, file.name);
3791
3915
  const prompt = import_node_fs8.default.readFileSync(promptPath, "utf-8").trim();
3792
3916
  releases.push({ version, prompt, promptPath });
3793
3917
  }
@@ -3809,17 +3933,17 @@ function listAvailableUpgradeTargets(currentVersion, releases) {
3809
3933
  }
3810
3934
  function resolvePromptDirectory(explicitPath) {
3811
3935
  if (explicitPath) {
3812
- return import_node_path7.default.resolve(explicitPath);
3936
+ return import_node_path8.default.resolve(explicitPath);
3813
3937
  }
3814
- const entryFile = process.argv[1] ? import_node_path7.default.resolve(process.argv[1]) : __filename;
3815
- const entryDir = import_node_path7.default.dirname(entryFile);
3816
- 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;
3817
3941
  const candidates = [
3818
- import_node_path7.default.resolve(resolvedEntryDir, "..", UPGRADE_PROMPTS_DIR),
3819
- import_node_path7.default.resolve(entryDir, "..", UPGRADE_PROMPTS_DIR),
3820
- import_node_path7.default.resolve(__dirname, "..", UPGRADE_PROMPTS_DIR),
3821
- import_node_path7.default.resolve(__dirname, "..", "..", UPGRADE_PROMPTS_DIR),
3822
- 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)
3823
3947
  ];
3824
3948
  const existing = candidates.find((candidate) => import_node_fs8.default.existsSync(candidate));
3825
3949
  return existing ?? candidates[0];
@@ -3837,12 +3961,12 @@ function loadUpgradeCatalog(options = {}) {
3837
3961
  releases
3838
3962
  };
3839
3963
  }
3840
- 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;
3841
3965
  var init_upgrade = __esm({
3842
3966
  "src/core/upgrade.ts"() {
3843
3967
  "use strict";
3844
3968
  import_node_fs8 = __toESM(require("fs"));
3845
- import_node_path7 = __toESM(require("path"));
3969
+ import_node_path8 = __toESM(require("path"));
3846
3970
  init_repo_config();
3847
3971
  SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)$/;
3848
3972
  UPGRADE_PROMPTS_DIR = "upgrade-prompts";
@@ -4054,7 +4178,7 @@ function parseDevcontainerConfig(contents, sourcePath) {
4054
4178
  return parsed;
4055
4179
  }
4056
4180
  function readDevcontainerConfig(repoPath) {
4057
- const sourcePath = import_node_path8.default.join(repoPath, ".devcontainer", "devcontainer.json");
4181
+ const sourcePath = import_node_path9.default.join(repoPath, ".devcontainer", "devcontainer.json");
4058
4182
  if (!import_node_fs9.default.existsSync(sourcePath) || !import_node_fs9.default.lstatSync(sourcePath).isFile()) {
4059
4183
  throw new Error(`Managed Dev Container source config does not exist: ${sourcePath}`);
4060
4184
  }
@@ -4074,12 +4198,12 @@ function assertManagedDevcontainerLifecycle(repoPath) {
4074
4198
  `Managed Dev Container source '${sourcePath}' defines postCreateCommand, but waitFor ${actual}. Set waitFor to 'postCreateCommand' or 'postStartCommand' before retrying devrouter ensure.`
4075
4199
  );
4076
4200
  }
4077
- var import_node_fs9, import_node_path8, import_jsonc_parser;
4201
+ var import_node_fs9, import_node_path9, import_jsonc_parser;
4078
4202
  var init_devcontainer_config = __esm({
4079
4203
  "src/core/devcontainer-config.ts"() {
4080
4204
  "use strict";
4081
4205
  import_node_fs9 = __toESM(require("fs"));
4082
- import_node_path8 = __toESM(require("path"));
4206
+ import_node_path9 = __toESM(require("path"));
4083
4207
  import_jsonc_parser = require("jsonc-parser");
4084
4208
  }
4085
4209
  });
@@ -4089,7 +4213,7 @@ function renderDeliveryScript(targetPath) {
4089
4213
  if (!/^\/tmp\/devrouter\/bin\/[a-zA-Z0-9._-]+$/.test(targetPath)) {
4090
4214
  throw new Error(`Unsafe managed runtime delivery path: ${targetPath}`);
4091
4215
  }
4092
- const temporaryPrefix = import_node_path9.default.posix.basename(targetPath);
4216
+ const temporaryPrefix = import_node_path10.default.posix.basename(targetPath);
4093
4217
  return `set -eu
4094
4218
  umask 077
4095
4219
  runtime_root=/tmp/devrouter
@@ -4135,8 +4259,8 @@ function adapterFingerprint(adapter) {
4135
4259
  }
4136
4260
  function resolveProcessHelperPath() {
4137
4261
  const candidates = [
4138
- import_node_path9.default.resolve(__dirname, "..", "bin", "devrouter-process"),
4139
- 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")
4140
4264
  ];
4141
4265
  const helperPath = candidates.find((candidate) => import_node_fs10.default.existsSync(candidate));
4142
4266
  if (!helperPath) {
@@ -4145,11 +4269,11 @@ function resolveProcessHelperPath() {
4145
4269
  return helperPath;
4146
4270
  }
4147
4271
  function resolveManagedPostStartPlan(repoPath) {
4148
- const adapterPath = import_node_path9.default.join(repoPath, MANAGED_ADAPTER_PATH);
4272
+ const adapterPath = import_node_path10.default.join(repoPath, MANAGED_ADAPTER_PATH);
4149
4273
  const adapterBytes = readRegularFileBytes(adapterPath);
4150
4274
  const adapterText = adapterBytes?.toString("utf-8");
4151
- const dockerfilePath = import_node_path9.default.join(repoPath, ".devcontainer", "Dockerfile");
4152
- 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");
4153
4277
  const dockerfile = readRegularFile(dockerfilePath) ?? "";
4154
4278
  const devcontainer = readRegularFile(devcontainerPath) ?? "";
4155
4279
  const devrouterPattern = [adapterText, dockerfile, devcontainer].filter((value) => value !== void 0).some(
@@ -4278,14 +4402,14 @@ function runManagedProcessAction(options) {
4278
4402
  }
4279
4403
  return "drifted";
4280
4404
  }
4281
- var import_node_child_process5, import_node_crypto4, 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;
4282
4406
  var init_managed_post_start = __esm({
4283
4407
  "src/core/managed-post-start.ts"() {
4284
4408
  "use strict";
4285
4409
  import_node_child_process5 = require("child_process");
4286
4410
  import_node_crypto4 = require("crypto");
4287
4411
  import_node_fs10 = __toESM(require("fs"));
4288
- import_node_path9 = __toESM(require("path"));
4412
+ import_node_path10 = __toESM(require("path"));
4289
4413
  init_devcontainer_config();
4290
4414
  MANAGED_MARKER = "devrouter:managed devcontainer";
4291
4415
  MANAGED_ADAPTER_PATH = ".devcontainer/post-start.sh";
@@ -4418,11 +4542,11 @@ function routedProxyApps(config) {
4418
4542
  );
4419
4543
  }
4420
4544
  function buildDevcontainerChecks(repoPath, config, workspace) {
4421
- const devcontainerDir = import_node_path10.default.join(repoPath, ".devcontainer");
4545
+ const devcontainerDir = import_node_path11.default.join(repoPath, ".devcontainer");
4422
4546
  if (!import_node_fs11.default.existsSync(devcontainerDir)) {
4423
4547
  return [];
4424
4548
  }
4425
- 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);
4426
4550
  const checks = [];
4427
4551
  if (compose.parseError) {
4428
4552
  checks.push({
@@ -4449,7 +4573,7 @@ function buildDevcontainerChecks(repoPath, config, workspace) {
4449
4573
  details: compose.publishedPorts.length > 0 ? compose.publishedPorts.join(", ") : void 0,
4450
4574
  suggestion: compose.parseError || compose.publishedPorts.length > 0 ? "Remove published ports and route services through devnet aliases with devrouter proxy apps." : void 0
4451
4575
  });
4452
- const dockerfilePath = import_node_path10.default.join(devcontainerDir, "Dockerfile");
4576
+ const dockerfilePath = import_node_path11.default.join(devcontainerDir, "Dockerfile");
4453
4577
  const dockerfileExists = import_node_fs11.default.existsSync(dockerfilePath);
4454
4578
  const dockerfile = dockerfileExists ? import_node_fs11.default.readFileSync(dockerfilePath, "utf-8") : "";
4455
4579
  const devrouterArtifacts = ["@devrouter/cli", "devrouter-process"].filter(
@@ -4506,12 +4630,12 @@ function buildDevcontainerChecks(repoPath, config, workspace) {
4506
4630
  });
4507
4631
  return checks;
4508
4632
  }
4509
- var import_node_fs11, import_node_path10, import_yaml3;
4633
+ var import_node_fs11, import_node_path11, import_yaml3;
4510
4634
  var init_devcontainer_diagnostics = __esm({
4511
4635
  "src/core/devcontainer-diagnostics.ts"() {
4512
4636
  "use strict";
4513
4637
  import_node_fs11 = __toESM(require("fs"));
4514
- import_node_path10 = __toESM(require("path"));
4638
+ import_node_path11 = __toESM(require("path"));
4515
4639
  import_yaml3 = __toESM(require("yaml"));
4516
4640
  init_managed_post_start();
4517
4641
  }
@@ -4519,18 +4643,18 @@ var init_devcontainer_diagnostics = __esm({
4519
4643
 
4520
4644
  // src/core/paths.ts
4521
4645
  function assertPathWithinRepo(filePath, repoRoot, label) {
4522
- const resolvedRoot = import_node_path11.default.resolve(repoRoot);
4523
- const resolved = import_node_path11.default.resolve(repoRoot, filePath);
4524
- 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)) {
4525
4649
  throw new Error(`${label} path '${filePath}' escapes the repository root.`);
4526
4650
  }
4527
4651
  return resolved;
4528
4652
  }
4529
- var import_node_path11;
4653
+ var import_node_path12;
4530
4654
  var init_paths = __esm({
4531
4655
  "src/core/paths.ts"() {
4532
4656
  "use strict";
4533
- import_node_path11 = __toESM(require("path"));
4657
+ import_node_path12 = __toESM(require("path"));
4534
4658
  }
4535
4659
  });
4536
4660
 
@@ -4788,7 +4912,7 @@ function stateKey(repoPath, workspace) {
4788
4912
  return (0, import_node_crypto5.createHash)("sha256").update(`${repoPath}\0${workspace ?? ""}`, "utf-8").digest("hex");
4789
4913
  }
4790
4914
  function managedRuntimeStatePath(repoPath, workspace) {
4791
- 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`);
4792
4916
  }
4793
4917
  function isStringArray(value) {
4794
4918
  return Array.isArray(value) && value.every((item) => typeof item === "string") && new Set(value).size === value.length;
@@ -4876,13 +5000,13 @@ function markManagedRuntimeDegraded(state, transitionPhase) {
4876
5000
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
4877
5001
  });
4878
5002
  }
4879
- var import_node_crypto5, import_node_fs13, import_node_path12;
5003
+ var import_node_crypto5, import_node_fs13, import_node_path13;
4880
5004
  var init_managed_runtime_state = __esm({
4881
5005
  "src/core/managed-runtime-state.ts"() {
4882
5006
  "use strict";
4883
5007
  import_node_crypto5 = require("crypto");
4884
5008
  import_node_fs13 = __toESM(require("fs"));
4885
- import_node_path12 = __toESM(require("path"));
5009
+ import_node_path13 = __toESM(require("path"));
4886
5010
  init_atomic_file();
4887
5011
  init_router();
4888
5012
  }
@@ -4933,7 +5057,7 @@ function resolveComposeFiles(source, repoPath, linked) {
4933
5057
  "managedRuntime requires .devcontainer/devcontainer.json to define dockerComposeFile."
4934
5058
  );
4935
5059
  }
4936
- const directory = import_node_path13.default.join(repoPath, ".devcontainer");
5060
+ const directory = import_node_path14.default.join(repoPath, ".devcontainer");
4937
5061
  const files = references.map((reference) => {
4938
5062
  const resolved2 = assertPathWithinRepo(
4939
5063
  resolveComposeReference(reference, linked),
@@ -5078,7 +5202,7 @@ function inspectManagedDevcontainerConfig(options) {
5078
5202
  const contents = `${MANAGED_DEVCONTAINER_MARKER}
5079
5203
  ${JSON.stringify(effective, null, 2)}
5080
5204
  `;
5081
- 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);
5082
5206
  assertIgnoredGeneratedPath(options.repoPath, generatedPath);
5083
5207
  assertGeneratedPathOwnership(generatedPath);
5084
5208
  return {
@@ -5198,14 +5322,14 @@ function stopExactManagedService(containerId, service) {
5198
5322
  throw new Error(`Could not stop exact managed service '${service}' (${containerId}).`);
5199
5323
  }
5200
5324
  }
5201
- var import_node_child_process6, import_node_crypto6, 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;
5202
5326
  var init_devcontainer_profile = __esm({
5203
5327
  "src/core/devcontainer-profile.ts"() {
5204
5328
  "use strict";
5205
5329
  import_node_child_process6 = require("child_process");
5206
5330
  import_node_crypto6 = require("crypto");
5207
5331
  import_node_fs14 = __toESM(require("fs"));
5208
- import_node_path13 = __toESM(require("path"));
5332
+ import_node_path14 = __toESM(require("path"));
5209
5333
  import_yaml4 = __toESM(require("yaml"));
5210
5334
  init_atomic_file();
5211
5335
  init_devcontainer_config();
@@ -5219,9 +5343,17 @@ var init_devcontainer_profile = __esm({
5219
5343
  function inspectWorkspaceContainers(options) {
5220
5344
  let ids = options?.ids;
5221
5345
  if (!ids) {
5222
- const listed = (0, import_node_child_process7.spawnSync)("docker", ["ps", "-a", "--format", "{{.ID}}"], {
5223
- encoding: "utf-8"
5224
- });
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
+ );
5225
5357
  if (listed.status !== 0) {
5226
5358
  throw new Error(
5227
5359
  `docker ps failed: ${(listed.stderr || listed.stdout || listed.error?.message || "unknown error").trim()}`
@@ -5246,7 +5378,7 @@ function inspectWorkspaceContainers(options) {
5246
5378
  function workspaceAppContainers(containers, repoPath) {
5247
5379
  return containers.filter((container) => {
5248
5380
  const workingDir = container.labels["com.docker.compose.project.working_dir"];
5249
- 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(
5250
5382
  (mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath)
5251
5383
  );
5252
5384
  });
@@ -5254,7 +5386,7 @@ function workspaceAppContainers(containers, repoPath) {
5254
5386
  function hasExactComposeIdentity(container, options) {
5255
5387
  if (options.composeProject !== void 0 && container.labels["com.docker.compose.project"] !== options.composeProject || container.labels["com.docker.compose.service"] !== options.service || !sameWorkspacePath(
5256
5388
  container.labels["com.docker.compose.project.working_dir"] ?? "",
5257
- import_node_path14.default.join(options.repoPath, ".devcontainer")
5389
+ import_node_path15.default.join(options.repoPath, ".devcontainer")
5258
5390
  )) {
5259
5391
  return false;
5260
5392
  }
@@ -5291,12 +5423,12 @@ function resolveRunningWorkspaceContainer(repoPath) {
5291
5423
  }
5292
5424
  return { id: container.id, workspacePath: repoMount.Destination };
5293
5425
  }
5294
- 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;
5295
5427
  var init_devpod_environment = __esm({
5296
5428
  "src/core/devpod-environment.ts"() {
5297
5429
  "use strict";
5298
5430
  import_node_child_process7 = require("child_process");
5299
- import_node_path14 = __toESM(require("path"));
5431
+ import_node_path15 = __toESM(require("path"));
5300
5432
  init_workspace();
5301
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}}}';
5302
5434
  SIZE_INSPECT_TEMPLATE = SAFE_INSPECT_TEMPLATE.replace(
@@ -5941,7 +6073,7 @@ function tlsSetupCommand(repoPath) {
5941
6073
  }
5942
6074
  function getMkcertRootCAPath(options = {}) {
5943
6075
  ensureMkcert();
5944
- 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");
5945
6077
  if (!import_node_fs16.default.existsSync(rootCAPath)) {
5946
6078
  throw new Error(
5947
6079
  `mkcert root CA was not found at '${rootCAPath}'. Run: ${tlsSetupCommand(options.repoPath)}`
@@ -6175,20 +6307,20 @@ Run: ${tlsSetupCommand(options.repoPath)}`
6175
6307
  );
6176
6308
  }
6177
6309
  }
6178
- var import_node_child_process8, import_node_crypto7, 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;
6179
6311
  var init_tls = __esm({
6180
6312
  "src/core/tls.ts"() {
6181
6313
  "use strict";
6182
6314
  import_node_child_process8 = require("child_process");
6183
6315
  import_node_crypto7 = require("crypto");
6184
6316
  import_node_fs16 = __toESM(require("fs"));
6185
- import_node_path15 = __toESM(require("path"));
6317
+ import_node_path16 = __toESM(require("path"));
6186
6318
  init_docker();
6187
6319
  init_file_lock();
6188
6320
  init_host_routes();
6189
6321
  init_router();
6190
6322
  DEFAULT_TLS_CERT_HOSTS = ["localhost", "*.localhost"];
6191
- 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");
6192
6324
  TLS_CERTIFICATE_LOCK_WAIT_MS = 6e4;
6193
6325
  TLS_CERTIFICATE_WRITE_ATTEMPTS = 2;
6194
6326
  }
@@ -6547,20 +6679,20 @@ function resetWorkspaceRuntimeCaches() {
6547
6679
  cachedRuntime = void 0;
6548
6680
  cachedSnapshots = void 0;
6549
6681
  }
6550
- 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;
6551
6683
  var init_workspace_runtime = __esm({
6552
6684
  "src/core/workspace-runtime.ts"() {
6553
6685
  "use strict";
6554
6686
  import_node_child_process11 = require("child_process");
6555
6687
  import_node_fs17 = __toESM(require("fs"));
6556
- import_node_path16 = __toESM(require("path"));
6688
+ import_node_path17 = __toESM(require("path"));
6557
6689
  init_atomic_file();
6558
6690
  init_devpod_registry();
6559
6691
  init_devsy_workspaces();
6560
6692
  init_router();
6561
6693
  init_workspace();
6562
6694
  SUPPORTED_RUNTIMES2 = ["devpod", "devsy"];
6563
- 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");
6564
6696
  INACTIVITY_TIMEOUT_PATTERN = /^(?:\d+(?:ms|s|m|h))+$/;
6565
6697
  UnsupportedWorkspaceRuntimeError = class extends Error {
6566
6698
  };
@@ -6638,7 +6770,7 @@ function parseMinimumNodeMajor(value) {
6638
6770
  return Number(match[1]);
6639
6771
  }
6640
6772
  function readPackageJson(repoPath) {
6641
- const packagePath = import_node_path17.default.join(repoPath, "package.json");
6773
+ const packagePath = import_node_path18.default.join(repoPath, "package.json");
6642
6774
  if (!import_node_fs18.default.existsSync(packagePath)) {
6643
6775
  return void 0;
6644
6776
  }
@@ -6783,13 +6915,13 @@ function buildGlobalToolChecks(repoPath) {
6783
6915
  checks.push(nodeToolchainCheck(repoPath));
6784
6916
  return checks;
6785
6917
  }
6786
- var import_node_child_process12, import_node_fs18, import_node_path17;
6918
+ var import_node_child_process12, import_node_fs18, import_node_path18;
6787
6919
  var init_tool_diagnostics = __esm({
6788
6920
  "src/core/tool-diagnostics.ts"() {
6789
6921
  "use strict";
6790
6922
  import_node_child_process12 = require("child_process");
6791
6923
  import_node_fs18 = __toESM(require("fs"));
6792
- import_node_path17 = __toESM(require("path"));
6924
+ import_node_path18 = __toESM(require("path"));
6793
6925
  init_workspace_runtime();
6794
6926
  }
6795
6927
  });
@@ -6919,13 +7051,65 @@ function withMutationLock(activity, target, operation) {
6919
7051
  import_node_fs19.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
6920
7052
  return withFileLockSync(
6921
7053
  DEVSY_MUTATION_LOCK_FILE,
6922
- { 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
+ },
6923
7075
  operation
6924
7076
  );
6925
7077
  }
6926
7078
  function commandFailure2(result) {
6927
7079
  return [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
6928
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
+ }
6929
7113
  function runDevsyAction(action2, devsyId, force = false) {
6930
7114
  const args = action2 === "delete" ? ["delete", devsyId, ...force ? ["--force"] : [], "--ignore-not-found"] : ["stop", devsyId];
6931
7115
  const result = (0, import_node_child_process14.spawnSync)("devsy", ["workspace", ...args], { encoding: "utf-8" });
@@ -6990,7 +7174,7 @@ function assertDevsyTarget(devsyId, repoPath) {
6990
7174
  }
6991
7175
  function startDevsyWorkspace(options) {
6992
7176
  const activity = options.recreate ? "Devsy recreate" : "Devsy start";
6993
- return withMutationLock(activity, options.repoPath, () => {
7177
+ return withMutationLockAsync(activity, options.repoPath, async () => {
6994
7178
  let devsyId = assertDevsyTarget(options.devsyId, options.repoPath);
6995
7179
  if (devsyId && options.recreate) {
6996
7180
  const attached = listDevsyWorkspaces();
@@ -7030,12 +7214,13 @@ function startDevsyWorkspace(options) {
7030
7214
  delete env.DEVROUTER_GIT_COMMON_DIR;
7031
7215
  delete env.DEVCONTAINER_COMPOSE_OVERLAY;
7032
7216
  }
7033
- const result = (0, import_node_child_process14.spawnSync)("devsy", args, {
7034
- stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
7035
- env
7036
- });
7217
+ const result = await runDevsyUp(args, env, options.quiet ?? false);
7037
7218
  if (result.status !== 0) {
7038
- 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
+ }
7039
7224
  if (failedStartMayHaveAttached(devsyId, options.repoPath)) {
7040
7225
  throw new DevsyStartPostconditionError(message);
7041
7226
  }
@@ -7061,20 +7246,22 @@ function startDevsyWorkspace(options) {
7061
7246
  }
7062
7247
  });
7063
7248
  }
7064
- 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;
7065
7250
  var init_devsy_mutation = __esm({
7066
7251
  "src/core/devsy-mutation.ts"() {
7067
7252
  "use strict";
7068
7253
  import_node_child_process14 = require("child_process");
7069
7254
  import_node_fs19 = __toESM(require("fs"));
7070
- import_node_path18 = __toESM(require("path"));
7255
+ import_node_path19 = __toESM(require("path"));
7071
7256
  init_devsy_workspaces();
7072
7257
  init_file_lock();
7073
7258
  init_router();
7074
- DEVSY_MUTATION_LOCK_FILE = import_node_path18.default.join(DEVROUTER_HOME, "devsy-mutation.lock");
7075
- 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;
7076
7261
  DevsyStartPostconditionError = class extends Error {
7077
7262
  };
7263
+ AGENT_ACQUISITION_RE = /inject agent.*agent binary not found/i;
7264
+ DEVSY_STDERR_TAIL_BYTES = 8192;
7078
7265
  }
7079
7266
  });
7080
7267
 
@@ -7083,7 +7270,13 @@ function withMutationLock2(activity, target, operation) {
7083
7270
  import_node_fs20.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
7084
7271
  return withFileLockSync(
7085
7272
  DEVPOD_MUTATION_LOCK_FILE,
7086
- { 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
+ },
7087
7280
  operation
7088
7281
  );
7089
7282
  }
@@ -7158,10 +7351,10 @@ function deleteOwnedDevpodWorkspace(devpodId, worktreePath) {
7158
7351
  resetWorkspaceRuntimeCaches();
7159
7352
  return result;
7160
7353
  }
7161
- function startDevpodWorkspace(options) {
7354
+ async function startDevpodWorkspace(options) {
7162
7355
  if (resolveWorkspaceRuntimeOrDefault(options.repoPath) === "devsy") {
7163
7356
  try {
7164
- const result = startDevsyWorkspace({
7357
+ const result = await startDevsyWorkspace({
7165
7358
  repoPath: options.repoPath,
7166
7359
  devsyId: options.devpodId,
7167
7360
  devcontainerPath: options.devcontainerPath,
@@ -7248,20 +7441,20 @@ function startDevpodWorkspace(options) {
7248
7441
  }
7249
7442
  });
7250
7443
  }
7251
- 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;
7252
7445
  var init_devpod_mutation = __esm({
7253
7446
  "src/core/devpod-mutation.ts"() {
7254
7447
  "use strict";
7255
7448
  import_node_child_process15 = require("child_process");
7256
7449
  import_node_fs20 = __toESM(require("fs"));
7257
- import_node_path19 = __toESM(require("path"));
7450
+ import_node_path20 = __toESM(require("path"));
7258
7451
  init_devpod_workspaces();
7259
7452
  init_devsy_mutation();
7260
7453
  init_file_lock();
7261
7454
  init_router();
7262
7455
  init_workspace_runtime();
7263
- DEVPOD_MUTATION_LOCK_FILE = import_node_path19.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
7264
- 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;
7265
7458
  DevpodStartPostconditionError = class extends Error {
7266
7459
  };
7267
7460
  }
@@ -7282,7 +7475,7 @@ function resolveGitCommonDir(repoPath) {
7282
7475
  if (result.status !== 0 || !output2) {
7283
7476
  throw commandError("Could not resolve the Git common directory", repoPath, result.stderr);
7284
7477
  }
7285
- 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));
7286
7479
  }
7287
7480
  function resolveGitTopLevel(repoPath) {
7288
7481
  const result = (0, import_node_child_process16.spawnSync)("git", ["-C", repoPath, "rev-parse", "--show-toplevel"], {
@@ -7333,7 +7526,7 @@ function listGitWorktrees(repoPath) {
7333
7526
  return worktrees;
7334
7527
  }
7335
7528
  function ownershipDirectory(repoPath) {
7336
- return import_node_path20.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
7529
+ return import_node_path21.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
7337
7530
  }
7338
7531
  function validateWorkspace(value, label) {
7339
7532
  if (typeof value !== "string" || wsFromBranch(value) !== value) {
@@ -7361,7 +7554,7 @@ function validateRecord(value, expectedWorkspace) {
7361
7554
  `workspace ownership file '${expectedWorkspace}' contains identity '${workspace}'`
7362
7555
  );
7363
7556
  }
7364
- 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)) {
7365
7558
  throw new Error("invalid workspace ownership worktreePath");
7366
7559
  }
7367
7560
  if (candidate.branch !== null && typeof candidate.branch !== "string") {
@@ -7405,14 +7598,14 @@ function listWorkspaceOwnershipInDirectory(directory) {
7405
7598
  return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
7406
7599
  const workspace = entry.name.slice(0, -".json".length);
7407
7600
  validateWorkspace(workspace, "filename");
7408
- return readRecordFile(import_node_path20.default.join(directory, entry.name), workspace);
7601
+ return readRecordFile(import_node_path21.default.join(directory, entry.name), workspace);
7409
7602
  });
7410
7603
  }
7411
7604
  function writeWorkspaceOwnershipInDirectory(directory, input2) {
7412
7605
  const workspace = validateWorkspace(input2.workspace, "workspace");
7413
7606
  const devpodId = validateWorkspace(input2.devpodId, "devpodId");
7414
7607
  const worktreePath = comparableWorkspacePath(input2.worktreePath);
7415
- const filePath = import_node_path20.default.join(directory, `${workspace}.json`);
7608
+ const filePath = import_node_path21.default.join(directory, `${workspace}.json`);
7416
7609
  const now = (/* @__PURE__ */ new Date()).toISOString();
7417
7610
  const records = listWorkspaceOwnershipInDirectory(directory);
7418
7611
  const existing = records.find((record2) => record2.workspace === workspace);
@@ -7448,7 +7641,7 @@ function writeWorkspaceOwnershipInDirectory(directory, input2) {
7448
7641
  return record;
7449
7642
  }
7450
7643
  function removeWorkspaceOwnershipInDirectory(directory, workspace) {
7451
- 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`);
7452
7645
  try {
7453
7646
  import_node_fs21.default.rmSync(filePath);
7454
7647
  return true;
@@ -7461,7 +7654,7 @@ function sameOwnershipRecord(left, right) {
7461
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;
7462
7655
  }
7463
7656
  function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
7464
- const filePath = import_node_path20.default.join(
7657
+ const filePath = import_node_path21.default.join(
7465
7658
  directory,
7466
7659
  `${validateWorkspace(expected.workspace, "workspace")}.json`
7467
7660
  );
@@ -7480,7 +7673,7 @@ function withWorkspaceOwnershipTransaction(repoPath, operation, options = {}) {
7480
7673
  const directory = ownershipDirectory(repoPath);
7481
7674
  import_node_fs21.default.mkdirSync(directory, { recursive: true });
7482
7675
  return withFileLockSync(
7483
- import_node_path20.default.join(directory, ".lock"),
7676
+ import_node_path21.default.join(directory, ".lock"),
7484
7677
  {
7485
7678
  activity: "workspace ownership transaction",
7486
7679
  target: `'${repoPath}'`,
@@ -7700,20 +7893,20 @@ function listMissingWorkspaceOwnership(repoPath) {
7700
7893
  (record) => inspectWorkspaceOwnership(record, worktrees, void 0).ownerStatus === "missing"
7701
7894
  );
7702
7895
  }
7703
- 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;
7704
7897
  var init_workspace_ownership = __esm({
7705
7898
  "src/core/workspace-ownership.ts"() {
7706
7899
  "use strict";
7707
7900
  import_node_child_process16 = require("child_process");
7708
7901
  import_node_fs21 = __toESM(require("fs"));
7709
- import_node_path20 = __toESM(require("path"));
7902
+ import_node_path21 = __toESM(require("path"));
7710
7903
  init_atomic_file();
7711
7904
  init_devpod_workspaces();
7712
7905
  init_file_lock();
7713
7906
  init_workspace();
7714
7907
  READ_ONLY_GIT_ENV = { ...process.env, GIT_OPTIONAL_LOCKS: "0" };
7715
7908
  OWNERSHIP_VERSION = 1;
7716
- OWNERSHIP_DIR = import_node_path20.default.join("devrouter", "workspaces");
7909
+ OWNERSHIP_DIR = import_node_path21.default.join("devrouter", "workspaces");
7717
7910
  }
7718
7911
  });
7719
7912
 
@@ -7727,10 +7920,10 @@ function inRepositoryWorkspaceScope(repoPath, worktreePath, livePaths) {
7727
7920
  if (livePaths.some((candidate) => sameWorkspacePath(candidate, worktreePath))) return true;
7728
7921
  const comparableRepo = comparableWorkspacePath(repoPath);
7729
7922
  const comparableWorktree = comparableWorkspacePath(worktreePath);
7730
- 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;
7731
7924
  if (comparableWorktree.startsWith(localRoot)) return true;
7732
- const legacyPrefix = `${import_node_path21.default.basename(comparableRepo)}-`;
7733
- 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);
7734
7927
  }
7735
7928
  function previewActions(devpodStatus, routeCount, includeRecord) {
7736
7929
  const actions = [
@@ -7982,11 +8175,11 @@ function applyWorkspaceGc(plan) {
7982
8175
  candidates
7983
8176
  };
7984
8177
  }
7985
- var import_node_path21;
8178
+ var import_node_path22;
7986
8179
  var init_workspace_gc = __esm({
7987
8180
  "src/core/workspace-gc.ts"() {
7988
8181
  "use strict";
7989
- import_node_path21 = __toESM(require("path"));
8182
+ import_node_path22 = __toESM(require("path"));
7990
8183
  init_devpod_mutation();
7991
8184
  init_devpod_workspaces();
7992
8185
  init_host_routes();
@@ -8339,7 +8532,7 @@ async function buildDoctorReport(options = {}) {
8339
8532
  const config = runtimeConfig.config;
8340
8533
  loadedConfig = config;
8341
8534
  loadedWorkspace = runtimeConfig.workspace;
8342
- const cliVersion = true ? "0.0.45" : "0.0.0-dev";
8535
+ const cliVersion = true ? "0.0.46" : "0.0.0-dev";
8343
8536
  const configVersion = config.devrouter?.version;
8344
8537
  if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
8345
8538
  addCheck(checks, {
@@ -8369,7 +8562,7 @@ async function buildDoctorReport(options = {}) {
8369
8562
  (app) => app.docker.composeFiles.map((filePath) => ({
8370
8563
  app: app.name,
8371
8564
  filePath,
8372
- absolutePath: import_node_path22.default.resolve(repo.path, filePath)
8565
+ absolutePath: import_node_path23.default.resolve(repo.path, filePath)
8373
8566
  }))
8374
8567
  ).filter((entry) => !import_node_fs22.default.existsSync(entry.absolutePath));
8375
8568
  addCheck(checks, {
@@ -8414,7 +8607,7 @@ async function buildDoctorReport(options = {}) {
8414
8607
  const missingHostCwds = config.apps.filter((app) => app.runtime === "host").map((app) => ({
8415
8608
  app: app.name,
8416
8609
  cwd: app.hostRun.cwd,
8417
- absolutePath: import_node_path22.default.resolve(repo.path, app.hostRun.cwd)
8610
+ absolutePath: import_node_path23.default.resolve(repo.path, app.hostRun.cwd)
8418
8611
  })).filter((entry) => !import_node_fs22.default.existsSync(entry.absolutePath));
8419
8612
  addCheck(checks, {
8420
8613
  id: "repo.host-cwd",
@@ -8559,12 +8752,12 @@ async function buildDoctorReport(options = {}) {
8559
8752
  nextSteps
8560
8753
  };
8561
8754
  }
8562
- var import_node_fs22, import_node_path22, import_yaml5, POSTGRES_DEFAULTS;
8755
+ var import_node_fs22, import_node_path23, import_yaml5, POSTGRES_DEFAULTS;
8563
8756
  var init_doctor = __esm({
8564
8757
  "src/core/doctor.ts"() {
8565
8758
  "use strict";
8566
8759
  import_node_fs22 = __toESM(require("fs"));
8567
- import_node_path22 = __toESM(require("path"));
8760
+ import_node_path23 = __toESM(require("path"));
8568
8761
  import_yaml5 = __toESM(require("yaml"));
8569
8762
  init_docker();
8570
8763
  init_host_routes();
@@ -9059,11 +9252,11 @@ var init_route_publication = __esm({
9059
9252
  // src/core/workspace-ensure.ts
9060
9253
  function assertOverlay(container, repoPath) {
9061
9254
  const workingDir = container.labels["com.docker.compose.project.working_dir"];
9062
- if (!workingDir || !sameWorkspacePath(workingDir, import_node_path23.default.join(repoPath, ".devcontainer"))) {
9255
+ if (!workingDir || !sameWorkspacePath(workingDir, import_node_path24.default.join(repoPath, ".devcontainer"))) {
9063
9256
  throw new Error(`Container '${container.id}' does not belong to the exact worktree.`);
9064
9257
  }
9065
9258
  const configFiles = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").filter(Boolean);
9066
- const expectedOverlay = import_node_path23.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
9259
+ const expectedOverlay = import_node_path24.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
9067
9260
  if (!configFiles.some((configFile) => sameWorkspacePath(configFile, expectedOverlay))) {
9068
9261
  throw new Error(`Container '${container.id}' was not started with ${DEVCONTAINER_OVERLAY}.`);
9069
9262
  }
@@ -9239,6 +9432,20 @@ function isWarmWorkspaceActive(repoPath) {
9239
9432
  return true;
9240
9433
  }
9241
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
+ }
9242
9449
  function captureFirstTransitionBaseline(options) {
9243
9450
  const containers = inspectWorkspaceContainers();
9244
9451
  const runningPrimary = workspaceAppContainers(containers, options.repoPath).filter(
@@ -9453,7 +9660,7 @@ function resolvePrimaryTarget(repoPath) {
9453
9660
  }
9454
9661
  function isPrimaryCheckout(repoPath) {
9455
9662
  try {
9456
- 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();
9457
9664
  } catch {
9458
9665
  return false;
9459
9666
  }
@@ -9503,7 +9710,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
9503
9710
  const target = linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
9504
9711
  let devpodId = target.devpodId;
9505
9712
  if (target.kind === "linked") {
9506
- const overlayPath = import_node_path23.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
9713
+ const overlayPath = import_node_path24.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
9507
9714
  if (!import_node_fs23.default.existsSync(overlayPath)) {
9508
9715
  throw new Error(`Missing required DevPod compose overlay: ${overlayPath}`);
9509
9716
  }
@@ -9521,6 +9728,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
9521
9728
  const desiredProcesses = managedRuntime ? desiredManagedProcesses(managedRuntime, runtime.resolvedProfile) : [];
9522
9729
  if (managedRuntime) {
9523
9730
  previousManagedState = readManagedRuntimeState(repoPath, target.workspace);
9731
+ if (previousManagedState && !hasExactManagedComposeProject(repoPath, previousManagedState)) {
9732
+ previousManagedState = void 0;
9733
+ }
9524
9734
  if (previousManagedState?.status === "degraded") {
9525
9735
  throw new Error(
9526
9736
  "Managed runtime state is degraded; refusing a new profile transition until drift is repaired."
@@ -9571,7 +9781,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
9571
9781
  }
9572
9782
  const apps = proxyAppsFromConfig(runtime.config);
9573
9783
  const parsedUpstreams = apps.map((app) => parseUpstream(app.upstream));
9574
- 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";
9575
9785
  for (const [index, app] of apps.entries()) {
9576
9786
  if (!parsedUpstreams[index].host.startsWith(`${aliasPrefix}-`)) {
9577
9787
  const owner = target.kind === "linked" ? "workspace" : "checkout";
@@ -9586,10 +9796,10 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
9586
9796
  }
9587
9797
  const upstreamHosts = parsedUpstreams.map((upstream) => upstream.host);
9588
9798
  const currentTarget = () => target.kind === "linked" ? target : { ...target, devpodId };
9589
- const startAndProveAttachment = (recreate = false) => {
9799
+ const startAndProveAttachment = async (recreate = false) => {
9590
9800
  const requestedTarget = currentTarget();
9591
9801
  try {
9592
- devpodId = startDevpodWorkspace({
9802
+ devpodId = await startDevpodWorkspace({
9593
9803
  repoPath,
9594
9804
  devpodId: requestedTarget.devpodId,
9595
9805
  devcontainerPath: managedPlan?.generatedRelativePath,
@@ -9610,13 +9820,13 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
9610
9820
  };
9611
9821
  const preflight = (timeoutMs) => waitForContainerPreflight(repoPath, currentTarget(), upstreamHosts, timeoutMs);
9612
9822
  const recreateAndPreflight = async () => {
9613
- startAndProveAttachment(true);
9823
+ await startAndProveAttachment(true);
9614
9824
  return preflight(options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS);
9615
9825
  };
9616
9826
  let recreated = false;
9617
9827
  let container;
9618
9828
  try {
9619
- startAndProveAttachment();
9829
+ await startAndProveAttachment();
9620
9830
  } catch (error) {
9621
9831
  if (managedPlan || !target.hadExactDevpod) {
9622
9832
  throw error;
@@ -10003,13 +10213,13 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
10003
10213
  }
10004
10214
  });
10005
10215
  }
10006
- 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;
10007
10217
  var init_workspace_ensure = __esm({
10008
10218
  "src/core/workspace-ensure.ts"() {
10009
10219
  "use strict";
10010
10220
  import_node_child_process19 = require("child_process");
10011
10221
  import_node_fs23 = __toESM(require("fs"));
10012
- import_node_path23 = __toESM(require("path"));
10222
+ import_node_path24 = __toESM(require("path"));
10013
10223
  init_devcontainer_profile();
10014
10224
  init_devpod_environment();
10015
10225
  init_devpod_mutation();
@@ -10095,7 +10305,7 @@ function warnMissingWorkspaceOwnership(repoPath) {
10095
10305
  );
10096
10306
  }
10097
10307
  function defaultWorktreePath(mainRepo, ws) {
10098
- return import_node_path24.default.join(mainRepo, "trees", ws);
10308
+ return import_node_path25.default.join(mainRepo, "trees", ws);
10099
10309
  }
10100
10310
  function assertDefaultWorktreeRootIgnored(mainRepo) {
10101
10311
  const ignored = (0, import_node_child_process20.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
@@ -10103,12 +10313,12 @@ function assertDefaultWorktreeRootIgnored(mainRepo) {
10103
10313
  });
10104
10314
  if (ignored.status !== 0) {
10105
10315
  throw new Error(
10106
- `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.`
10107
10317
  );
10108
10318
  }
10109
10319
  }
10110
10320
  function legacyDefaultWorktreePath(mainRepo, ws) {
10111
- 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}`);
10112
10322
  }
10113
10323
  function teardownFallbackPath(mainRepo, workspace) {
10114
10324
  const candidates = [
@@ -10262,7 +10472,7 @@ async function workspaceUp(branch, opts = {}) {
10262
10472
  const generatedPath = candidatePaths.find(
10263
10473
  (candidate) => !import_node_fs24.default.existsSync(candidate) && !worktrees.some((worktree) => sameWorkspacePath(worktree.path, candidate))
10264
10474
  );
10265
- const selectedPath = opts.path ? import_node_path24.default.resolve(opts.path) : existingBranch?.path ?? generatedPath;
10475
+ const selectedPath = opts.path ? import_node_path25.default.resolve(opts.path) : existingBranch?.path ?? generatedPath;
10266
10476
  if (!selectedPath) {
10267
10477
  throw new Error(`Could not allocate a collision-safe worktree path for branch '${branch}'.`);
10268
10478
  }
@@ -10450,13 +10660,13 @@ async function workspaceStop(target, opts = {}) {
10450
10660
  async function workspaceDown(target, opts = {}) {
10451
10661
  return runWorkspaceLifecycle("down", target, opts);
10452
10662
  }
10453
- var import_node_child_process20, import_node_fs24, import_node_path24, WORKSPACE_ALLOCATION_LOCK_WAIT_MS;
10663
+ var import_node_child_process20, import_node_fs24, import_node_path25, WORKSPACE_ALLOCATION_LOCK_WAIT_MS;
10454
10664
  var init_workspace_lifecycle = __esm({
10455
10665
  "src/core/workspace-lifecycle.ts"() {
10456
10666
  "use strict";
10457
10667
  import_node_child_process20 = require("child_process");
10458
10668
  import_node_fs24 = __toESM(require("fs"));
10459
- import_node_path24 = __toESM(require("path"));
10669
+ import_node_path25 = __toESM(require("path"));
10460
10670
  init_devpod_mutation();
10461
10671
  init_devpod_workspaces();
10462
10672
  init_repo_config();
@@ -11035,7 +11245,7 @@ function readJson(filePath) {
11035
11245
  }
11036
11246
  }
11037
11247
  function relative(repoPath, filePath) {
11038
- return import_node_path25.default.relative(repoPath, filePath) || ".";
11248
+ return import_node_path26.default.relative(repoPath, filePath) || ".";
11039
11249
  }
11040
11250
  function redactEnvAssignments(value) {
11041
11251
  return value.replace(
@@ -11076,7 +11286,7 @@ function inspectPackageManager(repoPath, pkg) {
11076
11286
  ["bun.lock", "bun"]
11077
11287
  ];
11078
11288
  for (const [fileName, name] of lockfiles) {
11079
- 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))) {
11080
11290
  return { name, source: fileName };
11081
11291
  }
11082
11292
  }
@@ -11091,7 +11301,7 @@ function inspectNode(repoPath, pkg) {
11091
11301
  if (typeof engines?.node === "string") {
11092
11302
  return { version: engines.node, source: "package.json:engines.node" };
11093
11303
  }
11094
- const nvmrc = import_node_path25.default.join(repoPath, ".nvmrc");
11304
+ const nvmrc = import_node_path26.default.join(repoPath, ".nvmrc");
11095
11305
  if (import_node_fs25.default.existsSync(nvmrc)) {
11096
11306
  const version = import_node_fs25.default.readFileSync(nvmrc, "utf-8").trim();
11097
11307
  return { version, source: ".nvmrc" };
@@ -11154,7 +11364,7 @@ function configuredComposeFiles(repoPath) {
11154
11364
  const files = config.apps.filter(
11155
11365
  (app) => app.runtime === "docker"
11156
11366
  ).flatMap((app) => app.docker.composeFiles).filter(
11157
- (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("..")
11158
11368
  );
11159
11369
  return Array.from(new Set(files));
11160
11370
  } catch {
@@ -11172,7 +11382,7 @@ function composeFiles(repoPath) {
11172
11382
  ...configuredComposeFiles(repoPath)
11173
11383
  ];
11174
11384
  return Array.from(new Set(candidates)).filter(
11175
- (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))
11176
11386
  );
11177
11387
  }
11178
11388
  function stringArray2(value) {
@@ -11210,7 +11420,7 @@ function inspectServices(repoPath) {
11210
11420
  const services = [];
11211
11421
  for (const fileName of composeFiles(repoPath)) {
11212
11422
  try {
11213
- 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"));
11214
11424
  const serviceMap = asRecord3(asRecord3(parsed)?.services);
11215
11425
  for (const [name, value] of Object.entries(serviceMap ?? {})) {
11216
11426
  const service = asRecord3(value);
@@ -11246,7 +11456,7 @@ function inspectServices(repoPath) {
11246
11456
  }
11247
11457
  function inspectEnvFiles(repoPath) {
11248
11458
  const files = import_node_fs25.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
11249
- 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");
11250
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();
11251
11461
  return { path: fileName, names };
11252
11462
  });
@@ -11260,13 +11470,13 @@ function inspectEnvFiles(repoPath) {
11260
11470
  };
11261
11471
  }
11262
11472
  function inspectDevcontainer(repoPath) {
11263
- const dir = import_node_path25.default.join(repoPath, ".devcontainer");
11473
+ const dir = import_node_path26.default.join(repoPath, ".devcontainer");
11264
11474
  if (!import_node_fs25.default.existsSync(dir)) {
11265
11475
  return { exists: false, files: [] };
11266
11476
  }
11267
11477
  return {
11268
11478
  exists: true,
11269
- 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}`)
11270
11480
  };
11271
11481
  }
11272
11482
  function inspectDevrouter(repoPath) {
@@ -11315,14 +11525,14 @@ function inspectAgentGuidance(repoPath) {
11315
11525
  ["AGENTS.md", "agents"],
11316
11526
  ["CLAUDE.md", "claude"]
11317
11527
  ]) {
11318
- 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))) {
11319
11529
  results.push({ path: fileName, kind });
11320
11530
  }
11321
11531
  }
11322
- const skillsDir = import_node_path25.default.join(repoPath, ".agents", "skills");
11532
+ const skillsDir = import_node_path26.default.join(repoPath, ".agents", "skills");
11323
11533
  if (import_node_fs25.default.existsSync(skillsDir)) {
11324
11534
  for (const name of import_node_fs25.default.readdirSync(skillsDir).sort()) {
11325
- const skillPath = import_node_path25.default.join(skillsDir, name, "SKILL.md");
11535
+ const skillPath = import_node_path26.default.join(skillsDir, name, "SKILL.md");
11326
11536
  if (import_node_fs25.default.existsSync(skillPath)) {
11327
11537
  results.push({ path: relative(repoPath, skillPath), kind: "skill" });
11328
11538
  }
@@ -11366,7 +11576,7 @@ function buildIssues(report) {
11366
11576
  }
11367
11577
  function inspectRepo(options = {}) {
11368
11578
  const repoPath = resolveRepoPath(options.repo);
11369
- const pkg = readJson(import_node_path25.default.join(repoPath, "package.json"));
11579
+ const pkg = readJson(import_node_path26.default.join(repoPath, "package.json"));
11370
11580
  const scripts = inspectScripts(pkg);
11371
11581
  const reportWithoutIssues = {
11372
11582
  repoPath,
@@ -11385,12 +11595,12 @@ function inspectRepo(options = {}) {
11385
11595
  issues: buildIssues(reportWithoutIssues)
11386
11596
  };
11387
11597
  }
11388
- var import_node_fs25, import_node_path25, import_yaml6;
11598
+ var import_node_fs25, import_node_path26, import_yaml6;
11389
11599
  var init_repo_inspect = __esm({
11390
11600
  "src/core/repo-inspect.ts"() {
11391
11601
  "use strict";
11392
11602
  import_node_fs25 = __toESM(require("fs"));
11393
- import_node_path25 = __toESM(require("path"));
11603
+ import_node_path26 = __toESM(require("path"));
11394
11604
  import_yaml6 = __toESM(require("yaml"));
11395
11605
  init_repo_config();
11396
11606
  }
@@ -11495,7 +11705,7 @@ function requiredFileChecks(repoPath) {
11495
11705
  ".devcontainer/docker-compose.yml",
11496
11706
  ".devrouter.yml"
11497
11707
  ];
11498
- 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)));
11499
11709
  return {
11500
11710
  id: "repo.devcontainer.verify-files",
11501
11711
  level: missing.length === 0 ? "ok" : "error",
@@ -11714,12 +11924,12 @@ async function verifyDevcontainer(options = {}) {
11714
11924
  nextSteps: collectNextSteps3(checks)
11715
11925
  };
11716
11926
  }
11717
- var import_node_fs26, import_node_path26;
11927
+ var import_node_fs26, import_node_path27;
11718
11928
  var init_devcontainer_verify = __esm({
11719
11929
  "src/core/devcontainer-verify.ts"() {
11720
11930
  "use strict";
11721
11931
  import_node_fs26 = __toESM(require("fs"));
11722
- import_node_path26 = __toESM(require("path"));
11932
+ import_node_path27 = __toESM(require("path"));
11723
11933
  init_capabilities();
11724
11934
  init_doctor();
11725
11935
  init_host_routes();
@@ -12016,7 +12226,7 @@ function packageManagerIssues(repo) {
12016
12226
  }
12017
12227
  function plannedFiles(repoPath, version) {
12018
12228
  const repo = inspectRepo({ repo: repoPath });
12019
- const projectName = sanitizeProjectName(import_node_path27.default.basename(repo.repoPath));
12229
+ const projectName = sanitizeProjectName(import_node_path28.default.basename(repo.repoPath));
12020
12230
  const nodeMajor = majorVersion(repo.node?.version, "24");
12021
12231
  const pnpmVersion = repo.packageManager?.name === "pnpm" && repo.packageManager.version ? repo.packageManager.version : DEFAULT_PNPM_VERSION;
12022
12232
  const port = inferPort2(repo);
@@ -12061,7 +12271,7 @@ function plannedFiles(repoPath, version) {
12061
12271
  };
12062
12272
  }
12063
12273
  function classifyFile(repoPath, file) {
12064
- const absolutePath = import_node_path27.default.join(repoPath, file.relativePath);
12274
+ const absolutePath = import_node_path28.default.join(repoPath, file.relativePath);
12065
12275
  if (!import_node_fs27.default.existsSync(absolutePath)) {
12066
12276
  return {
12067
12277
  path: file.relativePath,
@@ -12127,8 +12337,8 @@ function buildPlan(repoPath, dryRun, version) {
12127
12337
  };
12128
12338
  }
12129
12339
  function writeFile(repoPath, file) {
12130
- const absolutePath = import_node_path27.default.join(repoPath, file.relativePath);
12131
- 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 });
12132
12342
  import_node_fs27.default.writeFileSync(absolutePath, file.content, "utf-8");
12133
12343
  if (file.executable) {
12134
12344
  import_node_fs27.default.chmodSync(absolutePath, 493);
@@ -12169,12 +12379,12 @@ function writeDevcontainer(options = {}) {
12169
12379
  nextSteps: postWriteNextSteps(repoPath)
12170
12380
  };
12171
12381
  }
12172
- 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;
12173
12383
  var init_devcontainer_write = __esm({
12174
12384
  "src/core/devcontainer-write.ts"() {
12175
12385
  "use strict";
12176
12386
  import_node_fs27 = __toESM(require("fs"));
12177
- import_node_path27 = __toESM(require("path"));
12387
+ import_node_path28 = __toESM(require("path"));
12178
12388
  init_repo_config();
12179
12389
  init_repo_inspect();
12180
12390
  MANAGED_MARKER2 = "devrouter:managed devcontainer";
@@ -12565,7 +12775,7 @@ function sanitizeRouterId(value) {
12565
12775
  return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
12566
12776
  }
12567
12777
  function repoHash(repoPath) {
12568
- return (0, import_node_crypto9.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);
12569
12779
  }
12570
12780
  function asDockerApp(app) {
12571
12781
  return app.runtime === "docker";
@@ -12644,9 +12854,9 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
12644
12854
  if (dockerApps.length === 0) {
12645
12855
  throw new Error("No docker apps selected to prepare compose overlay.");
12646
12856
  }
12647
- 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));
12648
12858
  import_node_fs28.default.mkdirSync(cachePath, { recursive: true });
12649
- const overlayPath = import_node_path28.default.join(cachePath, "compose.devrouter.yml");
12859
+ const overlayPath = import_node_path29.default.join(cachePath, "compose.devrouter.yml");
12650
12860
  const overlayDocument = buildOverlayDocument(dockerApps, publishTcpPorts);
12651
12861
  import_node_fs28.default.writeFileSync(overlayPath, import_yaml7.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
12652
12862
  return {
@@ -12768,14 +12978,14 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
12768
12978
  const port = Number(match[1]);
12769
12979
  return Number.isInteger(port) && port > 0 ? port : void 0;
12770
12980
  }
12771
- var import_node_child_process25, import_node_crypto9, 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;
12772
12982
  var init_docker_run = __esm({
12773
12983
  "src/core/docker-run.ts"() {
12774
12984
  "use strict";
12775
12985
  import_node_child_process25 = require("child_process");
12776
12986
  import_node_crypto9 = require("crypto");
12777
12987
  import_node_fs28 = __toESM(require("fs"));
12778
- import_node_path28 = __toESM(require("path"));
12988
+ import_node_path29 = __toESM(require("path"));
12779
12989
  import_yaml7 = __toESM(require("yaml"));
12780
12990
  init_docker_error_guidance();
12781
12991
  init_paths();
@@ -13629,7 +13839,7 @@ function measureWorktreeConsumption(worktreePath, options) {
13629
13839
  timedOut = true;
13630
13840
  break;
13631
13841
  }
13632
- const entryPath = import_node_path29.default.join(dirPath, entry.name);
13842
+ const entryPath = import_node_path30.default.join(dirPath, entry.name);
13633
13843
  let entryStat;
13634
13844
  try {
13635
13845
  entryStat = import_node_fs29.default.lstatSync(entryPath);
@@ -13706,12 +13916,12 @@ function describeError(error, worktreePath) {
13706
13916
  }
13707
13917
  return `could not stat worktree path '${worktreePath}': ${error?.message ?? String(error)}`;
13708
13918
  }
13709
- 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;
13710
13920
  var init_workspace_consumption = __esm({
13711
13921
  "src/core/workspace-consumption.ts"() {
13712
13922
  "use strict";
13713
13923
  import_node_fs29 = __toESM(require("fs"));
13714
- import_node_path29 = __toESM(require("path"));
13924
+ import_node_path30 = __toESM(require("path"));
13715
13925
  init_devpod_environment();
13716
13926
  DEFAULT_DEADLINE_MS = 1e4;
13717
13927
  BLOCK_SIZE_BYTES = 512;
@@ -14337,7 +14547,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
14337
14547
  containers = measureContainersFn(worktreePaths);
14338
14548
  } catch (error) {
14339
14549
  const reason = `container measurement failed: ${describeCause(error)}`;
14340
- containers = new Map(worktreePaths.map((path29) => [path29, unknownContainers(reason)]));
14550
+ containers = new Map(worktreePaths.map((path30) => [path30, unknownContainers(reason)]));
14341
14551
  }
14342
14552
  }
14343
14553
  const rows = records.map((record) => {
@@ -14568,7 +14778,7 @@ var init_version = __esm({
14568
14778
 
14569
14779
  // src/cli.ts
14570
14780
  var import_commander = require("commander");
14571
- var CLI_VERSION = true ? "0.0.45" : "0.0.0-dev";
14781
+ var CLI_VERSION = true ? "0.0.46" : "0.0.0-dev";
14572
14782
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
14573
14783
  function withErrorHandling(action2) {
14574
14784
  return async (...args) => {