@devrouter/cli 0.0.45 → 0.0.48

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
@@ -44,6 +44,7 @@ function buildDevrouterSection() {
44
44
  "",
45
45
  "Quick validation sequence:",
46
46
  "- Managed devcontainer consumer images contain no devrouter package or helper; `devrouter ensure` delivers the matching helper at runtime.",
47
+ "- Devsy runtime: run `devrouter setup --yes --workspace-runtime devsy` once; `doctor` then reports verified agent readiness without network access.",
47
48
  "- `devrouter up`",
48
49
  "- `devrouter tls install` (required when repo defines tcp/postgres apps)",
49
50
  "- `devrouter app ls --repo .`",
@@ -322,7 +323,7 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
322
323
  - **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
324
  - **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
325
  - **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.
326
+ - **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. For Devsy, run \`devrouter setup --yes --workspace-runtime devsy\` once so Devrouter acquires and verifies the pinned agent in its own machine cache. \`doctor\` reports \`ready\`, \`missing\`, \`stale\`, or \`invalid\` without network access, and \`ensure\` fails before the provider queue when readiness is not \`ready\`. An explicit \`DEVSY_AGENT_BINARY\` remains authoritative and must match a pinned official asset.
326
327
  - **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
328
  - **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
329
  - **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.
@@ -376,8 +377,9 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
376
377
  - \`devrouter init [--write-agents] [--write-skill]\`: print AI onboarding prompt (non-mutating by default)
377
378
  - \`devrouter -V [--repo .]\`: show installed CLI version, local repo version, and next upgrade target
378
379
  - \`devrouter upgrade [version] [--repo .]\`: list upgrade targets or print target Agent Adaptation Prompt
379
- - \`devrouter setup --yes [--repo .] [--json]\`: first-run machine setup plus structured diagnostics
380
+ - \`devrouter setup --yes [--repo .] [--json] [--workspace-runtime <devpod|devsy>]\`: first-run machine setup plus structured diagnostics; explicit Devsy selection acquires its verified agent
380
381
  - \`devrouter ensure [path] [--profile <name>] [--open] [--json]\`: canonical startup/reconciliation for primary and linked checkouts; a managed profile selects its independent resource dimensions
382
+ - \`devrouter profile resolve --repo <path> [--profile <selection>] [--json]\`: resolve exact profile resources for automation without starting or inspecting a runtime
381
383
  - \`devrouter stop [path] [--delete] [--json]\`: stop the exact workspace runtime and remove exact routes; \`--delete\` explicitly deletes its ownership-proven data without removing the checkout
382
384
  - \`devrouter exec [path] -- <command...>\`: literal one-shot command inside the exact running workspace runtime
383
385
  - \`devrouter up\` / \`devrouter down\`: start/stop shared Traefik router
@@ -411,6 +413,9 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
411
413
 
412
414
  For devcontainer onboarding:
413
415
 
416
+ When Devsy owns the workspace, first run
417
+ \`devrouter setup --repo . --yes --workspace-runtime devsy --json\`.
418
+
414
419
  1. \`devrouter setup --repo . --yes --json\`
415
420
  2. \`devrouter doctor --repo . --json\`
416
421
  3. \`devrouter repo inspect --repo . --json\`
@@ -720,6 +725,9 @@ function runDockerCompose(args) {
720
725
  function startRouterStack() {
721
726
  runDockerCompose(["up", "-d"]);
722
727
  }
728
+ function restartRouterStack() {
729
+ runDockerCompose(["restart", "traefik"]);
730
+ }
723
731
  function stopRouterStack() {
724
732
  runDockerCompose(["down"]);
725
733
  }
@@ -898,16 +906,65 @@ function parseLockOwner(value) {
898
906
  return { pid };
899
907
  }
900
908
  }
901
- function isLockOwnerLive(owner) {
902
- if (!isProcessAlive(owner.pid)) return false;
909
+ function parseCanonicalLockOwner(value) {
910
+ const trimmed = value.trim();
911
+ return CANONICAL_OWNER_RE.test(trimmed) ? parseLockOwner(trimmed) : void 0;
912
+ }
913
+ function parseLockRecord(value) {
914
+ const trimmed = value.trim();
915
+ const lastColon = trimmed.lastIndexOf(":");
916
+ if (lastColon >= 0 && CANONICAL_OWNER_RE.test(trimmed.slice(0, lastColon))) {
917
+ const tail = Number(trimmed.slice(lastColon + 1));
918
+ if (Number.isInteger(tail) && tail > 0) {
919
+ return { owner: parseLockOwner(trimmed.slice(0, lastColon)), acquiredAtMs: tail };
920
+ }
921
+ }
922
+ return { owner: parseLockOwner(trimmed) };
923
+ }
924
+ function isLockOwnerLive(owner, cache, now = Date.now()) {
925
+ const cacheKey = `${owner.pid}:${owner.processBirth ?? "legacy"}`;
926
+ if (!isProcessAlive(owner.pid)) {
927
+ cache.delete(cacheKey);
928
+ return false;
929
+ }
903
930
  if (!owner.processBirth) return true;
931
+ const cached = cache.get(cacheKey);
932
+ if (cached && now - cached.checkedAtMs < PROCESS_BIRTH_RECHECK_INTERVAL_MS) {
933
+ return cached.live;
934
+ }
904
935
  const currentBirth = processBirthIdentity(owner.pid);
905
- return currentBirth === void 0 || currentBirth === owner.processBirth;
936
+ const live = currentBirth === void 0 || currentBirth === owner.processBirth;
937
+ cache.set(cacheKey, { checkedAtMs: now, live });
938
+ return live;
906
939
  }
907
940
  function sameFile(left, right) {
908
941
  return left.dev === right.dev && left.ino === right.ino;
909
942
  }
910
- function tryReclaimStaleLock(lockPath, staleLinkPath) {
943
+ function inspectFairQueue(lockPath, ownTicketPath, livenessCache) {
944
+ const directory = import_node_path4.default.dirname(lockPath);
945
+ const prefix = `${import_node_path4.default.basename(lockPath)}.queue.`;
946
+ for (; ; ) {
947
+ const names = import_node_fs4.default.readdirSync(directory).filter((name) => name.startsWith(prefix)).sort();
948
+ const ownName = import_node_path4.default.basename(ownTicketPath);
949
+ const position = names.indexOf(ownName);
950
+ if (position < 0) {
951
+ throw new Error("provider mutation queue ticket disappeared before acquisition");
952
+ }
953
+ const leaderPath = import_node_path4.default.join(directory, names[0]);
954
+ let leader;
955
+ try {
956
+ leader = parseCanonicalLockOwner(import_node_fs4.default.readFileSync(leaderPath, "utf-8"));
957
+ } catch (error) {
958
+ if (error.code === "ENOENT") continue;
959
+ throw error;
960
+ }
961
+ if (leader && isLockOwnerLive(leader, livenessCache)) {
962
+ return { leaderPid: leader.pid, position: position + 1 };
963
+ }
964
+ import_node_fs4.default.rmSync(leaderPath, { force: true });
965
+ }
966
+ }
967
+ function tryReclaimStaleLock(lockPath, staleLinkPath, livenessCache) {
911
968
  let fd;
912
969
  try {
913
970
  fd = import_node_fs4.default.openSync(lockPath, "r");
@@ -918,9 +975,9 @@ function tryReclaimStaleLock(lockPath, staleLinkPath) {
918
975
  throw error;
919
976
  }
920
977
  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 };
978
+ const record = parseLockRecord(import_node_fs4.default.readFileSync(fd, "utf-8"));
979
+ if (record.owner && isLockOwnerLive(record.owner, livenessCache)) {
980
+ return { kind: "live", pid: record.owner.pid, acquiredAtMs: record.acquiredAtMs };
924
981
  }
925
982
  const staleStat = import_node_fs4.default.fstatSync(fd);
926
983
  if (staleStat.nlink !== 1) {
@@ -965,28 +1022,87 @@ function acquireFileLock(lockPath, options) {
965
1022
  const owner = `${process.pid}:${Buffer.from(processBirth).toString("base64url")}:${ownerId}`;
966
1023
  const candidatePath = `${lockPath}.${process.pid}.${ownerId}.candidate`;
967
1024
  const staleLinkPath = `${candidatePath}.stale`;
1025
+ const enqueuedAtMs = Date.now();
1026
+ const queueTicketPath = `${lockPath}.queue.${String(enqueuedAtMs).padStart(13, "0")}.${String(process.pid).padStart(10, "0")}.${ownerId}`;
1027
+ const queueCandidatePath = `${queueTicketPath}.candidate`;
968
1028
  const deadline = Date.now() + (options.waitMs ?? 0);
1029
+ const waitStartedAt = Date.now();
1030
+ const progressIntervalMs = options.progressIntervalMs ?? DEFAULT_WAIT_PROGRESS_INTERVAL_MS;
1031
+ let lastProgressAt = waitStartedAt;
969
1032
  let reclaimAttempts = 0;
1033
+ const livenessCache = /* @__PURE__ */ new Map();
970
1034
  import_node_fs4.default.writeFileSync(candidatePath, `${owner}
1035
+ `, {
1036
+ encoding: "utf-8",
1037
+ flag: "wx"
1038
+ });
1039
+ if (options.fair) {
1040
+ import_node_fs4.default.writeFileSync(queueCandidatePath, `${owner}
971
1041
  `, { encoding: "utf-8", flag: "wx" });
1042
+ import_node_fs4.default.renameSync(queueCandidatePath, queueTicketPath);
1043
+ }
972
1044
  try {
973
1045
  for (; ; ) {
1046
+ let queueState;
1047
+ if (options.fair) {
1048
+ queueState = inspectFairQueue(lockPath, queueTicketPath, livenessCache);
1049
+ }
1050
+ if (queueState && queueState.position > 1) {
1051
+ const now = Date.now();
1052
+ if (now >= deadline) {
1053
+ const target = options.target ? ` for ${options.target}` : "";
1054
+ const waitedSeconds = Math.round((now - waitStartedAt) / 1e3);
1055
+ throw new Error(
1056
+ `${options.activity} is still queued${target} (position ${queueState.position}, ahead PID ${queueState.leaderPid}); gave up after waiting ${waitedSeconds}s`
1057
+ );
1058
+ }
1059
+ if (options.onWait && now - lastProgressAt >= progressIntervalMs) {
1060
+ lastProgressAt = now;
1061
+ options.onWait({
1062
+ waitingMs: now - waitStartedAt,
1063
+ holderPid: queueState.leaderPid,
1064
+ queuePosition: queueState.position,
1065
+ waitingOn: "queue"
1066
+ });
1067
+ }
1068
+ sleepSync(20);
1069
+ continue;
1070
+ }
1071
+ const acquiredAtMs = Date.now();
1072
+ const record = `${owner}:${acquiredAtMs}`;
1073
+ import_node_fs4.default.writeFileSync(candidatePath, `${record}
1074
+ `, "utf-8");
974
1075
  try {
975
1076
  import_node_fs4.default.linkSync(candidatePath, lockPath);
976
- return owner;
1077
+ return record;
977
1078
  } catch (error) {
978
1079
  if (error.code !== "EEXIST") {
979
1080
  throw error;
980
1081
  }
981
1082
  }
982
- const state = tryReclaimStaleLock(lockPath, staleLinkPath);
1083
+ const state = tryReclaimStaleLock(lockPath, staleLinkPath, livenessCache);
983
1084
  if (state.kind === "reclaimed") {
984
1085
  continue;
985
1086
  }
986
1087
  if (state.kind === "live") {
987
- if (Date.now() >= deadline) {
1088
+ const now = Date.now();
1089
+ if (now >= deadline) {
988
1090
  const target = options.target ? ` for ${options.target}` : "";
989
- throw new Error(`${options.activity} is already running${target} (PID ${state.pid})`);
1091
+ const waitedSeconds = Math.round((now - waitStartedAt) / 1e3);
1092
+ const heldSeconds = state.acquiredAtMs !== void 0 ? `, held for ${Math.round((now - state.acquiredAtMs) / 1e3)}s` : "";
1093
+ throw new Error(
1094
+ `${options.activity} is already running${target} (PID ${state.pid}${heldSeconds}); gave up after waiting ${waitedSeconds}s`
1095
+ );
1096
+ }
1097
+ if (options.onWait && now - lastProgressAt >= progressIntervalMs) {
1098
+ lastProgressAt = now;
1099
+ options.onWait({
1100
+ waitingMs: now - waitStartedAt,
1101
+ holderPid: state.pid,
1102
+ holderHeldMs: state.acquiredAtMs !== void 0 ? Math.max(0, now - state.acquiredAtMs) : void 0,
1103
+ queuePosition: queueState?.position,
1104
+ waitingOn: "lock"
1105
+ });
990
1106
  }
991
1107
  sleepSync(20);
992
1108
  continue;
@@ -1002,6 +1118,8 @@ function acquireFileLock(lockPath, options) {
1002
1118
  } finally {
1003
1119
  import_node_fs4.default.rmSync(candidatePath, { force: true });
1004
1120
  import_node_fs4.default.rmSync(staleLinkPath, { force: true });
1121
+ import_node_fs4.default.rmSync(queueCandidatePath, { force: true });
1122
+ import_node_fs4.default.rmSync(queueTicketPath, { force: true });
1005
1123
  }
1006
1124
  }
1007
1125
  function releaseFileLock(lockPath, owner) {
@@ -1031,19 +1149,33 @@ async function withFileLock(lockPath, options, operation) {
1031
1149
  releaseFileLock(lockPath, owner);
1032
1150
  }
1033
1151
  }
1034
- var import_node_child_process2, import_node_crypto2, import_node_fs4;
1152
+ function createStderrWaitReporter(activity, target) {
1153
+ return (progress) => {
1154
+ const heldSeconds = progress.holderHeldMs !== void 0 ? `, held for ${Math.round(progress.holderHeldMs / 1e3)}s` : "";
1155
+ 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}`;
1156
+ process.stderr.write(
1157
+ `${activity} for ${target}: ${status}; waited ${Math.round(progress.waitingMs / 1e3)}s so far
1158
+ `
1159
+ );
1160
+ };
1161
+ }
1162
+ 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
1163
  var init_file_lock = __esm({
1036
1164
  "src/core/file-lock.ts"() {
1037
1165
  "use strict";
1038
1166
  import_node_child_process2 = require("child_process");
1039
1167
  import_node_crypto2 = require("crypto");
1040
1168
  import_node_fs4 = __toESM(require("fs"));
1169
+ import_node_path4 = __toESM(require("path"));
1170
+ DEFAULT_WAIT_PROGRESS_INTERVAL_MS = 1e4;
1171
+ PROCESS_BIRTH_RECHECK_INTERVAL_MS = 1e3;
1172
+ 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
1173
  }
1042
1174
  });
1043
1175
 
1044
1176
  // src/core/workspace.ts
1045
1177
  function comparableWorkspacePath(filePath) {
1046
- const resolved = import_node_path4.default.resolve(filePath);
1178
+ const resolved = import_node_path5.default.resolve(filePath);
1047
1179
  try {
1048
1180
  return import_node_fs5.default.realpathSync.native(resolved);
1049
1181
  } catch {
@@ -1073,7 +1205,7 @@ function workspaceIdentityCandidates(source) {
1073
1205
  return candidates;
1074
1206
  }
1075
1207
  function isLinkedWorktree(repoPath) {
1076
- const gitPath = import_node_path4.default.join(repoPath, ".git");
1208
+ const gitPath = import_node_path5.default.join(repoPath, ".git");
1077
1209
  let stat;
1078
1210
  try {
1079
1211
  stat = import_node_fs5.default.statSync(gitPath);
@@ -1097,7 +1229,7 @@ function isLinkedWorktree(repoPath) {
1097
1229
  return /(^|\/)worktrees\//.test(gitdir);
1098
1230
  }
1099
1231
  function gitMetadataDir(repoPath) {
1100
- const gitPath = import_node_path4.default.join(repoPath, ".git");
1232
+ const gitPath = import_node_path5.default.join(repoPath, ".git");
1101
1233
  let stat;
1102
1234
  try {
1103
1235
  stat = import_node_fs5.default.statSync(gitPath);
@@ -1114,7 +1246,7 @@ function gitMetadataDir(repoPath) {
1114
1246
  if (!match) {
1115
1247
  return void 0;
1116
1248
  }
1117
- return import_node_path4.default.resolve(repoPath, match[1].trim());
1249
+ return import_node_path5.default.resolve(repoPath, match[1].trim());
1118
1250
  }
1119
1251
  function validatePersistedWorkspace(value) {
1120
1252
  const workspace = value.trim();
@@ -1128,7 +1260,7 @@ function readPersistedWorkspace(repoPath) {
1128
1260
  if (!gitDir) {
1129
1261
  return void 0;
1130
1262
  }
1131
- const metadataPath = import_node_path4.default.join(gitDir, WORKSPACE_METADATA_FILE);
1263
+ const metadataPath = import_node_path5.default.join(gitDir, WORKSPACE_METADATA_FILE);
1132
1264
  try {
1133
1265
  return validatePersistedWorkspace(import_node_fs5.default.readFileSync(metadataPath, "utf-8"));
1134
1266
  } catch (error) {
@@ -1153,7 +1285,7 @@ function persistWorkspace(repoPath, value) {
1153
1285
  if (!gitDir) {
1154
1286
  throw new Error(`cannot persist workspace identity: '${repoPath}' is not a Git checkout`);
1155
1287
  }
1156
- writeFileAtomically(import_node_path4.default.join(gitDir, WORKSPACE_METADATA_FILE), `${workspace}
1288
+ writeFileAtomically(import_node_path5.default.join(gitDir, WORKSPACE_METADATA_FILE), `${workspace}
1157
1289
  `);
1158
1290
  return workspace;
1159
1291
  }
@@ -1162,7 +1294,7 @@ async function withWorkspaceLifecycleLock(repoPath, operation) {
1162
1294
  if (!gitDir) {
1163
1295
  throw new Error(`cannot lock workspace lifecycle: '${repoPath}' is not a Git checkout`);
1164
1296
  }
1165
- const lockPath = import_node_path4.default.join(gitDir, WORKSPACE_LOCK_FILE);
1297
+ const lockPath = import_node_path5.default.join(gitDir, WORKSPACE_LOCK_FILE);
1166
1298
  return withFileLock(
1167
1299
  lockPath,
1168
1300
  { activity: "workspace lifecycle", target: `'${repoPath}'` },
@@ -1190,7 +1322,7 @@ function deriveLinkedWorktreeWorkspace(repoPath, linkedWorktreeBranch) {
1190
1322
  return void 0;
1191
1323
  }
1192
1324
  const branch = currentBranch(repoPath);
1193
- return wsFromBranch(branch ?? import_node_path4.default.basename(import_node_path4.default.resolve(repoPath)));
1325
+ return wsFromBranch(branch ?? import_node_path5.default.basename(import_node_path5.default.resolve(repoPath)));
1194
1326
  }
1195
1327
  function resolveWorktreeWorkspace(repoPath, linkedWorktreeBranch) {
1196
1328
  return readPersistedWorkspace(repoPath) ?? deriveLinkedWorktreeWorkspace(repoPath, linkedWorktreeBranch);
@@ -1213,14 +1345,14 @@ function resolveWorkspace(repoPath, override) {
1213
1345
  }
1214
1346
  return deriveLinkedWorktreeWorkspace(repoPath);
1215
1347
  }
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;
1348
+ 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
1349
  var init_workspace = __esm({
1218
1350
  "src/core/workspace.ts"() {
1219
1351
  "use strict";
1220
1352
  import_node_child_process3 = require("child_process");
1221
1353
  import_node_crypto3 = require("crypto");
1222
1354
  import_node_fs5 = __toESM(require("fs"));
1223
- import_node_path4 = __toESM(require("path"));
1355
+ import_node_path5 = __toESM(require("path"));
1224
1356
  init_atomic_file();
1225
1357
  init_file_lock();
1226
1358
  MAX_WORKSPACE_LENGTH = 32;
@@ -1282,6 +1414,12 @@ function hostRouteFromInput(input2, existing, now) {
1282
1414
  function sanitizeKey(value) {
1283
1415
  return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1284
1416
  }
1417
+ function buildHostRouteRouterKey(routeId) {
1418
+ return `host-${sanitizeKey(routeId)}`;
1419
+ }
1420
+ function buildHostRouteRouterName(repoPath, name) {
1421
+ return `${buildHostRouteRouterKey(buildHostRouteId(repoPath, name))}@file`;
1422
+ }
1285
1423
  function tcpTlsOptionName(tcpProtocol) {
1286
1424
  return `devrouter-tcp-${tcpProtocol}`;
1287
1425
  }
@@ -1292,7 +1430,7 @@ function buildHostRoutesDocument(routes, tlsEnabled) {
1292
1430
  const tcpServices = {};
1293
1431
  const tlsOptions = {};
1294
1432
  for (const route of routes) {
1295
- const key = `host-${sanitizeKey(route.id)}`;
1433
+ const key = buildHostRouteRouterKey(route.id);
1296
1434
  if (route.protocol === "tcp") {
1297
1435
  const alpn = route.tcpProtocol ? TCP_TLS_ALPN_PROTOCOLS[route.tcpProtocol] : void 0;
1298
1436
  let tls = {};
@@ -1644,7 +1782,7 @@ function listHostRoutes(tlsEnabled) {
1644
1782
  protocol: isTcp ? `tcp/${tcpProtocol}` : "http",
1645
1783
  appName: route.name,
1646
1784
  serviceName: route.name,
1647
- projectName: import_node_path5.default.basename(route.repoPath),
1785
+ projectName: import_node_path6.default.basename(route.repoPath),
1648
1786
  hosts: [route.host],
1649
1787
  // TCP routes are reached via an SNI-aware client on the shared protocol
1650
1788
  // port; surface a protocol-scheme URL rather than http(s)://.
@@ -1659,12 +1797,12 @@ function listHostRoutes(tlsEnabled) {
1659
1797
  };
1660
1798
  });
1661
1799
  }
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;
1800
+ 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
1801
  var init_host_routes = __esm({
1664
1802
  "src/core/host-routes.ts"() {
1665
1803
  "use strict";
1666
1804
  import_node_fs6 = __toESM(require("fs"));
1667
- import_node_path5 = __toESM(require("path"));
1805
+ import_node_path6 = __toESM(require("path"));
1668
1806
  import_node_util = require("util");
1669
1807
  import_yaml = __toESM(require("yaml"));
1670
1808
  init_atomic_file();
@@ -2512,10 +2650,10 @@ function appToNodeValue(app) {
2512
2650
  return value;
2513
2651
  }
2514
2652
  function resolveRepoPath(repoPath) {
2515
- return import_node_path6.default.resolve(repoPath ?? process.cwd());
2653
+ return import_node_path7.default.resolve(repoPath ?? process.cwd());
2516
2654
  }
2517
2655
  function getRepoConfigPath(repoPath) {
2518
- return import_node_path6.default.join(resolveRepoPath(repoPath), CONFIG_FILE_NAME);
2656
+ return import_node_path7.default.join(resolveRepoPath(repoPath), CONFIG_FILE_NAME);
2519
2657
  }
2520
2658
  function loadRepoConfig(repoPath) {
2521
2659
  const resolvedRepoPath = resolveRepoPath(repoPath);
@@ -2530,7 +2668,7 @@ function loadRepoConfig(repoPath) {
2530
2668
  const config = parseConfig(parsed ?? {}, configPath);
2531
2669
  const requiredVersion = config.devrouter?.version;
2532
2670
  if (requiredVersion && !hasWarnedVersionMismatch) {
2533
- const cliVersion = true ? "0.0.45" : "0.0.0-dev";
2671
+ const cliVersion = true ? "0.0.48" : "0.0.0-dev";
2534
2672
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
2535
2673
  hasWarnedVersionMismatch = true;
2536
2674
  process.stderr.write(
@@ -2563,7 +2701,7 @@ function initRepoConfig(repoPath, options = {}) {
2563
2701
  }
2564
2702
  } : {},
2565
2703
  project: {
2566
- name: import_node_path6.default.basename(resolvedRepoPath)
2704
+ name: import_node_path7.default.basename(resolvedRepoPath)
2567
2705
  },
2568
2706
  apps: []
2569
2707
  };
@@ -2792,7 +2930,7 @@ function namespaceHost(host, workspace) {
2792
2930
  return `${base}.${workspace}${suffix}`;
2793
2931
  }
2794
2932
  function applyWorkspace(config, workspace, repoPath) {
2795
- const defaultToken = wsFromBranch(config.project?.name ?? import_node_path6.default.basename(import_node_path6.default.resolve(repoPath))) ?? "app";
2933
+ const defaultToken = wsFromBranch(config.project?.name ?? import_node_path7.default.basename(import_node_path7.default.resolve(repoPath))) ?? "app";
2796
2934
  const substitutionToken = workspace ?? defaultToken;
2797
2935
  const next = structuredClone(config);
2798
2936
  for (const app of next.apps) {
@@ -2874,12 +3012,12 @@ function resolveAppDependencies(config, app) {
2874
3012
  }
2875
3013
  return results;
2876
3014
  }
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;
3015
+ 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
3016
  var init_repo_config = __esm({
2879
3017
  "src/core/repo-config.ts"() {
2880
3018
  "use strict";
2881
3019
  import_node_fs7 = __toESM(require("fs"));
2882
- import_node_path6 = __toESM(require("path"));
3020
+ import_node_path7 = __toESM(require("path"));
2883
3021
  import_yaml2 = __toESM(require("yaml"));
2884
3022
  init_capabilities();
2885
3023
  init_host_routes();
@@ -3060,7 +3198,8 @@ function buildOnboardingPrompt(options = {}) {
3060
3198
  "- 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
3199
  `- 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
3200
  "- 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.",
3201
+ "- 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.",
3202
+ "- Devsy agent readiness: run `devrouter setup --yes --workspace-runtime devsy` once so Devrouter acquires and verifies the pinned agent in its own machine cache. `doctor` reports `ready`, `missing`, `stale`, or `invalid` without network access, and `ensure` fails before the provider queue when readiness is not `ready`. An explicit `DEVSY_AGENT_BINARY` remains authoritative and must match a pinned official asset.",
3064
3203
  "- A managed adapter plus `postCreateCommand` requires `waitFor` exactly `postCreateCommand` or `postStartCommand`; managed selective config preserves lifecycle fields and changes only `runServices`.",
3065
3204
  "- 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
3205
  "- 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.",
@@ -3092,7 +3231,7 @@ function buildOnboardingPrompt(options = {}) {
3092
3231
  "- Warning: Do not run migration/seed until env probe confirms expected DB variables and values.",
3093
3232
  "",
3094
3233
  "Required workflow:",
3095
- "1) Run `devrouter setup --yes --json` for devrouter-owned machine state; use `devrouter doctor --repo <REPO_PATH> --json` to diagnose missing prerequisites without mutation.",
3234
+ "1) Run `devrouter setup --yes --json` for devrouter-owned machine state. When selecting Devsy, use `devrouter setup --yes --workspace-runtime devsy --json` to acquire its verified agent. Use `devrouter doctor --repo <REPO_PATH> --json` to diagnose missing prerequisites without mutation.",
3096
3235
  "2) Run `devrouter repo inspect --repo <REPO_PATH> --json` before editing files.",
3097
3236
  "3) For the supported Node/pnpm/Postgres devcontainer shape, run `devrouter repo devcontainer write --repo <REPO_PATH> --dry-run --json`, review the plan, then run `devrouter repo devcontainer write --repo <REPO_PATH> --yes`.",
3098
3237
  "4) For unsupported shapes or custom existing files, make minimal manual edits and explain the assumptions.",
@@ -3102,6 +3241,7 @@ function buildOnboardingPrompt(options = {}) {
3102
3241
  "",
3103
3242
  "Validation commands to run/report for the devcontainer path:",
3104
3243
  "- devrouter setup --yes --json",
3244
+ "- Devsy runtime setup: devrouter setup --yes --workspace-runtime devsy --json",
3105
3245
  "- devrouter doctor --repo <REPO_PATH> --json",
3106
3246
  "- devrouter repo inspect --repo <REPO_PATH> --json",
3107
3247
  "- devrouter repo devcontainer write --repo <REPO_PATH> --dry-run --json",
@@ -3229,6 +3369,10 @@ var init_ai_prompt = __esm({
3229
3369
  command: "devrouter ensure [path] [--profile <name>] [--open] [--json]",
3230
3370
  purpose: "Canonical startup and proof for an exact primary or linked checkout; atomically publishes routes after readiness."
3231
3371
  },
3372
+ {
3373
+ command: "devrouter profile resolve --repo <path> [--profile <selection>] [--json]",
3374
+ purpose: "Resolve exact profile resources without starting or inspecting a runtime; fail closed on invalid selections or unknown report schemas."
3375
+ },
3232
3376
  {
3233
3377
  command: "devrouter stop [path] [--delete] [--json]",
3234
3378
  purpose: "Stop the exact checkout workspace runtime and routes; --delete explicitly deletes the ownership-proven runtime without removing the checkout."
@@ -3787,7 +3931,7 @@ function readPromptDirectory(promptDirPath) {
3787
3931
  continue;
3788
3932
  }
3789
3933
  const version = normalizeVersion(basename);
3790
- const promptPath = import_node_path7.default.join(promptDirPath, file.name);
3934
+ const promptPath = import_node_path8.default.join(promptDirPath, file.name);
3791
3935
  const prompt = import_node_fs8.default.readFileSync(promptPath, "utf-8").trim();
3792
3936
  releases.push({ version, prompt, promptPath });
3793
3937
  }
@@ -3809,17 +3953,17 @@ function listAvailableUpgradeTargets(currentVersion, releases) {
3809
3953
  }
3810
3954
  function resolvePromptDirectory(explicitPath) {
3811
3955
  if (explicitPath) {
3812
- return import_node_path7.default.resolve(explicitPath);
3956
+ return import_node_path8.default.resolve(explicitPath);
3813
3957
  }
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;
3958
+ const entryFile = process.argv[1] ? import_node_path8.default.resolve(process.argv[1]) : __filename;
3959
+ const entryDir = import_node_path8.default.dirname(entryFile);
3960
+ const resolvedEntryDir = import_node_fs8.default.existsSync(entryFile) ? import_node_path8.default.dirname(import_node_fs8.default.realpathSync(entryFile)) : entryDir;
3817
3961
  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)
3962
+ import_node_path8.default.resolve(resolvedEntryDir, "..", UPGRADE_PROMPTS_DIR),
3963
+ import_node_path8.default.resolve(entryDir, "..", UPGRADE_PROMPTS_DIR),
3964
+ import_node_path8.default.resolve(__dirname, "..", UPGRADE_PROMPTS_DIR),
3965
+ import_node_path8.default.resolve(__dirname, "..", "..", UPGRADE_PROMPTS_DIR),
3966
+ import_node_path8.default.resolve(process.cwd(), UPGRADE_PROMPTS_DIR)
3823
3967
  ];
3824
3968
  const existing = candidates.find((candidate) => import_node_fs8.default.existsSync(candidate));
3825
3969
  return existing ?? candidates[0];
@@ -3837,12 +3981,12 @@ function loadUpgradeCatalog(options = {}) {
3837
3981
  releases
3838
3982
  };
3839
3983
  }
3840
- var import_node_fs8, import_node_path7, SEMVER_RE, UPGRADE_PROMPTS_DIR;
3984
+ var import_node_fs8, import_node_path8, SEMVER_RE, UPGRADE_PROMPTS_DIR;
3841
3985
  var init_upgrade = __esm({
3842
3986
  "src/core/upgrade.ts"() {
3843
3987
  "use strict";
3844
3988
  import_node_fs8 = __toESM(require("fs"));
3845
- import_node_path7 = __toESM(require("path"));
3989
+ import_node_path8 = __toESM(require("path"));
3846
3990
  init_repo_config();
3847
3991
  SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)$/;
3848
3992
  UPGRADE_PROMPTS_DIR = "upgrade-prompts";
@@ -3931,6 +4075,384 @@ var init_upgrade2 = __esm({
3931
4075
  }
3932
4076
  });
3933
4077
 
4078
+ // src/core/devsy-agent.ts
4079
+ function parseVersion(output2) {
4080
+ return output2?.match(
4081
+ /\bv?(\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?)\b/
4082
+ )?.[1];
4083
+ }
4084
+ function devsyAgentRepairSuggestion(inspection) {
4085
+ if (inspection.state === "stale") {
4086
+ return `Install Devsy ${SUPPORTED_DEVSY_VERSION} for a supported host, then run: ${DEVSY_AGENT_SETUP_COMMAND}`;
4087
+ }
4088
+ return inspection.source === "explicit" ? `Fix or unset DEVSY_AGENT_BINARY, then run: ${DEVSY_AGENT_SETUP_COMMAND}` : `Run: ${DEVSY_AGENT_SETUP_COMMAND}`;
4089
+ }
4090
+ function installedVersionOutput() {
4091
+ const result = (0, import_node_child_process4.spawnSync)("devsy", ["--version"], { encoding: "utf-8" });
4092
+ if (result.error || result.status !== 0) return void 0;
4093
+ return [result.stdout, result.stderr].filter(Boolean).join("\n").trim() || void 0;
4094
+ }
4095
+ function nativeAssetName(platform, arch) {
4096
+ if (platform !== "darwin") return void 0;
4097
+ if (arch === "arm64") return "devsy-linux-arm64";
4098
+ if (arch === "x64") return "devsy-linux-amd64";
4099
+ return void 0;
4100
+ }
4101
+ function managedBinaryPath(cacheRoot, asset) {
4102
+ return import_node_path9.default.join(cacheRoot, `v${SUPPORTED_DEVSY_VERSION}`, asset.name);
4103
+ }
4104
+ function hashOpenFile(file) {
4105
+ const digest = (0, import_node_crypto4.createHash)("sha256");
4106
+ const buffer = Buffer.allocUnsafe(1024 * 1024);
4107
+ let position = 0;
4108
+ for (; ; ) {
4109
+ const bytesRead = import_node_fs9.default.readSync(file, buffer, 0, buffer.length, position);
4110
+ if (bytesRead === 0) break;
4111
+ digest.update(buffer.subarray(0, bytesRead));
4112
+ position += bytesRead;
4113
+ }
4114
+ return digest.digest("hex");
4115
+ }
4116
+ function inspectBinary(binaryPath, assets, missingState) {
4117
+ let file;
4118
+ try {
4119
+ file = import_node_fs9.default.openSync(binaryPath, import_node_fs9.default.constants.O_RDONLY | import_node_fs9.default.constants.O_NOFOLLOW);
4120
+ const stat = import_node_fs9.default.fstatSync(file);
4121
+ if (!stat.isFile()) {
4122
+ return { state: "invalid", reason: "the selected source is not a regular file" };
4123
+ }
4124
+ const sizeMatches = assets.filter((asset2) => asset2.size === stat.size);
4125
+ if (sizeMatches.length === 0) {
4126
+ return { state: "invalid", reason: "the selected source has an unexpected size" };
4127
+ }
4128
+ const sha2562 = hashOpenFile(file);
4129
+ const asset = sizeMatches.find((candidate) => candidate.sha256 === sha2562);
4130
+ return asset ? { state: "ready", reason: "the selected source matches the pinned manifest", asset } : { state: "invalid", reason: "the selected source has an unexpected digest" };
4131
+ } catch (error) {
4132
+ const code = error.code;
4133
+ return code === "ENOENT" ? { state: missingState, reason: "the selected source is missing" } : { state: "invalid", reason: "the selected source is not a readable regular file" };
4134
+ } finally {
4135
+ if (file !== void 0) import_node_fs9.default.closeSync(file);
4136
+ }
4137
+ }
4138
+ function resolvedOptions(options) {
4139
+ return {
4140
+ env: options.env ?? process.env,
4141
+ platform: options.platform ?? process.platform,
4142
+ arch: options.arch ?? process.arch,
4143
+ versionOutput: options.versionOutput ?? installedVersionOutput(),
4144
+ cacheRoot: options.cacheRoot ?? import_node_path9.default.join(CACHE_DIR, "devsy", "agents"),
4145
+ lockPath: options.lockPath ?? DEVSY_AGENT_CACHE_LOCK,
4146
+ assets: options.assets ?? DEVSY_AGENT_ASSETS,
4147
+ nativeAssetName: options.nativeAssetName
4148
+ };
4149
+ }
4150
+ function inspectDevsyAgent(options = {}) {
4151
+ const resolved = resolvedOptions(options);
4152
+ const installedVersion = parseVersion(resolved.versionOutput);
4153
+ const explicitPath = resolved.env.DEVSY_AGENT_BINARY?.trim();
4154
+ const source = explicitPath ? "explicit" : "managed";
4155
+ if (installedVersion !== SUPPORTED_DEVSY_VERSION) {
4156
+ return {
4157
+ state: "stale",
4158
+ source,
4159
+ reason: installedVersion ? `installed Devsy ${installedVersion} is not supported by this Devrouter release` : "the supported Devsy version is not available",
4160
+ installedVersion
4161
+ };
4162
+ }
4163
+ if (explicitPath) {
4164
+ return {
4165
+ ...inspectBinary(explicitPath, resolved.assets, "invalid"),
4166
+ source,
4167
+ binaryPath: explicitPath,
4168
+ installedVersion
4169
+ };
4170
+ }
4171
+ const expectedName = resolved.nativeAssetName ?? nativeAssetName(resolved.platform, resolved.arch);
4172
+ const asset = resolved.assets.find((candidate) => candidate.name === expectedName);
4173
+ if (!asset) {
4174
+ return {
4175
+ state: "stale",
4176
+ source,
4177
+ reason: `this Devrouter release has no pinned Devsy agent for ${resolved.platform}/${resolved.arch}`,
4178
+ installedVersion
4179
+ };
4180
+ }
4181
+ const binaryPath = managedBinaryPath(resolved.cacheRoot, asset);
4182
+ return {
4183
+ ...inspectBinary(binaryPath, [asset], "missing"),
4184
+ source,
4185
+ binaryPath,
4186
+ asset,
4187
+ installedVersion
4188
+ };
4189
+ }
4190
+ async function writeAll(handle, chunk, position) {
4191
+ let offset = 0;
4192
+ while (offset < chunk.byteLength) {
4193
+ const { bytesWritten } = await handle.write(
4194
+ chunk,
4195
+ offset,
4196
+ chunk.byteLength - offset,
4197
+ position + offset
4198
+ );
4199
+ if (bytesWritten === 0) throw new Error("Devsy agent download stopped making progress");
4200
+ offset += bytesWritten;
4201
+ }
4202
+ }
4203
+ function appendBoundedTail(current, chunk) {
4204
+ if (chunk.length >= GITHUB_CLI_STDERR_TAIL_BYTES) {
4205
+ return Buffer.from(chunk.subarray(chunk.length - GITHUB_CLI_STDERR_TAIL_BYTES));
4206
+ }
4207
+ const combined = Buffer.concat([current, chunk]);
4208
+ return combined.length > GITHUB_CLI_STDERR_TAIL_BYTES ? combined.subarray(combined.length - GITHUB_CLI_STDERR_TAIL_BYTES) : combined;
4209
+ }
4210
+ function errorMessage(error) {
4211
+ return error instanceof Error ? error.message : String(error);
4212
+ }
4213
+ async function downloadWithGitHubCli(asset, writeChunk) {
4214
+ const child = (0, import_node_child_process4.spawn)(
4215
+ "gh",
4216
+ [
4217
+ "api",
4218
+ `repos/devsy-org/devsy/releases/assets/${asset.githubAssetId}`,
4219
+ "-H",
4220
+ "Accept: application/octet-stream"
4221
+ ],
4222
+ { stdio: ["ignore", "pipe", "pipe"] }
4223
+ );
4224
+ if (!child.stdout || !child.stderr) {
4225
+ child.kill();
4226
+ throw new Error("GitHub CLI fallback did not create output pipes");
4227
+ }
4228
+ let stderrTail = Buffer.alloc(0);
4229
+ child.stderr.on("data", (chunk) => {
4230
+ const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4231
+ stderrTail = appendBoundedTail(stderrTail, value);
4232
+ });
4233
+ let spawnError;
4234
+ const completion = new Promise((resolve) => {
4235
+ child.once("error", (error) => {
4236
+ spawnError = error;
4237
+ });
4238
+ child.once("close", (status) => {
4239
+ resolve(status);
4240
+ });
4241
+ });
4242
+ try {
4243
+ for await (const chunk of child.stdout) {
4244
+ await writeChunk(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
4245
+ }
4246
+ const status = await completion;
4247
+ if (spawnError) throw spawnError;
4248
+ if (status !== 0) {
4249
+ const details = stderrTail.toString("utf-8").trim();
4250
+ throw new Error(
4251
+ `gh api exited with status ${status ?? "unknown"}${details ? `: ${details}` : ""}`
4252
+ );
4253
+ }
4254
+ } catch (error) {
4255
+ if (child.exitCode === null) child.kill();
4256
+ await completion.catch(() => void 0);
4257
+ throw error;
4258
+ }
4259
+ }
4260
+ function fsyncDirectory2(directory) {
4261
+ const handle = import_node_fs9.default.openSync(directory, "r");
4262
+ try {
4263
+ import_node_fs9.default.fsyncSync(handle);
4264
+ } finally {
4265
+ import_node_fs9.default.closeSync(handle);
4266
+ }
4267
+ }
4268
+ async function downloadAndPublish(asset, binaryPath, fetcher, githubCliDownloader) {
4269
+ const directory = import_node_path9.default.dirname(binaryPath);
4270
+ import_node_fs9.default.mkdirSync(directory, { recursive: true, mode: 448 });
4271
+ const temporaryPath = import_node_path9.default.join(
4272
+ directory,
4273
+ `.${import_node_path9.default.basename(binaryPath)}.${process.pid}.${(0, import_node_crypto4.randomUUID)()}.tmp`
4274
+ );
4275
+ let handle;
4276
+ try {
4277
+ let response;
4278
+ let directFailure;
4279
+ try {
4280
+ response = await fetcher(asset.url);
4281
+ } catch (error) {
4282
+ directFailure = error;
4283
+ }
4284
+ handle = await import_node_fs9.default.promises.open(temporaryPath, "wx", 384);
4285
+ const digest = (0, import_node_crypto4.createHash)("sha256");
4286
+ let size = 0;
4287
+ const writeChunk = async (chunk) => {
4288
+ size += chunk.byteLength;
4289
+ if (size > asset.size) {
4290
+ throw new Error("Devsy agent download exceeded the pinned size");
4291
+ }
4292
+ digest.update(chunk);
4293
+ await writeAll(handle, chunk, size - chunk.byteLength);
4294
+ };
4295
+ let transport;
4296
+ if (response) {
4297
+ if (!response.ok) {
4298
+ throw new Error(`Devsy agent download failed with HTTP ${response.status}`);
4299
+ }
4300
+ if (!response.body) throw new Error("Devsy agent download returned no response body");
4301
+ const reader = response.body.getReader();
4302
+ try {
4303
+ for (; ; ) {
4304
+ const { done, value } = await reader.read();
4305
+ if (done) break;
4306
+ await writeChunk(value);
4307
+ }
4308
+ } catch (error) {
4309
+ await reader.cancel().catch(() => void 0);
4310
+ throw error;
4311
+ }
4312
+ transport = "https";
4313
+ } else {
4314
+ try {
4315
+ await githubCliDownloader(asset, writeChunk);
4316
+ } catch (fallbackError) {
4317
+ throw new Error(
4318
+ `Devsy agent direct HTTPS download failed: ${errorMessage(directFailure)}; GitHub CLI fallback failed: ${errorMessage(fallbackError)}. Restore direct GitHub release access or install and authenticate gh.`
4319
+ );
4320
+ }
4321
+ transport = "github-cli";
4322
+ }
4323
+ if (size !== asset.size) throw new Error("Devsy agent download has an unexpected size");
4324
+ if (digest.digest("hex") !== asset.sha256) {
4325
+ throw new Error("Devsy agent download has an unexpected digest");
4326
+ }
4327
+ await handle.sync();
4328
+ await handle.chmod(448);
4329
+ await handle.sync();
4330
+ await handle.close();
4331
+ handle = void 0;
4332
+ await import_node_fs9.default.promises.rename(temporaryPath, binaryPath);
4333
+ fsyncDirectory2(directory);
4334
+ return transport;
4335
+ } finally {
4336
+ if (handle) await handle.close();
4337
+ await import_node_fs9.default.promises.rm(temporaryPath, { force: true });
4338
+ }
4339
+ }
4340
+ async function prepareDevsyAgent(options = {}) {
4341
+ const resolved = resolvedOptions(options);
4342
+ const inspectOptions = resolved;
4343
+ const before = inspectDevsyAgent(inspectOptions);
4344
+ if (before.state === "ready" && before.binaryPath && before.asset) {
4345
+ return {
4346
+ binaryPath: before.binaryPath,
4347
+ source: before.source,
4348
+ asset: before.asset,
4349
+ changed: false,
4350
+ transport: "existing"
4351
+ };
4352
+ }
4353
+ if (before.source === "explicit" || before.state === "stale" || !before.binaryPath || !before.asset) {
4354
+ throw new DevsyAgentReadinessError(before);
4355
+ }
4356
+ const lock = options.withLock ?? ((operation) => {
4357
+ import_node_fs9.default.mkdirSync(import_node_path9.default.dirname(resolved.lockPath), { recursive: true });
4358
+ return withFileLock(
4359
+ resolved.lockPath,
4360
+ {
4361
+ activity: "Devsy agent acquisition",
4362
+ waitMs: DEVSY_AGENT_LOCK_WAIT_MS,
4363
+ fair: true,
4364
+ onWait: createStderrWaitReporter("Devsy agent acquisition", "the managed cache")
4365
+ },
4366
+ operation
4367
+ );
4368
+ });
4369
+ return lock(async () => {
4370
+ const current = inspectDevsyAgent(inspectOptions);
4371
+ if (current.state === "ready" && current.binaryPath && current.asset) {
4372
+ return {
4373
+ binaryPath: current.binaryPath,
4374
+ source: current.source,
4375
+ asset: current.asset,
4376
+ changed: false,
4377
+ transport: "existing"
4378
+ };
4379
+ }
4380
+ if (current.source === "explicit" || current.state === "stale" || !current.binaryPath || !current.asset) {
4381
+ throw new DevsyAgentReadinessError(current);
4382
+ }
4383
+ const transport = await downloadAndPublish(
4384
+ current.asset,
4385
+ current.binaryPath,
4386
+ options.fetcher ?? fetch,
4387
+ options.githubCliDownloader ?? downloadWithGitHubCli
4388
+ );
4389
+ const published = inspectDevsyAgent(inspectOptions);
4390
+ if (published.state !== "ready" || !published.binaryPath || !published.asset) {
4391
+ throw new DevsyAgentReadinessError(published);
4392
+ }
4393
+ return {
4394
+ binaryPath: published.binaryPath,
4395
+ source: published.source,
4396
+ asset: published.asset,
4397
+ changed: true,
4398
+ transport
4399
+ };
4400
+ });
4401
+ }
4402
+ function requireReadyDevsyAgent(options = {}) {
4403
+ const inspection = inspectDevsyAgent(options);
4404
+ if (inspection.state !== "ready" || !inspection.binaryPath || !inspection.asset) {
4405
+ throw new DevsyAgentReadinessError(inspection);
4406
+ }
4407
+ return {
4408
+ binaryPath: inspection.binaryPath,
4409
+ source: inspection.source,
4410
+ asset: inspection.asset,
4411
+ changed: false,
4412
+ transport: "existing"
4413
+ };
4414
+ }
4415
+ var import_node_child_process4, import_node_crypto4, import_node_fs9, import_node_path9, SUPPORTED_DEVSY_VERSION, DEVSY_AGENT_SETUP_COMMAND, DEVSY_AGENT_ASSETS, DevsyAgentReadinessError, DEVSY_AGENT_CACHE_LOCK, DEVSY_AGENT_LOCK_WAIT_MS, GITHUB_CLI_STDERR_TAIL_BYTES;
4416
+ var init_devsy_agent = __esm({
4417
+ "src/core/devsy-agent.ts"() {
4418
+ "use strict";
4419
+ import_node_child_process4 = require("child_process");
4420
+ import_node_crypto4 = require("crypto");
4421
+ import_node_fs9 = __toESM(require("fs"));
4422
+ import_node_path9 = __toESM(require("path"));
4423
+ init_file_lock();
4424
+ init_router();
4425
+ SUPPORTED_DEVSY_VERSION = "1.16.2";
4426
+ DEVSY_AGENT_SETUP_COMMAND = "devrouter setup --yes --workspace-runtime devsy";
4427
+ DEVSY_AGENT_ASSETS = [
4428
+ {
4429
+ githubAssetId: 529830010,
4430
+ name: "devsy-linux-arm64",
4431
+ size: 124518562,
4432
+ sha256: "31060b96486b5398f2aa3ee0875b2555782a2db0954a799d387be38ed4b4990d",
4433
+ url: "https://github.com/devsy-org/devsy/releases/download/v1.16.2/devsy-linux-arm64"
4434
+ },
4435
+ {
4436
+ githubAssetId: 529830011,
4437
+ name: "devsy-linux-amd64",
4438
+ size: 133505186,
4439
+ sha256: "4983c52a3536c5a91d1b5f356a1c3428778ebf3f896d9897f60bce3978abc839",
4440
+ url: "https://github.com/devsy-org/devsy/releases/download/v1.16.2/devsy-linux-amd64"
4441
+ }
4442
+ ];
4443
+ DevsyAgentReadinessError = class extends Error {
4444
+ constructor(inspection) {
4445
+ super(`Devsy agent is ${inspection.state}: ${inspection.reason}`);
4446
+ this.inspection = inspection;
4447
+ this.name = "DevsyAgentReadinessError";
4448
+ }
4449
+ };
4450
+ DEVSY_AGENT_CACHE_LOCK = import_node_path9.default.join(DEVROUTER_HOME, "devsy-agent-cache.lock");
4451
+ DEVSY_AGENT_LOCK_WAIT_MS = 18e5;
4452
+ GITHUB_CLI_STDERR_TAIL_BYTES = 4096;
4453
+ }
4454
+ });
4455
+
3934
4456
  // src/core/docker.ts
3935
4457
  async function getDockerodeConstructor() {
3936
4458
  if (!dockerodeConstructorPromise) {
@@ -3942,7 +4464,7 @@ async function getDockerodeConstructor() {
3942
4464
  return dockerodeConstructorPromise;
3943
4465
  }
3944
4466
  function runDockerContextCommand(args) {
3945
- const result = (0, import_node_child_process4.spawnSync)("docker", ["context", ...args], {
4467
+ const result = (0, import_node_child_process5.spawnSync)("docker", ["context", ...args], {
3946
4468
  encoding: "utf-8"
3947
4469
  });
3948
4470
  if (result.status !== 0) {
@@ -3955,7 +4477,7 @@ function getCurrentDockerContext() {
3955
4477
  return runDockerContextCommand(["show"]);
3956
4478
  }
3957
4479
  function getDockerHostFromContext(context) {
3958
- const result = (0, import_node_child_process4.spawnSync)(
4480
+ const result = (0, import_node_child_process5.spawnSync)(
3959
4481
  "docker",
3960
4482
  ["context", "inspect", context, "--format", "{{ .Endpoints.docker.Host }}"],
3961
4483
  { encoding: "utf-8" }
@@ -4023,11 +4545,11 @@ async function networkExists(name) {
4023
4545
  return false;
4024
4546
  }
4025
4547
  }
4026
- var import_node_child_process4, dockerodeConstructorPromise;
4548
+ var import_node_child_process5, dockerodeConstructorPromise;
4027
4549
  var init_docker = __esm({
4028
4550
  "src/core/docker.ts"() {
4029
4551
  "use strict";
4030
- import_node_child_process4 = require("child_process");
4552
+ import_node_child_process5 = require("child_process");
4031
4553
  dockerodeConstructorPromise = null;
4032
4554
  }
4033
4555
  });
@@ -4054,11 +4576,11 @@ function parseDevcontainerConfig(contents, sourcePath) {
4054
4576
  return parsed;
4055
4577
  }
4056
4578
  function readDevcontainerConfig(repoPath) {
4057
- const sourcePath = import_node_path8.default.join(repoPath, ".devcontainer", "devcontainer.json");
4058
- if (!import_node_fs9.default.existsSync(sourcePath) || !import_node_fs9.default.lstatSync(sourcePath).isFile()) {
4579
+ const sourcePath = import_node_path10.default.join(repoPath, ".devcontainer", "devcontainer.json");
4580
+ if (!import_node_fs10.default.existsSync(sourcePath) || !import_node_fs10.default.lstatSync(sourcePath).isFile()) {
4059
4581
  throw new Error(`Managed Dev Container source config does not exist: ${sourcePath}`);
4060
4582
  }
4061
- const sourceContents = import_node_fs9.default.readFileSync(sourcePath, "utf-8");
4583
+ const sourceContents = import_node_fs10.default.readFileSync(sourcePath, "utf-8");
4062
4584
  return {
4063
4585
  sourcePath,
4064
4586
  sourceContents,
@@ -4074,12 +4596,12 @@ function assertManagedDevcontainerLifecycle(repoPath) {
4074
4596
  `Managed Dev Container source '${sourcePath}' defines postCreateCommand, but waitFor ${actual}. Set waitFor to 'postCreateCommand' or 'postStartCommand' before retrying devrouter ensure.`
4075
4597
  );
4076
4598
  }
4077
- var import_node_fs9, import_node_path8, import_jsonc_parser;
4599
+ var import_node_fs10, import_node_path10, import_jsonc_parser;
4078
4600
  var init_devcontainer_config = __esm({
4079
4601
  "src/core/devcontainer-config.ts"() {
4080
4602
  "use strict";
4081
- import_node_fs9 = __toESM(require("fs"));
4082
- import_node_path8 = __toESM(require("path"));
4603
+ import_node_fs10 = __toESM(require("fs"));
4604
+ import_node_path10 = __toESM(require("path"));
4083
4605
  import_jsonc_parser = require("jsonc-parser");
4084
4606
  }
4085
4607
  });
@@ -4089,7 +4611,7 @@ function renderDeliveryScript(targetPath) {
4089
4611
  if (!/^\/tmp\/devrouter\/bin\/[a-zA-Z0-9._-]+$/.test(targetPath)) {
4090
4612
  throw new Error(`Unsafe managed runtime delivery path: ${targetPath}`);
4091
4613
  }
4092
- const temporaryPrefix = import_node_path9.default.posix.basename(targetPath);
4614
+ const temporaryPrefix = import_node_path11.default.posix.basename(targetPath);
4093
4615
  return `set -eu
4094
4616
  umask 077
4095
4617
  runtime_root=/tmp/devrouter
@@ -4113,7 +4635,7 @@ function commandFailure(result) {
4113
4635
  return [result.error?.message, result.stderr, result.stdout].filter(Boolean).map((value) => String(value).trim()).filter(Boolean).join("\n");
4114
4636
  }
4115
4637
  function deliverRuntimeFile(containerId, targetPath, contents, label) {
4116
- const result = (0, import_node_child_process5.spawnSync)(
4638
+ const result = (0, import_node_child_process6.spawnSync)(
4117
4639
  "docker",
4118
4640
  ["exec", "-i", containerId, "sh", "-c", renderDeliveryScript(targetPath)],
4119
4641
  { input: contents, encoding: "utf-8" }
@@ -4124,32 +4646,32 @@ function deliverRuntimeFile(containerId, targetPath, contents, label) {
4124
4646
  }
4125
4647
  }
4126
4648
  function readRegularFileBytes(filePath) {
4127
- if (!import_node_fs10.default.existsSync(filePath) || !import_node_fs10.default.lstatSync(filePath).isFile()) return void 0;
4128
- return import_node_fs10.default.readFileSync(filePath);
4649
+ if (!import_node_fs11.default.existsSync(filePath) || !import_node_fs11.default.lstatSync(filePath).isFile()) return void 0;
4650
+ return import_node_fs11.default.readFileSync(filePath);
4129
4651
  }
4130
4652
  function readRegularFile(filePath) {
4131
4653
  return readRegularFileBytes(filePath)?.toString("utf-8");
4132
4654
  }
4133
4655
  function adapterFingerprint(adapter) {
4134
- return (0, import_node_crypto4.createHash)("sha256").update(adapter).digest("hex");
4656
+ return (0, import_node_crypto5.createHash)("sha256").update(adapter).digest("hex");
4135
4657
  }
4136
4658
  function resolveProcessHelperPath() {
4137
4659
  const candidates = [
4138
- import_node_path9.default.resolve(__dirname, "..", "bin", "devrouter-process"),
4139
- import_node_path9.default.resolve(__dirname, "..", "..", "bin", "devrouter-process")
4660
+ import_node_path11.default.resolve(__dirname, "..", "bin", "devrouter-process"),
4661
+ import_node_path11.default.resolve(__dirname, "..", "..", "bin", "devrouter-process")
4140
4662
  ];
4141
- const helperPath = candidates.find((candidate) => import_node_fs10.default.existsSync(candidate));
4663
+ const helperPath = candidates.find((candidate) => import_node_fs11.default.existsSync(candidate));
4142
4664
  if (!helperPath) {
4143
4665
  throw new Error("Could not locate the packaged devrouter-process helper.");
4144
4666
  }
4145
4667
  return helperPath;
4146
4668
  }
4147
4669
  function resolveManagedPostStartPlan(repoPath) {
4148
- const adapterPath = import_node_path9.default.join(repoPath, MANAGED_ADAPTER_PATH);
4670
+ const adapterPath = import_node_path11.default.join(repoPath, MANAGED_ADAPTER_PATH);
4149
4671
  const adapterBytes = readRegularFileBytes(adapterPath);
4150
4672
  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");
4673
+ const dockerfilePath = import_node_path11.default.join(repoPath, ".devcontainer", "Dockerfile");
4674
+ const devcontainerPath = import_node_path11.default.join(repoPath, ".devcontainer", "devcontainer.json");
4153
4675
  const dockerfile = readRegularFile(dockerfilePath) ?? "";
4154
4676
  const devcontainer = readRegularFile(devcontainerPath) ?? "";
4155
4677
  const devrouterPattern = [adapterText, dockerfile, devcontainer].filter((value) => value !== void 0).some(
@@ -4190,7 +4712,7 @@ function resolveManagedPostStartPlan(repoPath) {
4190
4712
  }
4191
4713
  function runManagedPostStart(options) {
4192
4714
  if (options.plan.kind !== "runtime") return;
4193
- const helper = import_node_fs10.default.readFileSync(resolveProcessHelperPath());
4715
+ const helper = import_node_fs11.default.readFileSync(resolveProcessHelperPath());
4194
4716
  deliverRuntimeFile(
4195
4717
  options.container.id,
4196
4718
  RUNTIME_HELPER_PATH,
@@ -4204,7 +4726,7 @@ function runManagedPostStart(options) {
4204
4726
  options.plan.adapterContents,
4205
4727
  "the managed post-start adapter"
4206
4728
  );
4207
- const started = (0, import_node_child_process5.spawnSync)(
4729
+ const started = (0, import_node_child_process6.spawnSync)(
4208
4730
  "docker",
4209
4731
  [
4210
4732
  "exec",
@@ -4242,7 +4764,7 @@ function assertSafeProcessName(name) {
4242
4764
  }
4243
4765
  function runManagedProcessAction(options) {
4244
4766
  assertSafeProcessName(options.name);
4245
- const result = (0, import_node_child_process5.spawnSync)(
4767
+ const result = (0, import_node_child_process6.spawnSync)(
4246
4768
  "docker",
4247
4769
  [
4248
4770
  "exec",
@@ -4278,14 +4800,14 @@ function runManagedProcessAction(options) {
4278
4800
  }
4279
4801
  return "drifted";
4280
4802
  }
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;
4803
+ var import_node_child_process6, import_node_crypto5, import_node_fs11, import_node_path11, MANAGED_MARKER, MANAGED_ADAPTER_PATH, RUNTIME_HELPER_PATH, ADAPTER_WRAPPER;
4282
4804
  var init_managed_post_start = __esm({
4283
4805
  "src/core/managed-post-start.ts"() {
4284
4806
  "use strict";
4285
- import_node_child_process5 = require("child_process");
4286
- import_node_crypto4 = require("crypto");
4287
- import_node_fs10 = __toESM(require("fs"));
4288
- import_node_path9 = __toESM(require("path"));
4807
+ import_node_child_process6 = require("child_process");
4808
+ import_node_crypto5 = require("crypto");
4809
+ import_node_fs11 = __toESM(require("fs"));
4810
+ import_node_path11 = __toESM(require("path"));
4289
4811
  init_devcontainer_config();
4290
4812
  MANAGED_MARKER = "devrouter:managed devcontainer";
4291
4813
  MANAGED_ADAPTER_PATH = ".devcontainer/post-start.sh";
@@ -4352,7 +4874,7 @@ function isLoopbackHost(host) {
4352
4874
  return host === "127.0.0.1" || host === "localhost" || host === "host.docker.internal";
4353
4875
  }
4354
4876
  function inspectCompose(composeFile, _workspace) {
4355
- if (!import_node_fs11.default.existsSync(composeFile)) {
4877
+ if (!import_node_fs12.default.existsSync(composeFile)) {
4356
4878
  return {
4357
4879
  aliases: [],
4358
4880
  publishedPorts: [],
@@ -4361,7 +4883,7 @@ function inspectCompose(composeFile, _workspace) {
4361
4883
  };
4362
4884
  }
4363
4885
  try {
4364
- const raw = import_node_fs11.default.readFileSync(composeFile, "utf-8");
4886
+ const raw = import_node_fs12.default.readFileSync(composeFile, "utf-8");
4365
4887
  const parsed = import_yaml3.default.parse(raw);
4366
4888
  const root = asRecord(parsed);
4367
4889
  const services = asRecord(root?.services);
@@ -4418,11 +4940,11 @@ function routedProxyApps(config) {
4418
4940
  );
4419
4941
  }
4420
4942
  function buildDevcontainerChecks(repoPath, config, workspace) {
4421
- const devcontainerDir = import_node_path10.default.join(repoPath, ".devcontainer");
4422
- if (!import_node_fs11.default.existsSync(devcontainerDir)) {
4943
+ const devcontainerDir = import_node_path12.default.join(repoPath, ".devcontainer");
4944
+ if (!import_node_fs12.default.existsSync(devcontainerDir)) {
4423
4945
  return [];
4424
4946
  }
4425
- const compose = inspectCompose(import_node_path10.default.join(devcontainerDir, "docker-compose.yml"), workspace);
4947
+ const compose = inspectCompose(import_node_path12.default.join(devcontainerDir, "docker-compose.yml"), workspace);
4426
4948
  const checks = [];
4427
4949
  if (compose.parseError) {
4428
4950
  checks.push({
@@ -4449,9 +4971,9 @@ function buildDevcontainerChecks(repoPath, config, workspace) {
4449
4971
  details: compose.publishedPorts.length > 0 ? compose.publishedPorts.join(", ") : void 0,
4450
4972
  suggestion: compose.parseError || compose.publishedPorts.length > 0 ? "Remove published ports and route services through devnet aliases with devrouter proxy apps." : void 0
4451
4973
  });
4452
- const dockerfilePath = import_node_path10.default.join(devcontainerDir, "Dockerfile");
4453
- const dockerfileExists = import_node_fs11.default.existsSync(dockerfilePath);
4454
- const dockerfile = dockerfileExists ? import_node_fs11.default.readFileSync(dockerfilePath, "utf-8") : "";
4974
+ const dockerfilePath = import_node_path12.default.join(devcontainerDir, "Dockerfile");
4975
+ const dockerfileExists = import_node_fs12.default.existsSync(dockerfilePath);
4976
+ const dockerfile = dockerfileExists ? import_node_fs12.default.readFileSync(dockerfilePath, "utf-8") : "";
4455
4977
  const devrouterArtifacts = ["@devrouter/cli", "devrouter-process"].filter(
4456
4978
  (artifact) => dockerfile.includes(artifact)
4457
4979
  );
@@ -4506,12 +5028,12 @@ function buildDevcontainerChecks(repoPath, config, workspace) {
4506
5028
  });
4507
5029
  return checks;
4508
5030
  }
4509
- var import_node_fs11, import_node_path10, import_yaml3;
5031
+ var import_node_fs12, import_node_path12, import_yaml3;
4510
5032
  var init_devcontainer_diagnostics = __esm({
4511
5033
  "src/core/devcontainer-diagnostics.ts"() {
4512
5034
  "use strict";
4513
- import_node_fs11 = __toESM(require("fs"));
4514
- import_node_path10 = __toESM(require("path"));
5035
+ import_node_fs12 = __toESM(require("fs"));
5036
+ import_node_path12 = __toESM(require("path"));
4515
5037
  import_yaml3 = __toESM(require("yaml"));
4516
5038
  init_managed_post_start();
4517
5039
  }
@@ -4519,18 +5041,18 @@ var init_devcontainer_diagnostics = __esm({
4519
5041
 
4520
5042
  // src/core/paths.ts
4521
5043
  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)) {
5044
+ const resolvedRoot = import_node_path13.default.resolve(repoRoot);
5045
+ const resolved = import_node_path13.default.resolve(repoRoot, filePath);
5046
+ if (resolved !== resolvedRoot && !resolved.startsWith(resolvedRoot + import_node_path13.default.sep)) {
4525
5047
  throw new Error(`${label} path '${filePath}' escapes the repository root.`);
4526
5048
  }
4527
5049
  return resolved;
4528
5050
  }
4529
- var import_node_path11;
5051
+ var import_node_path13;
4530
5052
  var init_paths = __esm({
4531
5053
  "src/core/paths.ts"() {
4532
5054
  "use strict";
4533
- import_node_path11 = __toESM(require("path"));
5055
+ import_node_path13 = __toESM(require("path"));
4534
5056
  }
4535
5057
  });
4536
5058
 
@@ -4615,11 +5137,11 @@ function reconcileRouteRunConflict(repoPath, app) {
4615
5137
  }
4616
5138
  }
4617
5139
  }
4618
- var import_node_fs12;
5140
+ var import_node_fs13;
4619
5141
  var init_route_state = __esm({
4620
5142
  "src/core/route-state.ts"() {
4621
5143
  "use strict";
4622
- import_node_fs12 = __toESM(require("fs"));
5144
+ import_node_fs13 = __toESM(require("fs"));
4623
5145
  init_host_routes();
4624
5146
  init_workspace();
4625
5147
  }
@@ -4785,10 +5307,10 @@ var init_routes = __esm({
4785
5307
 
4786
5308
  // src/core/managed-runtime-state.ts
4787
5309
  function stateKey(repoPath, workspace) {
4788
- return (0, import_node_crypto5.createHash)("sha256").update(`${repoPath}\0${workspace ?? ""}`, "utf-8").digest("hex");
5310
+ return (0, import_node_crypto6.createHash)("sha256").update(`${repoPath}\0${workspace ?? ""}`, "utf-8").digest("hex");
4789
5311
  }
4790
5312
  function managedRuntimeStatePath(repoPath, workspace) {
4791
- return import_node_path12.default.join(DEVROUTER_HOME, "managed-runtime", `${stateKey(repoPath, workspace)}.json`);
5313
+ return import_node_path14.default.join(DEVROUTER_HOME, "managed-runtime", `${stateKey(repoPath, workspace)}.json`);
4792
5314
  }
4793
5315
  function isStringArray(value) {
4794
5316
  return Array.isArray(value) && value.every((item) => typeof item === "string") && new Set(value).size === value.length;
@@ -4851,10 +5373,10 @@ function validateState(value, repoPath, workspace) {
4851
5373
  }
4852
5374
  function readManagedRuntimeState(repoPath, workspace) {
4853
5375
  const statePath = managedRuntimeStatePath(repoPath, workspace);
4854
- if (!import_node_fs13.default.existsSync(statePath)) return void 0;
5376
+ if (!import_node_fs14.default.existsSync(statePath)) return void 0;
4855
5377
  let parsed;
4856
5378
  try {
4857
- parsed = JSON.parse(import_node_fs13.default.readFileSync(statePath, "utf-8"));
5379
+ parsed = JSON.parse(import_node_fs14.default.readFileSync(statePath, "utf-8"));
4858
5380
  } catch (error) {
4859
5381
  throw new Error(`Could not parse managed runtime state: ${String(error)}`);
4860
5382
  }
@@ -4876,13 +5398,13 @@ function markManagedRuntimeDegraded(state, transitionPhase) {
4876
5398
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
4877
5399
  });
4878
5400
  }
4879
- var import_node_crypto5, import_node_fs13, import_node_path12;
5401
+ var import_node_crypto6, import_node_fs14, import_node_path14;
4880
5402
  var init_managed_runtime_state = __esm({
4881
5403
  "src/core/managed-runtime-state.ts"() {
4882
5404
  "use strict";
4883
- import_node_crypto5 = require("crypto");
4884
- import_node_fs13 = __toESM(require("fs"));
4885
- import_node_path12 = __toESM(require("path"));
5405
+ import_node_crypto6 = require("crypto");
5406
+ import_node_fs14 = __toESM(require("fs"));
5407
+ import_node_path14 = __toESM(require("path"));
4886
5408
  init_atomic_file();
4887
5409
  init_router();
4888
5410
  }
@@ -4933,14 +5455,14 @@ function resolveComposeFiles(source, repoPath, linked) {
4933
5455
  "managedRuntime requires .devcontainer/devcontainer.json to define dockerComposeFile."
4934
5456
  );
4935
5457
  }
4936
- const directory = import_node_path13.default.join(repoPath, ".devcontainer");
5458
+ const directory = import_node_path15.default.join(repoPath, ".devcontainer");
4937
5459
  const files = references.map((reference) => {
4938
5460
  const resolved2 = assertPathWithinRepo(
4939
5461
  resolveComposeReference(reference, linked),
4940
5462
  directory,
4941
5463
  "dockerComposeFile"
4942
5464
  );
4943
- if (!import_node_fs14.default.existsSync(resolved2) || !import_node_fs14.default.lstatSync(resolved2).isFile()) {
5465
+ if (!import_node_fs15.default.existsSync(resolved2) || !import_node_fs15.default.lstatSync(resolved2).isFile()) {
4944
5466
  throw new Error(`Managed Dev Container compose file does not exist: ${resolved2}`);
4945
5467
  }
4946
5468
  return resolved2;
@@ -4948,7 +5470,7 @@ function resolveComposeFiles(source, repoPath, linked) {
4948
5470
  for (const file of files) {
4949
5471
  let parsed;
4950
5472
  try {
4951
- parsed = import_yaml4.default.parse(import_node_fs14.default.readFileSync(file, "utf-8"));
5473
+ parsed = import_yaml4.default.parse(import_node_fs15.default.readFileSync(file, "utf-8"));
4952
5474
  } catch (error) {
4953
5475
  throw new Error(
4954
5476
  `Could not parse managed Dev Container compose file '${file}': ${String(error)}`
@@ -4959,7 +5481,7 @@ function resolveComposeFiles(source, repoPath, linked) {
4959
5481
  }
4960
5482
  }
4961
5483
  const fileArgs = files.flatMap((file) => ["-f", file]);
4962
- const resolved = (0, import_node_child_process6.spawnSync)(
5484
+ const resolved = (0, import_node_child_process7.spawnSync)(
4963
5485
  "docker",
4964
5486
  [
4965
5487
  "compose",
@@ -4985,7 +5507,7 @@ function resolveComposeFiles(source, repoPath, linked) {
4985
5507
  return { directory, files, services: services.sort() };
4986
5508
  }
4987
5509
  function assertIgnoredGeneratedPath(repoPath, generatedPath) {
4988
- const ignored = (0, import_node_child_process6.spawnSync)(
5510
+ const ignored = (0, import_node_child_process7.spawnSync)(
4989
5511
  "git",
4990
5512
  ["-C", repoPath, "check-ignore", "--quiet", "--no-index", "--", generatedPath],
4991
5513
  { encoding: "utf-8" }
@@ -4997,12 +5519,12 @@ function assertIgnoredGeneratedPath(repoPath, generatedPath) {
4997
5519
  }
4998
5520
  }
4999
5521
  function assertGeneratedPathOwnership(generatedPath) {
5000
- if (!import_node_fs14.default.existsSync(generatedPath)) return;
5001
- const stat = import_node_fs14.default.lstatSync(generatedPath);
5522
+ if (!import_node_fs15.default.existsSync(generatedPath)) return;
5523
+ const stat = import_node_fs15.default.lstatSync(generatedPath);
5002
5524
  if (!stat.isFile() || stat.isSymbolicLink()) {
5003
5525
  throw new Error(`Managed Dev Container path '${generatedPath}' is not a regular file.`);
5004
5526
  }
5005
- const firstLine2 = import_node_fs14.default.readFileSync(generatedPath, "utf-8").split(/\r?\n/, 1)[0];
5527
+ const firstLine2 = import_node_fs15.default.readFileSync(generatedPath, "utf-8").split(/\r?\n/, 1)[0];
5006
5528
  if (firstLine2 !== MANAGED_DEVCONTAINER_MARKER) {
5007
5529
  throw new Error(
5008
5530
  `Managed Dev Container path '${generatedPath}' exists without the devrouter ownership marker.`
@@ -5013,7 +5535,7 @@ function isWildcard(values) {
5013
5535
  return values?.length === 1 && values[0] === "*";
5014
5536
  }
5015
5537
  function sha256(contents) {
5016
- return (0, import_node_crypto6.createHash)("sha256").update(contents, "utf-8").digest("hex");
5538
+ return (0, import_node_crypto7.createHash)("sha256").update(contents, "utf-8").digest("hex");
5017
5539
  }
5018
5540
  function safeComposeProject(value) {
5019
5541
  if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(value)) {
@@ -5078,7 +5600,7 @@ function inspectManagedDevcontainerConfig(options) {
5078
5600
  const contents = `${MANAGED_DEVCONTAINER_MARKER}
5079
5601
  ${JSON.stringify(effective, null, 2)}
5080
5602
  `;
5081
- const generatedPath = import_node_path13.default.join(options.repoPath, MANAGED_DEVCONTAINER_PATH);
5603
+ const generatedPath = import_node_path15.default.join(options.repoPath, MANAGED_DEVCONTAINER_PATH);
5082
5604
  assertIgnoredGeneratedPath(options.repoPath, generatedPath);
5083
5605
  assertGeneratedPathOwnership(generatedPath);
5084
5606
  return {
@@ -5105,13 +5627,13 @@ function writeManagedDevcontainerConfig(plan) {
5105
5627
  function inspectManagedDevcontainerGeneratedConfig(plan) {
5106
5628
  let stat;
5107
5629
  try {
5108
- stat = import_node_fs14.default.lstatSync(plan.generatedPath);
5630
+ stat = import_node_fs15.default.lstatSync(plan.generatedPath);
5109
5631
  } catch (error) {
5110
5632
  if (error.code === "ENOENT") return { status: "missing" };
5111
5633
  throw error;
5112
5634
  }
5113
5635
  if (!stat.isFile() || stat.isSymbolicLink()) return { status: "foreign" };
5114
- const contents = import_node_fs14.default.readFileSync(plan.generatedPath, "utf-8");
5636
+ const contents = import_node_fs15.default.readFileSync(plan.generatedPath, "utf-8");
5115
5637
  if (!contents.startsWith(`${MANAGED_DEVCONTAINER_MARKER}
5116
5638
  `)) {
5117
5639
  return { status: "foreign" };
@@ -5123,17 +5645,17 @@ function inspectManagedDevcontainerGeneratedConfig(plan) {
5123
5645
  };
5124
5646
  }
5125
5647
  function removeManagedDevcontainerConfig(plan) {
5126
- if (!import_node_fs14.default.existsSync(plan.generatedPath)) return;
5127
- const stat = import_node_fs14.default.lstatSync(plan.generatedPath);
5648
+ if (!import_node_fs15.default.existsSync(plan.generatedPath)) return;
5649
+ const stat = import_node_fs15.default.lstatSync(plan.generatedPath);
5128
5650
  if (!stat.isFile()) {
5129
5651
  throw new Error(`Managed Dev Container path '${plan.generatedPath}' is not a regular file.`);
5130
5652
  }
5131
- const contents = import_node_fs14.default.readFileSync(plan.generatedPath, "utf-8");
5653
+ const contents = import_node_fs15.default.readFileSync(plan.generatedPath, "utf-8");
5132
5654
  if (!contents.startsWith(`${MANAGED_DEVCONTAINER_MARKER}
5133
5655
  `)) {
5134
5656
  throw new Error(`Managed Dev Container path '${plan.generatedPath}' is not devrouter-owned.`);
5135
5657
  }
5136
- import_node_fs14.default.unlinkSync(plan.generatedPath);
5658
+ import_node_fs15.default.unlinkSync(plan.generatedPath);
5137
5659
  }
5138
5660
  function assertSafeContainerId(containerId) {
5139
5661
  if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(containerId)) {
@@ -5160,7 +5682,7 @@ function startExactManagedServices(options) {
5160
5682
  delete env.DEVROUTER_GIT_COMMON_DIR;
5161
5683
  delete env.DEVCONTAINER_COMPOSE_OVERLAY;
5162
5684
  }
5163
- const result = (0, import_node_child_process6.spawnSync)(
5685
+ const result = (0, import_node_child_process7.spawnSync)(
5164
5686
  "docker",
5165
5687
  [
5166
5688
  "compose",
@@ -5190,7 +5712,7 @@ function startExactManagedServices(options) {
5190
5712
  }
5191
5713
  function stopExactManagedService(containerId, service) {
5192
5714
  assertSafeContainerId(containerId);
5193
- const result = (0, import_node_child_process6.spawnSync)("docker", ["stop", containerId], {
5715
+ const result = (0, import_node_child_process7.spawnSync)("docker", ["stop", containerId], {
5194
5716
  encoding: "utf-8",
5195
5717
  stdio: "inherit"
5196
5718
  });
@@ -5198,14 +5720,14 @@ function stopExactManagedService(containerId, service) {
5198
5720
  throw new Error(`Could not stop exact managed service '${service}' (${containerId}).`);
5199
5721
  }
5200
5722
  }
5201
- var import_node_child_process6, import_node_crypto6, import_node_fs14, import_node_path13, import_yaml4, MANAGED_DEVCONTAINER_PATH, MANAGED_DEVCONTAINER_MARKER;
5723
+ var import_node_child_process7, import_node_crypto7, import_node_fs15, import_node_path15, import_yaml4, MANAGED_DEVCONTAINER_PATH, MANAGED_DEVCONTAINER_MARKER;
5202
5724
  var init_devcontainer_profile = __esm({
5203
5725
  "src/core/devcontainer-profile.ts"() {
5204
5726
  "use strict";
5205
- import_node_child_process6 = require("child_process");
5206
- import_node_crypto6 = require("crypto");
5207
- import_node_fs14 = __toESM(require("fs"));
5208
- import_node_path13 = __toESM(require("path"));
5727
+ import_node_child_process7 = require("child_process");
5728
+ import_node_crypto7 = require("crypto");
5729
+ import_node_fs15 = __toESM(require("fs"));
5730
+ import_node_path15 = __toESM(require("path"));
5209
5731
  import_yaml4 = __toESM(require("yaml"));
5210
5732
  init_atomic_file();
5211
5733
  init_devcontainer_config();
@@ -5219,9 +5741,17 @@ var init_devcontainer_profile = __esm({
5219
5741
  function inspectWorkspaceContainers(options) {
5220
5742
  let ids = options?.ids;
5221
5743
  if (!ids) {
5222
- const listed = (0, import_node_child_process7.spawnSync)("docker", ["ps", "-a", "--format", "{{.ID}}"], {
5223
- encoding: "utf-8"
5224
- });
5744
+ const listed = (0, import_node_child_process8.spawnSync)(
5745
+ "docker",
5746
+ [
5747
+ "ps",
5748
+ "-a",
5749
+ ...options?.composeProject ? ["--filter", `label=com.docker.compose.project=${options.composeProject}`] : [],
5750
+ "--format",
5751
+ "{{.ID}}"
5752
+ ],
5753
+ { encoding: "utf-8" }
5754
+ );
5225
5755
  if (listed.status !== 0) {
5226
5756
  throw new Error(
5227
5757
  `docker ps failed: ${(listed.stderr || listed.stdout || listed.error?.message || "unknown error").trim()}`
@@ -5231,7 +5761,7 @@ function inspectWorkspaceContainers(options) {
5231
5761
  }
5232
5762
  if (ids.length === 0) return [];
5233
5763
  const template = options?.withSize ? SIZE_INSPECT_TEMPLATE : SAFE_INSPECT_TEMPLATE;
5234
- const inspected = (0, import_node_child_process7.spawnSync)(
5764
+ const inspected = (0, import_node_child_process8.spawnSync)(
5235
5765
  "docker",
5236
5766
  ["inspect", ...options?.withSize ? ["--size"] : [], "--format", template, ...ids],
5237
5767
  { encoding: "utf-8" }
@@ -5246,7 +5776,7 @@ function inspectWorkspaceContainers(options) {
5246
5776
  function workspaceAppContainers(containers, repoPath) {
5247
5777
  return containers.filter((container) => {
5248
5778
  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(
5779
+ return Boolean(workingDir && sameWorkspacePath(workingDir, import_node_path16.default.join(repoPath, ".devcontainer"))) && container.mounts.some(
5250
5780
  (mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath)
5251
5781
  );
5252
5782
  });
@@ -5254,7 +5784,7 @@ function workspaceAppContainers(containers, repoPath) {
5254
5784
  function hasExactComposeIdentity(container, options) {
5255
5785
  if (options.composeProject !== void 0 && container.labels["com.docker.compose.project"] !== options.composeProject || container.labels["com.docker.compose.service"] !== options.service || !sameWorkspacePath(
5256
5786
  container.labels["com.docker.compose.project.working_dir"] ?? "",
5257
- import_node_path14.default.join(options.repoPath, ".devcontainer")
5787
+ import_node_path16.default.join(options.repoPath, ".devcontainer")
5258
5788
  )) {
5259
5789
  return false;
5260
5790
  }
@@ -5291,12 +5821,12 @@ function resolveRunningWorkspaceContainer(repoPath) {
5291
5821
  }
5292
5822
  return { id: container.id, workspacePath: repoMount.Destination };
5293
5823
  }
5294
- var import_node_child_process7, import_node_path14, SAFE_INSPECT_TEMPLATE, SIZE_INSPECT_TEMPLATE;
5824
+ var import_node_child_process8, import_node_path16, SAFE_INSPECT_TEMPLATE, SIZE_INSPECT_TEMPLATE;
5295
5825
  var init_devpod_environment = __esm({
5296
5826
  "src/core/devpod-environment.ts"() {
5297
5827
  "use strict";
5298
- import_node_child_process7 = require("child_process");
5299
- import_node_path14 = __toESM(require("path"));
5828
+ import_node_child_process8 = require("child_process");
5829
+ import_node_path16 = __toESM(require("path"));
5300
5830
  init_workspace();
5301
5831
  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
5832
  SIZE_INSPECT_TEMPLATE = SAFE_INSPECT_TEMPLATE.replace(
@@ -5750,7 +6280,7 @@ function toRepoStatus(repoPath) {
5750
6280
  const resolvedRepoPath = resolveRepoPath(repoPath);
5751
6281
  const configPath = getRepoConfigPath(resolvedRepoPath);
5752
6282
  const explicitRepo = typeof repoPath === "string" && repoPath.trim().length > 0;
5753
- const configExists = import_node_fs15.default.existsSync(configPath);
6283
+ const configExists = import_node_fs16.default.existsSync(configPath);
5754
6284
  if (!explicitRepo && !configExists) {
5755
6285
  return void 0;
5756
6286
  }
@@ -5898,11 +6428,11 @@ async function collectRouterStatus(repoPath) {
5898
6428
  }
5899
6429
  };
5900
6430
  }
5901
- var import_node_fs15;
6431
+ var import_node_fs16;
5902
6432
  var init_status = __esm({
5903
6433
  "src/core/status.ts"() {
5904
6434
  "use strict";
5905
- import_node_fs15 = __toESM(require("fs"));
6435
+ import_node_fs16 = __toESM(require("fs"));
5906
6436
  init_docker();
5907
6437
  init_managed_runtime_state();
5908
6438
  init_managed_runtime_status();
@@ -5913,7 +6443,7 @@ var init_status = __esm({
5913
6443
 
5914
6444
  // src/core/tls.ts
5915
6445
  function runOrThrow(command, args) {
5916
- const result = (0, import_node_child_process8.spawnSync)(command, args, {
6446
+ const result = (0, import_node_child_process9.spawnSync)(command, args, {
5917
6447
  encoding: "utf-8"
5918
6448
  });
5919
6449
  if (result.status !== 0) {
@@ -5923,7 +6453,7 @@ function runOrThrow(command, args) {
5923
6453
  return result.stdout.trim();
5924
6454
  }
5925
6455
  function commandExists(command) {
5926
- const result = (0, import_node_child_process8.spawnSync)("sh", ["-c", `command -v ${command}`], { encoding: "utf-8" });
6456
+ const result = (0, import_node_child_process9.spawnSync)("sh", ["-c", `command -v ${command}`], { encoding: "utf-8" });
5927
6457
  return result.status === 0;
5928
6458
  }
5929
6459
  function ensureMkcert() {
@@ -5941,8 +6471,8 @@ function tlsSetupCommand(repoPath) {
5941
6471
  }
5942
6472
  function getMkcertRootCAPath(options = {}) {
5943
6473
  ensureMkcert();
5944
- const rootCAPath = import_node_path15.default.join(runOrThrow("mkcert", ["-CAROOT"]), "rootCA.pem");
5945
- if (!import_node_fs16.default.existsSync(rootCAPath)) {
6474
+ const rootCAPath = import_node_path17.default.join(runOrThrow("mkcert", ["-CAROOT"]), "rootCA.pem");
6475
+ if (!import_node_fs17.default.existsSync(rootCAPath)) {
5946
6476
  throw new Error(
5947
6477
  `mkcert root CA was not found at '${rootCAPath}'. Run: ${tlsSetupCommand(options.repoPath)}`
5948
6478
  );
@@ -5975,7 +6505,7 @@ function parseDnsHostsFromSubjectAltName(subjectAltName) {
5975
6505
  return normalizeUniqueHosts(names);
5976
6506
  }
5977
6507
  function parseCertificateDnsHosts(pem) {
5978
- const certificate = new import_node_crypto7.X509Certificate(pem);
6508
+ const certificate = new import_node_crypto8.X509Certificate(pem);
5979
6509
  const subjectAltName = certificate.subjectAltName ?? "";
5980
6510
  if (subjectAltName.length === 0) {
5981
6511
  return [];
@@ -6043,11 +6573,11 @@ function compactTLSCertificateHosts(hosts) {
6043
6573
  );
6044
6574
  }
6045
6575
  function readCurrentCertificateHosts(options = {}) {
6046
- if (!import_node_fs16.default.existsSync(CERT_FILE)) {
6576
+ if (!import_node_fs17.default.existsSync(CERT_FILE)) {
6047
6577
  return [];
6048
6578
  }
6049
6579
  try {
6050
- const pem = import_node_fs16.default.readFileSync(CERT_FILE, "utf-8");
6580
+ const pem = import_node_fs17.default.readFileSync(CERT_FILE, "utf-8");
6051
6581
  return parseCertificateDnsHosts(pem);
6052
6582
  } catch (error) {
6053
6583
  if (options.replaceMalformed) {
@@ -6175,20 +6705,20 @@ Run: ${tlsSetupCommand(options.repoPath)}`
6175
6705
  );
6176
6706
  }
6177
6707
  }
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;
6708
+ var import_node_child_process9, import_node_crypto8, import_node_fs17, import_node_path17, DEFAULT_TLS_CERT_HOSTS, TLS_CERTIFICATE_LOCK_FILE, TLS_CERTIFICATE_LOCK_WAIT_MS, TLS_CERTIFICATE_WRITE_ATTEMPTS;
6179
6709
  var init_tls = __esm({
6180
6710
  "src/core/tls.ts"() {
6181
6711
  "use strict";
6182
- import_node_child_process8 = require("child_process");
6183
- import_node_crypto7 = require("crypto");
6184
- import_node_fs16 = __toESM(require("fs"));
6185
- import_node_path15 = __toESM(require("path"));
6712
+ import_node_child_process9 = require("child_process");
6713
+ import_node_crypto8 = require("crypto");
6714
+ import_node_fs17 = __toESM(require("fs"));
6715
+ import_node_path17 = __toESM(require("path"));
6186
6716
  init_docker();
6187
6717
  init_file_lock();
6188
6718
  init_host_routes();
6189
6719
  init_router();
6190
6720
  DEFAULT_TLS_CERT_HOSTS = ["localhost", "*.localhost"];
6191
- TLS_CERTIFICATE_LOCK_FILE = import_node_path15.default.join(DEVROUTER_HOME, "tls-certificate.lock");
6721
+ TLS_CERTIFICATE_LOCK_FILE = import_node_path17.default.join(DEVROUTER_HOME, "tls-certificate.lock");
6192
6722
  TLS_CERTIFICATE_LOCK_WAIT_MS = 6e4;
6193
6723
  TLS_CERTIFICATE_WRITE_ATTEMPTS = 2;
6194
6724
  }
@@ -6196,7 +6726,7 @@ var init_tls = __esm({
6196
6726
 
6197
6727
  // src/core/devpod-registry.ts
6198
6728
  function listDevpodWorkspacesRaw() {
6199
- const result = (0, import_node_child_process9.spawnSync)("devpod", ["list", "--output", "json", "--skip-pro"], {
6729
+ const result = (0, import_node_child_process10.spawnSync)("devpod", ["list", "--output", "json", "--skip-pro"], {
6200
6730
  encoding: "utf-8"
6201
6731
  });
6202
6732
  if (result.status !== 0) {
@@ -6231,17 +6761,17 @@ function listDevpodWorkspacesRaw() {
6231
6761
  return workspace;
6232
6762
  });
6233
6763
  }
6234
- var import_node_child_process9;
6764
+ var import_node_child_process10;
6235
6765
  var init_devpod_registry = __esm({
6236
6766
  "src/core/devpod-registry.ts"() {
6237
6767
  "use strict";
6238
- import_node_child_process9 = require("child_process");
6768
+ import_node_child_process10 = require("child_process");
6239
6769
  }
6240
6770
  });
6241
6771
 
6242
6772
  // src/core/devsy-workspaces.ts
6243
6773
  function listDevsyWorkspaces() {
6244
- const result = (0, import_node_child_process10.spawnSync)(
6774
+ const result = (0, import_node_child_process11.spawnSync)(
6245
6775
  "devsy",
6246
6776
  ["workspace", "list", "--result-format", "json", "--skip-pro"],
6247
6777
  {
@@ -6307,7 +6837,7 @@ function parseDevsyRuntimeStatus(output2, expectedId) {
6307
6837
  }
6308
6838
  }
6309
6839
  function inspectDevsyRuntimeStatus(devsyId) {
6310
- const result = (0, import_node_child_process10.spawnSync)("devsy", ["workspace", "status", devsyId, "--result-format", "json"], {
6840
+ const result = (0, import_node_child_process11.spawnSync)("devsy", ["workspace", "status", devsyId, "--result-format", "json"], {
6311
6841
  encoding: "utf-8",
6312
6842
  timeout: 1e4
6313
6843
  });
@@ -6340,11 +6870,11 @@ function selectDevsyWorkspace(workspaces, repoPath) {
6340
6870
  }
6341
6871
  return matches[0];
6342
6872
  }
6343
- var import_node_child_process10;
6873
+ var import_node_child_process11;
6344
6874
  var init_devsy_workspaces = __esm({
6345
6875
  "src/core/devsy-workspaces.ts"() {
6346
6876
  "use strict";
6347
- import_node_child_process10 = require("child_process");
6877
+ import_node_child_process11 = require("child_process");
6348
6878
  init_workspace();
6349
6879
  }
6350
6880
  });
@@ -6361,13 +6891,13 @@ function parseWorkspaceRuntime(value) {
6361
6891
  }
6362
6892
  function isRuntimeInstalled(runtime) {
6363
6893
  const probeArgs = runtime === "devsy" ? ["--version"] : ["version"];
6364
- const probe = (0, import_node_child_process11.spawnSync)(runtime, probeArgs, { encoding: "utf-8" });
6894
+ const probe = (0, import_node_child_process12.spawnSync)(runtime, probeArgs, { encoding: "utf-8" });
6365
6895
  return probe.status === 0 && !probe.error;
6366
6896
  }
6367
6897
  function readWorkspaceRuntimeConfig() {
6368
6898
  let raw;
6369
6899
  try {
6370
- raw = JSON.parse(import_node_fs17.default.readFileSync(RUNTIME_CONFIG_FILE, "utf-8"));
6900
+ raw = JSON.parse(import_node_fs18.default.readFileSync(RUNTIME_CONFIG_FILE, "utf-8"));
6371
6901
  } catch {
6372
6902
  return {};
6373
6903
  }
@@ -6388,7 +6918,7 @@ function readWorkspaceRuntimeConfig() {
6388
6918
  function inspectWorkspaceRuntimeConfig() {
6389
6919
  let rawText;
6390
6920
  try {
6391
- rawText = import_node_fs17.default.readFileSync(RUNTIME_CONFIG_FILE, "utf-8");
6921
+ rawText = import_node_fs18.default.readFileSync(RUNTIME_CONFIG_FILE, "utf-8");
6392
6922
  } catch {
6393
6923
  return { exists: false, config: {}, problems: [] };
6394
6924
  }
@@ -6439,7 +6969,7 @@ function writeWorkspaceRuntimeConfig(config) {
6439
6969
  }
6440
6970
  next.devsyInactivityTimeout = timeout;
6441
6971
  }
6442
- import_node_fs17.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
6972
+ import_node_fs18.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
6443
6973
  writeFileAtomically(RUNTIME_CONFIG_FILE, `${JSON.stringify(next, null, 2)}
6444
6974
  `);
6445
6975
  }
@@ -6547,20 +7077,20 @@ function resetWorkspaceRuntimeCaches() {
6547
7077
  cachedRuntime = void 0;
6548
7078
  cachedSnapshots = void 0;
6549
7079
  }
6550
- var import_node_child_process11, import_node_fs17, import_node_path16, SUPPORTED_RUNTIMES2, RUNTIME_CONFIG_FILE, INACTIVITY_TIMEOUT_PATTERN, cachedRuntime, cachedSnapshots, UnsupportedWorkspaceRuntimeError, WorkspaceRuntimeOwnershipError;
7080
+ var import_node_child_process12, import_node_fs18, import_node_path18, SUPPORTED_RUNTIMES2, RUNTIME_CONFIG_FILE, INACTIVITY_TIMEOUT_PATTERN, cachedRuntime, cachedSnapshots, UnsupportedWorkspaceRuntimeError, WorkspaceRuntimeOwnershipError;
6551
7081
  var init_workspace_runtime = __esm({
6552
7082
  "src/core/workspace-runtime.ts"() {
6553
7083
  "use strict";
6554
- import_node_child_process11 = require("child_process");
6555
- import_node_fs17 = __toESM(require("fs"));
6556
- import_node_path16 = __toESM(require("path"));
7084
+ import_node_child_process12 = require("child_process");
7085
+ import_node_fs18 = __toESM(require("fs"));
7086
+ import_node_path18 = __toESM(require("path"));
6557
7087
  init_atomic_file();
6558
7088
  init_devpod_registry();
6559
7089
  init_devsy_workspaces();
6560
7090
  init_router();
6561
7091
  init_workspace();
6562
7092
  SUPPORTED_RUNTIMES2 = ["devpod", "devsy"];
6563
- RUNTIME_CONFIG_FILE = import_node_path16.default.join(DEVROUTER_HOME, "workspace-runtime.json");
7093
+ RUNTIME_CONFIG_FILE = import_node_path18.default.join(DEVROUTER_HOME, "workspace-runtime.json");
6564
7094
  INACTIVITY_TIMEOUT_PATTERN = /^(?:\d+(?:ms|s|m|h))+$/;
6565
7095
  UnsupportedWorkspaceRuntimeError = class extends Error {
6566
7096
  };
@@ -6581,7 +7111,7 @@ function outputFromResult(result) {
6581
7111
  return output2.length > 0 ? output2 : void 0;
6582
7112
  }
6583
7113
  function runTool(command, args = []) {
6584
- const result = (0, import_node_child_process12.spawnSync)(command, args, {
7114
+ const result = (0, import_node_child_process13.spawnSync)(command, args, {
6585
7115
  encoding: "utf-8"
6586
7116
  });
6587
7117
  if (result.error) {
@@ -6638,12 +7168,12 @@ function parseMinimumNodeMajor(value) {
6638
7168
  return Number(match[1]);
6639
7169
  }
6640
7170
  function readPackageJson(repoPath) {
6641
- const packagePath = import_node_path17.default.join(repoPath, "package.json");
6642
- if (!import_node_fs18.default.existsSync(packagePath)) {
7171
+ const packagePath = import_node_path19.default.join(repoPath, "package.json");
7172
+ if (!import_node_fs19.default.existsSync(packagePath)) {
6643
7173
  return void 0;
6644
7174
  }
6645
7175
  try {
6646
- return JSON.parse(import_node_fs18.default.readFileSync(packagePath, "utf-8"));
7176
+ return JSON.parse(import_node_fs19.default.readFileSync(packagePath, "utf-8"));
6647
7177
  } catch {
6648
7178
  return void 0;
6649
7179
  }
@@ -6708,7 +7238,44 @@ function nodeToolchainCheck(repoPath) {
6708
7238
  details: details.join(", ")
6709
7239
  };
6710
7240
  }
7241
+ function devsyAgentCheck(inspection) {
7242
+ const details = [
7243
+ `state=${inspection.state}`,
7244
+ `source=${inspection.source}`,
7245
+ ...inspection.installedVersion ? [`version=${inspection.installedVersion}`] : [],
7246
+ ...inspection.asset ? [`asset=${inspection.asset.name}`] : []
7247
+ ].join(", ");
7248
+ if (inspection.state === "ready") {
7249
+ return {
7250
+ id: "global.devsy-agent",
7251
+ level: "ok",
7252
+ summary: "Devsy agent source is ready.",
7253
+ details
7254
+ };
7255
+ }
7256
+ const summary = inspection.state === "missing" ? "Managed Devsy agent source is missing." : inspection.state === "stale" ? "Devsy agent source is stale for this Devrouter release." : "Devsy agent source is invalid.";
7257
+ return {
7258
+ id: "global.devsy-agent",
7259
+ level: "error",
7260
+ summary,
7261
+ details: `${details}, ${inspection.reason}`,
7262
+ suggestion: devsyAgentRepairSuggestion(inspection)
7263
+ };
7264
+ }
6711
7265
  function buildGlobalToolChecks(repoPath) {
7266
+ const previous = process.env[DEVSY_DISABLE_TELEMETRY];
7267
+ process.env[DEVSY_DISABLE_TELEMETRY] = "true";
7268
+ try {
7269
+ return buildGlobalToolChecksWithoutTelemetry(repoPath);
7270
+ } finally {
7271
+ if (previous === void 0) {
7272
+ delete process.env[DEVSY_DISABLE_TELEMETRY];
7273
+ } else {
7274
+ process.env[DEVSY_DISABLE_TELEMETRY] = previous;
7275
+ }
7276
+ }
7277
+ }
7278
+ function buildGlobalToolChecksWithoutTelemetry(repoPath) {
6712
7279
  const checks = [];
6713
7280
  const compose = runTool("docker", ["compose", "version"]);
6714
7281
  checks.push({
@@ -6762,6 +7329,13 @@ function buildGlobalToolChecks(repoPath) {
6762
7329
  ].filter(Boolean).join(", "),
6763
7330
  suggestion: runtimeTool.ok ? void 0 : workspaceRuntime === "devsy" ? "Install Devsy for devcontainer workspace flows: brew install devsy-org/homebrew-tap/devsy" : "Install DevPod for devcontainer workspace flows: brew install devpod"
6764
7331
  });
7332
+ if (workspaceRuntime === "devsy") {
7333
+ checks.push(
7334
+ devsyAgentCheck(
7335
+ inspectDevsyAgent({ versionOutput: runtimeTool.ok ? runtimeTool.output : "" })
7336
+ )
7337
+ );
7338
+ }
6765
7339
  const configProblems = [...configInspection.problems];
6766
7340
  const machineResolvedDevpod = workspaceRuntime === "devpod" && (runtimeResolution.source === "auto-detect" || runtimeResolution.source === "default");
6767
7341
  if (machineConfig.devsyInactivityTimeout && machineResolvedDevpod) {
@@ -6783,14 +7357,16 @@ function buildGlobalToolChecks(repoPath) {
6783
7357
  checks.push(nodeToolchainCheck(repoPath));
6784
7358
  return checks;
6785
7359
  }
6786
- var import_node_child_process12, import_node_fs18, import_node_path17;
7360
+ var import_node_child_process13, import_node_fs19, import_node_path19, DEVSY_DISABLE_TELEMETRY;
6787
7361
  var init_tool_diagnostics = __esm({
6788
7362
  "src/core/tool-diagnostics.ts"() {
6789
7363
  "use strict";
6790
- import_node_child_process12 = require("child_process");
6791
- import_node_fs18 = __toESM(require("fs"));
6792
- import_node_path17 = __toESM(require("path"));
7364
+ import_node_child_process13 = require("child_process");
7365
+ import_node_fs19 = __toESM(require("fs"));
7366
+ import_node_path19 = __toESM(require("path"));
7367
+ init_devsy_agent();
6793
7368
  init_workspace_runtime();
7369
+ DEVSY_DISABLE_TELEMETRY = "DEVSY_DISABLE_TELEMETRY";
6794
7370
  }
6795
7371
  });
6796
7372
 
@@ -6859,7 +7435,7 @@ function inspectDevpodRuntimeStatus(devpodId, repoPath) {
6859
7435
  if (resolveWorkspaceRuntimeOrDefault(repoPath) === "devsy") {
6860
7436
  return inspectDevsyRuntimeStatus(devpodId);
6861
7437
  }
6862
- const result = (0, import_node_child_process13.spawnSync)("devpod", ["status", devpodId, "--output", "json", "--timeout", "5s"], {
7438
+ const result = (0, import_node_child_process14.spawnSync)("devpod", ["status", devpodId, "--output", "json", "--timeout", "5s"], {
6863
7439
  encoding: "utf-8"
6864
7440
  });
6865
7441
  if (result.status !== 0 || result.error) return "unknown";
@@ -6892,11 +7468,11 @@ function selectDevpodWorkspace(workspaces, repoPath) {
6892
7468
  }
6893
7469
  return matches[0];
6894
7470
  }
6895
- var import_node_child_process13;
7471
+ var import_node_child_process14;
6896
7472
  var init_devpod_workspaces = __esm({
6897
7473
  "src/core/devpod-workspaces.ts"() {
6898
7474
  "use strict";
6899
- import_node_child_process13 = require("child_process");
7475
+ import_node_child_process14 = require("child_process");
6900
7476
  init_devpod_registry();
6901
7477
  init_devsy_workspaces();
6902
7478
  init_workspace();
@@ -6916,19 +7492,71 @@ function failedStartMayHaveAttached(devsyId, repoPath) {
6916
7492
  }
6917
7493
  }
6918
7494
  function withMutationLock(activity, target, operation) {
6919
- import_node_fs19.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
7495
+ import_node_fs20.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
6920
7496
  return withFileLockSync(
6921
7497
  DEVSY_MUTATION_LOCK_FILE,
6922
- { activity, target: `'${target}'`, waitMs: DEVSY_MUTATION_WAIT_MS },
7498
+ {
7499
+ activity,
7500
+ target: `'${target}'`,
7501
+ waitMs: DEVSY_MUTATION_WAIT_MS,
7502
+ fair: true,
7503
+ onWait: createStderrWaitReporter(activity, `'${target}'`)
7504
+ },
7505
+ operation
7506
+ );
7507
+ }
7508
+ function withMutationLockAsync(activity, target, operation) {
7509
+ import_node_fs20.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
7510
+ return withFileLock(
7511
+ DEVSY_MUTATION_LOCK_FILE,
7512
+ {
7513
+ activity,
7514
+ target: `'${target}'`,
7515
+ waitMs: DEVSY_MUTATION_WAIT_MS,
7516
+ fair: true,
7517
+ onWait: createStderrWaitReporter(activity, `'${target}'`)
7518
+ },
6923
7519
  operation
6924
7520
  );
6925
7521
  }
6926
7522
  function commandFailure2(result) {
6927
7523
  return [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
6928
7524
  }
7525
+ function runDevsyUp(args, env, quiet) {
7526
+ return new Promise((resolve) => {
7527
+ const child = (0, import_node_child_process15.spawn)("devsy", args, {
7528
+ stdio: ["inherit", quiet ? 2 : "inherit", "pipe"],
7529
+ env
7530
+ });
7531
+ const stderr = child.stderr;
7532
+ if (!stderr) throw new Error("Devsy startup stderr pipe was not created.");
7533
+ let stderrTail = Buffer.alloc(0);
7534
+ let spawnError;
7535
+ stderr.on("data", (chunk) => {
7536
+ const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
7537
+ const writable = process.stderr.write(value);
7538
+ if (!writable) {
7539
+ stderr.pause();
7540
+ process.stderr.once("drain", () => stderr.resume());
7541
+ }
7542
+ if (value.length >= DEVSY_STDERR_TAIL_BYTES) {
7543
+ stderrTail = Buffer.from(value.subarray(value.length - DEVSY_STDERR_TAIL_BYTES));
7544
+ } else {
7545
+ const combined = Buffer.concat([stderrTail, value]);
7546
+ stderrTail = combined.length > DEVSY_STDERR_TAIL_BYTES ? combined.subarray(combined.length - DEVSY_STDERR_TAIL_BYTES) : combined;
7547
+ }
7548
+ });
7549
+ child.once("error", (error) => {
7550
+ spawnError = error;
7551
+ });
7552
+ child.once("close", (status) => {
7553
+ resolve({ status, error: spawnError, stderrTail: stderrTail.toString("utf-8") });
7554
+ });
7555
+ });
7556
+ }
6929
7557
  function runDevsyAction(action2, devsyId, force = false) {
6930
7558
  const args = action2 === "delete" ? ["delete", devsyId, ...force ? ["--force"] : [], "--ignore-not-found"] : ["stop", devsyId];
6931
- const result = (0, import_node_child_process14.spawnSync)("devsy", ["workspace", ...args], { encoding: "utf-8" });
7559
+ const result = (0, import_node_child_process15.spawnSync)("devsy", ["workspace", ...args], { encoding: "utf-8" });
6932
7560
  if (result.status !== 0) {
6933
7561
  throw new Error(
6934
7562
  `devsy workspace ${action2}${force ? " --force" : ""} failed for '${devsyId}': ${commandFailure2(result) || "unknown error"}`
@@ -6988,9 +7616,17 @@ function assertDevsyTarget(devsyId, repoPath) {
6988
7616
  }
6989
7617
  return id;
6990
7618
  }
6991
- function startDevsyWorkspace(options) {
7619
+ async function startDevsyWorkspace(options) {
6992
7620
  const activity = options.recreate ? "Devsy recreate" : "Devsy start";
6993
- return withMutationLock(activity, options.repoPath, () => {
7621
+ let agent;
7622
+ try {
7623
+ agent = requireReadyDevsyAgent();
7624
+ } catch (error) {
7625
+ if (!(error instanceof DevsyAgentReadinessError)) throw error;
7626
+ const repair = devsyAgentRepairSuggestion(error.inspection);
7627
+ throw new Error(`${error.message}. ${repair}`);
7628
+ }
7629
+ return withMutationLockAsync(activity, options.repoPath, async () => {
6994
7630
  let devsyId = assertDevsyTarget(options.devsyId, options.repoPath);
6995
7631
  if (devsyId && options.recreate) {
6996
7632
  const attached = listDevsyWorkspaces();
@@ -7019,6 +7655,7 @@ function startDevsyWorkspace(options) {
7019
7655
  }
7020
7656
  if (options.recreate) args.push("--recreate");
7021
7657
  const env = { ...process.env };
7658
+ env.DEVSY_AGENT_BINARY = agent.binaryPath;
7022
7659
  if (options.workspace) {
7023
7660
  env.WORKSPACE = options.workspace.token;
7024
7661
  env.DEVROUTER_WORKSPACE = options.workspace.token;
@@ -7030,12 +7667,13 @@ function startDevsyWorkspace(options) {
7030
7667
  delete env.DEVROUTER_GIT_COMMON_DIR;
7031
7668
  delete env.DEVCONTAINER_COMPOSE_OVERLAY;
7032
7669
  }
7033
- const result = (0, import_node_child_process14.spawnSync)("devsy", args, {
7034
- stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
7035
- env
7036
- });
7670
+ const result = await runDevsyUp(args, env, options.quiet ?? false);
7037
7671
  if (result.status !== 0) {
7038
- const message = `devsy workspace up failed for '${devsyId ?? options.repoPath}'.`;
7672
+ let message = `devsy workspace up failed for '${devsyId ?? options.repoPath}'.`;
7673
+ if (result.error?.message) message += ` ${result.error.message}`;
7674
+ if (AGENT_ACQUISITION_RE.test(result.stderrTail)) {
7675
+ message += ` Devsy rejected the verified agent source. Run: ${DEVSY_AGENT_SETUP_COMMAND}`;
7676
+ }
7039
7677
  if (failedStartMayHaveAttached(devsyId, options.repoPath)) {
7040
7678
  throw new DevsyStartPostconditionError(message);
7041
7679
  }
@@ -7061,29 +7699,38 @@ function startDevsyWorkspace(options) {
7061
7699
  }
7062
7700
  });
7063
7701
  }
7064
- var import_node_child_process14, import_node_fs19, import_node_path18, DEVSY_MUTATION_LOCK_FILE, DEVSY_MUTATION_WAIT_MS, DevsyStartPostconditionError;
7702
+ var import_node_child_process15, import_node_fs20, import_node_path20, DEVSY_MUTATION_LOCK_FILE, DEVSY_MUTATION_WAIT_MS, DevsyStartPostconditionError, AGENT_ACQUISITION_RE, DEVSY_STDERR_TAIL_BYTES;
7065
7703
  var init_devsy_mutation = __esm({
7066
7704
  "src/core/devsy-mutation.ts"() {
7067
7705
  "use strict";
7068
- import_node_child_process14 = require("child_process");
7069
- import_node_fs19 = __toESM(require("fs"));
7070
- import_node_path18 = __toESM(require("path"));
7706
+ import_node_child_process15 = require("child_process");
7707
+ import_node_fs20 = __toESM(require("fs"));
7708
+ import_node_path20 = __toESM(require("path"));
7709
+ init_devsy_agent();
7071
7710
  init_devsy_workspaces();
7072
7711
  init_file_lock();
7073
7712
  init_router();
7074
- DEVSY_MUTATION_LOCK_FILE = import_node_path18.default.join(DEVROUTER_HOME, "devsy-mutation.lock");
7075
- DEVSY_MUTATION_WAIT_MS = 6e4;
7713
+ DEVSY_MUTATION_LOCK_FILE = import_node_path20.default.join(DEVROUTER_HOME, "devsy-mutation.lock");
7714
+ DEVSY_MUTATION_WAIT_MS = 18e5;
7076
7715
  DevsyStartPostconditionError = class extends Error {
7077
7716
  };
7717
+ AGENT_ACQUISITION_RE = /inject agent.*agent binary not found/i;
7718
+ DEVSY_STDERR_TAIL_BYTES = 8192;
7078
7719
  }
7079
7720
  });
7080
7721
 
7081
7722
  // src/core/devpod-mutation.ts
7082
7723
  function withMutationLock2(activity, target, operation) {
7083
- import_node_fs20.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
7724
+ import_node_fs21.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
7084
7725
  return withFileLockSync(
7085
7726
  DEVPOD_MUTATION_LOCK_FILE,
7086
- { activity, target: `'${target}'`, waitMs: DEVPOD_MUTATION_WAIT_MS },
7727
+ {
7728
+ activity,
7729
+ target: `'${target}'`,
7730
+ waitMs: DEVPOD_MUTATION_WAIT_MS,
7731
+ fair: true,
7732
+ onWait: createStderrWaitReporter(activity, `'${target}'`)
7733
+ },
7087
7734
  operation
7088
7735
  );
7089
7736
  }
@@ -7092,7 +7739,7 @@ function commandFailure3(result) {
7092
7739
  }
7093
7740
  function runDevpodAction(action2, devpodId, force = false) {
7094
7741
  const args = action2 === "delete" ? [action2, devpodId, ...force ? ["--force"] : [], "--ignore-not-found"] : [action2, devpodId];
7095
- const result = (0, import_node_child_process15.spawnSync)("devpod", args, { encoding: "utf-8" });
7742
+ const result = (0, import_node_child_process16.spawnSync)("devpod", args, { encoding: "utf-8" });
7096
7743
  if (result.status !== 0) {
7097
7744
  throw new Error(
7098
7745
  `devpod ${action2}${force ? " --force" : ""} failed for '${devpodId}': ${commandFailure3(result) || "unknown error"}`
@@ -7158,10 +7805,10 @@ function deleteOwnedDevpodWorkspace(devpodId, worktreePath) {
7158
7805
  resetWorkspaceRuntimeCaches();
7159
7806
  return result;
7160
7807
  }
7161
- function startDevpodWorkspace(options) {
7808
+ async function startDevpodWorkspace(options) {
7162
7809
  if (resolveWorkspaceRuntimeOrDefault(options.repoPath) === "devsy") {
7163
7810
  try {
7164
- const result = startDevsyWorkspace({
7811
+ const result = await startDevsyWorkspace({
7165
7812
  repoPath: options.repoPath,
7166
7813
  devsyId: options.devpodId,
7167
7814
  devcontainerPath: options.devcontainerPath,
@@ -7220,7 +7867,7 @@ function startDevpodWorkspace(options) {
7220
7867
  delete env.DEVROUTER_GIT_COMMON_DIR;
7221
7868
  delete env.DEVCONTAINER_COMPOSE_OVERLAY;
7222
7869
  }
7223
- const result = (0, import_node_child_process15.spawnSync)("devpod", args, {
7870
+ const result = (0, import_node_child_process16.spawnSync)("devpod", args, {
7224
7871
  stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
7225
7872
  env
7226
7873
  });
@@ -7248,20 +7895,20 @@ function startDevpodWorkspace(options) {
7248
7895
  }
7249
7896
  });
7250
7897
  }
7251
- var import_node_child_process15, import_node_fs20, import_node_path19, DEVPOD_MUTATION_LOCK_FILE, DEVPOD_MUTATION_WAIT_MS, DevpodStartPostconditionError;
7898
+ var import_node_child_process16, import_node_fs21, import_node_path21, DEVPOD_MUTATION_LOCK_FILE, DEVPOD_MUTATION_WAIT_MS, DevpodStartPostconditionError;
7252
7899
  var init_devpod_mutation = __esm({
7253
7900
  "src/core/devpod-mutation.ts"() {
7254
7901
  "use strict";
7255
- import_node_child_process15 = require("child_process");
7256
- import_node_fs20 = __toESM(require("fs"));
7257
- import_node_path19 = __toESM(require("path"));
7902
+ import_node_child_process16 = require("child_process");
7903
+ import_node_fs21 = __toESM(require("fs"));
7904
+ import_node_path21 = __toESM(require("path"));
7258
7905
  init_devpod_workspaces();
7259
7906
  init_devsy_mutation();
7260
7907
  init_file_lock();
7261
7908
  init_router();
7262
7909
  init_workspace_runtime();
7263
- DEVPOD_MUTATION_LOCK_FILE = import_node_path19.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
7264
- DEVPOD_MUTATION_WAIT_MS = 6e4;
7910
+ DEVPOD_MUTATION_LOCK_FILE = import_node_path21.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
7911
+ DEVPOD_MUTATION_WAIT_MS = 18e5;
7265
7912
  DevpodStartPostconditionError = class extends Error {
7266
7913
  };
7267
7914
  }
@@ -7274,7 +7921,7 @@ function commandError(command, repoPath, stderr) {
7274
7921
  );
7275
7922
  }
7276
7923
  function resolveGitCommonDir(repoPath) {
7277
- const result = (0, import_node_child_process16.spawnSync)("git", ["-C", repoPath, "rev-parse", "--git-common-dir"], {
7924
+ const result = (0, import_node_child_process17.spawnSync)("git", ["-C", repoPath, "rev-parse", "--git-common-dir"], {
7278
7925
  encoding: "utf-8",
7279
7926
  env: READ_ONLY_GIT_ENV
7280
7927
  });
@@ -7282,10 +7929,10 @@ function resolveGitCommonDir(repoPath) {
7282
7929
  if (result.status !== 0 || !output2) {
7283
7930
  throw commandError("Could not resolve the Git common directory", repoPath, result.stderr);
7284
7931
  }
7285
- return comparableWorkspacePath(import_node_path20.default.isAbsolute(output2) ? output2 : import_node_path20.default.resolve(repoPath, output2));
7932
+ return comparableWorkspacePath(import_node_path22.default.isAbsolute(output2) ? output2 : import_node_path22.default.resolve(repoPath, output2));
7286
7933
  }
7287
7934
  function resolveGitTopLevel(repoPath) {
7288
- const result = (0, import_node_child_process16.spawnSync)("git", ["-C", repoPath, "rev-parse", "--show-toplevel"], {
7935
+ const result = (0, import_node_child_process17.spawnSync)("git", ["-C", repoPath, "rev-parse", "--show-toplevel"], {
7289
7936
  encoding: "utf-8",
7290
7937
  env: READ_ONLY_GIT_ENV
7291
7938
  });
@@ -7296,7 +7943,7 @@ function resolveGitTopLevel(repoPath) {
7296
7943
  return comparableWorkspacePath(output2);
7297
7944
  }
7298
7945
  function listGitWorktrees(repoPath) {
7299
- const result = (0, import_node_child_process16.spawnSync)("git", ["-C", repoPath, "worktree", "list", "--porcelain"], {
7946
+ const result = (0, import_node_child_process17.spawnSync)("git", ["-C", repoPath, "worktree", "list", "--porcelain"], {
7300
7947
  encoding: "utf-8",
7301
7948
  env: READ_ONLY_GIT_ENV
7302
7949
  });
@@ -7333,7 +7980,7 @@ function listGitWorktrees(repoPath) {
7333
7980
  return worktrees;
7334
7981
  }
7335
7982
  function ownershipDirectory(repoPath) {
7336
- return import_node_path20.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
7983
+ return import_node_path22.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
7337
7984
  }
7338
7985
  function validateWorkspace(value, label) {
7339
7986
  if (typeof value !== "string" || wsFromBranch(value) !== value) {
@@ -7361,7 +8008,7 @@ function validateRecord(value, expectedWorkspace) {
7361
8008
  `workspace ownership file '${expectedWorkspace}' contains identity '${workspace}'`
7362
8009
  );
7363
8010
  }
7364
- if (typeof candidate.worktreePath !== "string" || !import_node_path20.default.isAbsolute(candidate.worktreePath)) {
8011
+ if (typeof candidate.worktreePath !== "string" || !import_node_path22.default.isAbsolute(candidate.worktreePath)) {
7365
8012
  throw new Error("invalid workspace ownership worktreePath");
7366
8013
  }
7367
8014
  if (candidate.branch !== null && typeof candidate.branch !== "string") {
@@ -7381,7 +8028,7 @@ function validateRecord(value, expectedWorkspace) {
7381
8028
  function readRecordFile(filePath, expectedWorkspace) {
7382
8029
  let parsed;
7383
8030
  try {
7384
- parsed = JSON.parse(import_node_fs21.default.readFileSync(filePath, "utf-8"));
8031
+ parsed = JSON.parse(import_node_fs22.default.readFileSync(filePath, "utf-8"));
7385
8032
  } catch (error) {
7386
8033
  if (error instanceof SyntaxError) {
7387
8034
  throw new Error(`invalid workspace ownership JSON at '${filePath}'`);
@@ -7397,7 +8044,7 @@ function listWorkspaceOwnership(repoPath) {
7397
8044
  function listWorkspaceOwnershipInDirectory(directory) {
7398
8045
  let entries;
7399
8046
  try {
7400
- entries = import_node_fs21.default.readdirSync(directory, { withFileTypes: true });
8047
+ entries = import_node_fs22.default.readdirSync(directory, { withFileTypes: true });
7401
8048
  } catch (error) {
7402
8049
  if (error.code === "ENOENT") return [];
7403
8050
  throw error;
@@ -7405,14 +8052,14 @@ function listWorkspaceOwnershipInDirectory(directory) {
7405
8052
  return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
7406
8053
  const workspace = entry.name.slice(0, -".json".length);
7407
8054
  validateWorkspace(workspace, "filename");
7408
- return readRecordFile(import_node_path20.default.join(directory, entry.name), workspace);
8055
+ return readRecordFile(import_node_path22.default.join(directory, entry.name), workspace);
7409
8056
  });
7410
8057
  }
7411
8058
  function writeWorkspaceOwnershipInDirectory(directory, input2) {
7412
8059
  const workspace = validateWorkspace(input2.workspace, "workspace");
7413
8060
  const devpodId = validateWorkspace(input2.devpodId, "devpodId");
7414
8061
  const worktreePath = comparableWorkspacePath(input2.worktreePath);
7415
- const filePath = import_node_path20.default.join(directory, `${workspace}.json`);
8062
+ const filePath = import_node_path22.default.join(directory, `${workspace}.json`);
7416
8063
  const now = (/* @__PURE__ */ new Date()).toISOString();
7417
8064
  const records = listWorkspaceOwnershipInDirectory(directory);
7418
8065
  const existing = records.find((record2) => record2.workspace === workspace);
@@ -7448,9 +8095,9 @@ function writeWorkspaceOwnershipInDirectory(directory, input2) {
7448
8095
  return record;
7449
8096
  }
7450
8097
  function removeWorkspaceOwnershipInDirectory(directory, workspace) {
7451
- const filePath = import_node_path20.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
8098
+ const filePath = import_node_path22.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
7452
8099
  try {
7453
- import_node_fs21.default.rmSync(filePath);
8100
+ import_node_fs22.default.rmSync(filePath);
7454
8101
  return true;
7455
8102
  } catch (error) {
7456
8103
  if (error.code === "ENOENT") return false;
@@ -7461,7 +8108,7 @@ function sameOwnershipRecord(left, right) {
7461
8108
  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
8109
  }
7463
8110
  function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
7464
- const filePath = import_node_path20.default.join(
8111
+ const filePath = import_node_path22.default.join(
7465
8112
  directory,
7466
8113
  `${validateWorkspace(expected.workspace, "workspace")}.json`
7467
8114
  );
@@ -7473,14 +8120,14 @@ function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
7473
8120
  throw error;
7474
8121
  }
7475
8122
  if (!sameOwnershipRecord(current, expected)) return "changed";
7476
- import_node_fs21.default.rmSync(filePath);
8123
+ import_node_fs22.default.rmSync(filePath);
7477
8124
  return "removed";
7478
8125
  }
7479
8126
  function withWorkspaceOwnershipTransaction(repoPath, operation, options = {}) {
7480
8127
  const directory = ownershipDirectory(repoPath);
7481
- import_node_fs21.default.mkdirSync(directory, { recursive: true });
8128
+ import_node_fs22.default.mkdirSync(directory, { recursive: true });
7482
8129
  return withFileLockSync(
7483
- import_node_path20.default.join(directory, ".lock"),
8130
+ import_node_path22.default.join(directory, ".lock"),
7484
8131
  {
7485
8132
  activity: "workspace ownership transaction",
7486
8133
  target: `'${repoPath}'`,
@@ -7513,7 +8160,7 @@ function persistedWorkspaceOwners(repoPath, worktreePath) {
7513
8160
  try {
7514
8161
  workspace = readPersistedWorkspace(worktree.path);
7515
8162
  } catch (error) {
7516
- if (!import_node_fs21.default.existsSync(worktree.path)) continue;
8163
+ if (!import_node_fs22.default.existsSync(worktree.path)) continue;
7517
8164
  throw error;
7518
8165
  }
7519
8166
  if (!workspace) continue;
@@ -7676,7 +8323,7 @@ function inspectWorkspaceOwnership(record, worktrees, devpods) {
7676
8323
  if (worktree?.locked) {
7677
8324
  return { ownerStatus: "locked", devpodStatus, worktree };
7678
8325
  }
7679
- if ((!worktree || worktree.prunable) && import_node_fs21.default.existsSync(record.worktreePath)) {
8326
+ if ((!worktree || worktree.prunable) && import_node_fs22.default.existsSync(record.worktreePath)) {
7680
8327
  return { ownerStatus: "conflict", devpodStatus, worktree };
7681
8328
  }
7682
8329
  if (!worktree || worktree.prunable) {
@@ -7700,20 +8347,20 @@ function listMissingWorkspaceOwnership(repoPath) {
7700
8347
  (record) => inspectWorkspaceOwnership(record, worktrees, void 0).ownerStatus === "missing"
7701
8348
  );
7702
8349
  }
7703
- var import_node_child_process16, import_node_fs21, import_node_path20, READ_ONLY_GIT_ENV, OWNERSHIP_VERSION, OWNERSHIP_DIR;
8350
+ var import_node_child_process17, import_node_fs22, import_node_path22, READ_ONLY_GIT_ENV, OWNERSHIP_VERSION, OWNERSHIP_DIR;
7704
8351
  var init_workspace_ownership = __esm({
7705
8352
  "src/core/workspace-ownership.ts"() {
7706
8353
  "use strict";
7707
- import_node_child_process16 = require("child_process");
7708
- import_node_fs21 = __toESM(require("fs"));
7709
- import_node_path20 = __toESM(require("path"));
8354
+ import_node_child_process17 = require("child_process");
8355
+ import_node_fs22 = __toESM(require("fs"));
8356
+ import_node_path22 = __toESM(require("path"));
7710
8357
  init_atomic_file();
7711
8358
  init_devpod_workspaces();
7712
8359
  init_file_lock();
7713
8360
  init_workspace();
7714
8361
  READ_ONLY_GIT_ENV = { ...process.env, GIT_OPTIONAL_LOCKS: "0" };
7715
8362
  OWNERSHIP_VERSION = 1;
7716
- OWNERSHIP_DIR = import_node_path20.default.join("devrouter", "workspaces");
8363
+ OWNERSHIP_DIR = import_node_path22.default.join("devrouter", "workspaces");
7717
8364
  }
7718
8365
  });
7719
8366
 
@@ -7727,10 +8374,10 @@ function inRepositoryWorkspaceScope(repoPath, worktreePath, livePaths) {
7727
8374
  if (livePaths.some((candidate) => sameWorkspacePath(candidate, worktreePath))) return true;
7728
8375
  const comparableRepo = comparableWorkspacePath(repoPath);
7729
8376
  const comparableWorktree = comparableWorkspacePath(worktreePath);
7730
- const localRoot = import_node_path21.default.join(comparableRepo, "trees") + import_node_path21.default.sep;
8377
+ const localRoot = import_node_path23.default.join(comparableRepo, "trees") + import_node_path23.default.sep;
7731
8378
  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);
8379
+ const legacyPrefix = `${import_node_path23.default.basename(comparableRepo)}-`;
8380
+ return import_node_path23.default.dirname(comparableWorktree) === import_node_path23.default.dirname(comparableRepo) && import_node_path23.default.basename(comparableWorktree).startsWith(legacyPrefix);
7734
8381
  }
7735
8382
  function previewActions(devpodStatus, routeCount, includeRecord) {
7736
8383
  const actions = [
@@ -7982,11 +8629,11 @@ function applyWorkspaceGc(plan) {
7982
8629
  candidates
7983
8630
  };
7984
8631
  }
7985
- var import_node_path21;
8632
+ var import_node_path23;
7986
8633
  var init_workspace_gc = __esm({
7987
8634
  "src/core/workspace-gc.ts"() {
7988
8635
  "use strict";
7989
- import_node_path21 = __toESM(require("path"));
8636
+ import_node_path23 = __toESM(require("path"));
7990
8637
  init_devpod_mutation();
7991
8638
  init_devpod_workspaces();
7992
8639
  init_host_routes();
@@ -8056,11 +8703,11 @@ function inspectPostgresCredentials(repoPath, config) {
8056
8703
  parseErrors.push(`${app.name}: ${composeFile} (${message})`);
8057
8704
  continue;
8058
8705
  }
8059
- if (!import_node_fs22.default.existsSync(absolutePath)) {
8706
+ if (!import_node_fs23.default.existsSync(absolutePath)) {
8060
8707
  continue;
8061
8708
  }
8062
8709
  try {
8063
- const raw = import_node_fs22.default.readFileSync(absolutePath, "utf-8");
8710
+ const raw = import_node_fs23.default.readFileSync(absolutePath, "utf-8");
8064
8711
  const parsed = import_yaml5.default.parse(raw);
8065
8712
  const root = asRecord2(parsed);
8066
8713
  const services = asRecord2(root?.services);
@@ -8339,7 +8986,7 @@ async function buildDoctorReport(options = {}) {
8339
8986
  const config = runtimeConfig.config;
8340
8987
  loadedConfig = config;
8341
8988
  loadedWorkspace = runtimeConfig.workspace;
8342
- const cliVersion = true ? "0.0.45" : "0.0.0-dev";
8989
+ const cliVersion = true ? "0.0.48" : "0.0.0-dev";
8343
8990
  const configVersion = config.devrouter?.version;
8344
8991
  if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
8345
8992
  addCheck(checks, {
@@ -8369,9 +9016,9 @@ async function buildDoctorReport(options = {}) {
8369
9016
  (app) => app.docker.composeFiles.map((filePath) => ({
8370
9017
  app: app.name,
8371
9018
  filePath,
8372
- absolutePath: import_node_path22.default.resolve(repo.path, filePath)
9019
+ absolutePath: import_node_path24.default.resolve(repo.path, filePath)
8373
9020
  }))
8374
- ).filter((entry) => !import_node_fs22.default.existsSync(entry.absolutePath));
9021
+ ).filter((entry) => !import_node_fs23.default.existsSync(entry.absolutePath));
8375
9022
  addCheck(checks, {
8376
9023
  id: "repo.compose-files",
8377
9024
  level: missingComposeFiles.length === 0 ? "ok" : "error",
@@ -8414,8 +9061,8 @@ async function buildDoctorReport(options = {}) {
8414
9061
  const missingHostCwds = config.apps.filter((app) => app.runtime === "host").map((app) => ({
8415
9062
  app: app.name,
8416
9063
  cwd: app.hostRun.cwd,
8417
- absolutePath: import_node_path22.default.resolve(repo.path, app.hostRun.cwd)
8418
- })).filter((entry) => !import_node_fs22.default.existsSync(entry.absolutePath));
9064
+ absolutePath: import_node_path24.default.resolve(repo.path, app.hostRun.cwd)
9065
+ })).filter((entry) => !import_node_fs23.default.existsSync(entry.absolutePath));
8419
9066
  addCheck(checks, {
8420
9067
  id: "repo.host-cwd",
8421
9068
  level: missingHostCwds.length === 0 ? "ok" : "error",
@@ -8559,12 +9206,12 @@ async function buildDoctorReport(options = {}) {
8559
9206
  nextSteps
8560
9207
  };
8561
9208
  }
8562
- var import_node_fs22, import_node_path22, import_yaml5, POSTGRES_DEFAULTS;
9209
+ var import_node_fs23, import_node_path24, import_yaml5, POSTGRES_DEFAULTS;
8563
9210
  var init_doctor = __esm({
8564
9211
  "src/core/doctor.ts"() {
8565
9212
  "use strict";
8566
- import_node_fs22 = __toESM(require("fs"));
8567
- import_node_path22 = __toESM(require("path"));
9213
+ import_node_fs23 = __toESM(require("fs"));
9214
+ import_node_path24 = __toESM(require("path"));
8568
9215
  import_yaml5 = __toESM(require("yaml"));
8569
9216
  init_docker();
8570
9217
  init_host_routes();
@@ -8671,11 +9318,13 @@ async function runSetup(options = {}) {
8671
9318
  })
8672
9319
  );
8673
9320
  }
9321
+ let requestedWorkspaceRuntime;
8674
9322
  if (options.workspaceRuntime !== void 0 || options.devsyInactivityTimeout !== void 0) {
8675
9323
  try {
8676
9324
  const existing = readWorkspaceRuntimeConfig();
9325
+ requestedWorkspaceRuntime = options.workspaceRuntime ? parseWorkspaceRuntime(options.workspaceRuntime) : void 0;
8677
9326
  const next = {
8678
- runtime: options.workspaceRuntime ? parseWorkspaceRuntime(options.workspaceRuntime) : existing.runtime,
9327
+ runtime: requestedWorkspaceRuntime ?? existing.runtime,
8679
9328
  devsyInactivityTimeout: options.devsyInactivityTimeout ?? existing.devsyInactivityTimeout
8680
9329
  };
8681
9330
  writeWorkspaceRuntimeConfig(next);
@@ -8697,6 +9346,28 @@ async function runSetup(options = {}) {
8697
9346
  );
8698
9347
  }
8699
9348
  }
9349
+ if (requestedWorkspaceRuntime === "devsy") {
9350
+ try {
9351
+ const prepared = await prepareDevsyAgent();
9352
+ actions.push(
9353
+ action(prepared.changed ? "performed" : "skipped", {
9354
+ id: "global.devsy-agent",
9355
+ summary: prepared.changed ? `Downloaded and verified ${prepared.asset.name} for Devsy.` : `Verified the ${prepared.source} Devsy agent source.`,
9356
+ details: `version=${SUPPORTED_DEVSY_VERSION}, asset=${prepared.asset.name}, source=${prepared.source}, transport=${prepared.transport}`
9357
+ })
9358
+ );
9359
+ } catch (error) {
9360
+ const details = error instanceof DevsyAgentReadinessError ? `state=${error.inspection.state}, source=${error.inspection.source}, ${error.inspection.reason}` : error instanceof Error ? error.message : String(error);
9361
+ actions.push(
9362
+ action("failed", {
9363
+ id: "global.devsy-agent",
9364
+ summary: "Failed to prepare a verified Devsy agent source.",
9365
+ details,
9366
+ suggestion: error instanceof DevsyAgentReadinessError ? devsyAgentRepairSuggestion(error.inspection) : `Run: ${DEVSY_AGENT_SETUP_COMMAND}`
9367
+ })
9368
+ );
9369
+ }
9370
+ }
8700
9371
  try {
8701
9372
  const existed = await networkExists(DEVNET_NAME);
8702
9373
  await ensureNetwork(DEVNET_NAME);
@@ -8786,6 +9457,7 @@ async function runSetup(options = {}) {
8786
9457
  var init_setup = __esm({
8787
9458
  "src/core/setup.ts"() {
8788
9459
  "use strict";
9460
+ init_devsy_agent();
8789
9461
  init_docker();
8790
9462
  init_doctor();
8791
9463
  init_repo_config();
@@ -8854,12 +9526,12 @@ function parseSsPortListeners(stdout, port) {
8854
9526
  return listeners;
8855
9527
  }
8856
9528
  function findPortListeners(port) {
8857
- const result = (0, import_node_child_process17.spawnSync)("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN"], {
9529
+ const result = (0, import_node_child_process18.spawnSync)("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN"], {
8858
9530
  encoding: "utf-8"
8859
9531
  });
8860
9532
  if (result.error && result.error.code === "ENOENT") {
8861
9533
  if (process.platform === "linux") {
8862
- const ssResult = (0, import_node_child_process17.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
9534
+ const ssResult = (0, import_node_child_process18.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
8863
9535
  if (ssResult.status === 0 && ssResult.stdout) {
8864
9536
  return parseSsPortListeners(ssResult.stdout, port);
8865
9537
  }
@@ -8888,11 +9560,11 @@ function findPortListeners(port) {
8888
9560
  };
8889
9561
  });
8890
9562
  }
8891
- var import_node_child_process17;
9563
+ var import_node_child_process18;
8892
9564
  var init_ports = __esm({
8893
9565
  "src/util/ports.ts"() {
8894
9566
  "use strict";
8895
- import_node_child_process17 = require("child_process");
9567
+ import_node_child_process18 = require("child_process");
8896
9568
  }
8897
9569
  });
8898
9570
 
@@ -8969,18 +9641,18 @@ function probeHttpRoute(host, options = {}) {
8969
9641
  args.push("--cacert", CERT_FILE);
8970
9642
  }
8971
9643
  args.push(url);
8972
- const result = (0, import_node_child_process18.spawnSync)("curl", args, { encoding: "utf-8" });
9644
+ const result = (0, import_node_child_process19.spawnSync)("curl", args, { encoding: "utf-8" });
8973
9645
  const stdout = result.stdout?.trim() ?? "";
8974
9646
  const status = /^\d{3}$/.test(stdout) ? Number(stdout) : void 0;
8975
9647
  const ok = result.status === 0 && status !== void 0 && status >= 100 && status < 500;
8976
9648
  const details = ok ? `HTTP ${status}` : [status === void 0 ? void 0 : `HTTP ${status}`, result.stderr?.trim()].filter(Boolean).join(": ") || `curl exited with status ${result.status ?? "unknown"}`;
8977
9649
  return { ok, status, details };
8978
9650
  }
8979
- var import_node_child_process18;
9651
+ var import_node_child_process19;
8980
9652
  var init_http_route_probe = __esm({
8981
9653
  "src/core/http-route-probe.ts"() {
8982
9654
  "use strict";
8983
- import_node_child_process18 = require("child_process");
9655
+ import_node_child_process19 = require("child_process");
8984
9656
  init_router();
8985
9657
  init_tls();
8986
9658
  }
@@ -9056,14 +9728,135 @@ var init_route_publication = __esm({
9056
9728
  }
9057
9729
  });
9058
9730
 
9731
+ // src/core/traefik-route-health.ts
9732
+ function sleep(ms) {
9733
+ return new Promise((resolve) => setTimeout(resolve, ms));
9734
+ }
9735
+ function inspectRouterApi(protocol) {
9736
+ const result = (0, import_node_child_process20.spawnSync)(
9737
+ "curl",
9738
+ [
9739
+ "--silent",
9740
+ "--show-error",
9741
+ "--fail",
9742
+ "--max-time",
9743
+ "2",
9744
+ `${ROUTER_API_BASE}/${protocol}/routers`
9745
+ ],
9746
+ { encoding: "utf-8" }
9747
+ );
9748
+ if (result.status !== 0) {
9749
+ return {
9750
+ ok: false,
9751
+ details: result.stderr?.trim() || `curl exited with status ${result.status ?? "unknown"}`
9752
+ };
9753
+ }
9754
+ let value;
9755
+ try {
9756
+ value = JSON.parse(result.stdout);
9757
+ } catch {
9758
+ return { ok: false, details: `${protocol.toUpperCase()} router API returned invalid JSON` };
9759
+ }
9760
+ if (!Array.isArray(value)) {
9761
+ return { ok: false, details: `${protocol.toUpperCase()} router API returned a non-array` };
9762
+ }
9763
+ return {
9764
+ ok: true,
9765
+ names: new Set(
9766
+ value.flatMap((item) => {
9767
+ if (!item || typeof item !== "object") return [];
9768
+ const name = item.name;
9769
+ return typeof name === "string" ? [name] : [];
9770
+ })
9771
+ )
9772
+ };
9773
+ }
9774
+ function inspectExpectedRoutes(routes) {
9775
+ const missing = [];
9776
+ const failures = [];
9777
+ for (const protocol of ["http", "tcp"]) {
9778
+ const expected = routes.filter((route) => (route.protocol ?? "http") === protocol).map((route) => buildHostRouteRouterName(route.repoPath, route.name));
9779
+ if (expected.length === 0) continue;
9780
+ const result = inspectRouterApi(protocol);
9781
+ if (!result.ok) {
9782
+ failures.push(result.details);
9783
+ missing.push(...expected);
9784
+ continue;
9785
+ }
9786
+ missing.push(...expected.filter((name) => !result.names.has(name)));
9787
+ }
9788
+ return missing.length === 0 ? { ok: true } : {
9789
+ ok: false,
9790
+ missing,
9791
+ details: failures.length > 0 ? failures.join("; ") : "router names are absent"
9792
+ };
9793
+ }
9794
+ async function waitForExpectedRoutes(routes, timeoutMs, pollIntervalMs) {
9795
+ const deadline = Date.now() + timeoutMs;
9796
+ let result;
9797
+ do {
9798
+ result = inspectExpectedRoutes(routes);
9799
+ if (result.ok) return result;
9800
+ if (Date.now() < deadline) {
9801
+ await sleep(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())));
9802
+ }
9803
+ } while (Date.now() < deadline);
9804
+ return result;
9805
+ }
9806
+ async function ensureTraefikRoutesLoaded(routes, options = {}) {
9807
+ if (routes.length === 0) return { restarted: false };
9808
+ const initialTimeoutMs = options.initialTimeoutMs ?? DEFAULT_INITIAL_TIMEOUT_MS;
9809
+ const recoveryTimeoutMs = options.recoveryTimeoutMs ?? DEFAULT_RECOVERY_TIMEOUT_MS;
9810
+ const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
9811
+ const initial = await waitForExpectedRoutes(routes, initialTimeoutMs, pollIntervalMs);
9812
+ if (initial.ok) return { restarted: false };
9813
+ import_node_fs24.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
9814
+ return withFileLock(
9815
+ ROUTER_RELOAD_LOCK_FILE,
9816
+ {
9817
+ activity: "Traefik route reload recovery",
9818
+ waitMs: initialTimeoutMs + recoveryTimeoutMs + 1e4,
9819
+ fair: true,
9820
+ onWait: createStderrWaitReporter("Traefik route reload recovery", "shared router")
9821
+ },
9822
+ async () => {
9823
+ const recheck = await waitForExpectedRoutes(routes, 0, pollIntervalMs);
9824
+ if (recheck.ok) return { restarted: false };
9825
+ restartRouterStack();
9826
+ const recovered = await waitForExpectedRoutes(routes, recoveryTimeoutMs, pollIntervalMs);
9827
+ if (recovered.ok) return { restarted: true };
9828
+ throw new Error(
9829
+ `Traefik did not load file-provider routes after one restart: ${recovered.missing.join(", ")} (${recovered.details}). Inspect: devrouter logs --tail 100`
9830
+ );
9831
+ }
9832
+ );
9833
+ }
9834
+ var import_node_child_process20, import_node_fs24, import_node_path25, ROUTER_API_BASE, ROUTER_RELOAD_LOCK_FILE, DEFAULT_INITIAL_TIMEOUT_MS, DEFAULT_RECOVERY_TIMEOUT_MS, DEFAULT_POLL_INTERVAL_MS;
9835
+ var init_traefik_route_health = __esm({
9836
+ "src/core/traefik-route-health.ts"() {
9837
+ "use strict";
9838
+ import_node_child_process20 = require("child_process");
9839
+ import_node_fs24 = __toESM(require("fs"));
9840
+ import_node_path25 = __toESM(require("path"));
9841
+ init_file_lock();
9842
+ init_host_routes();
9843
+ init_router();
9844
+ ROUTER_API_BASE = "http://127.0.0.1:8080/api";
9845
+ ROUTER_RELOAD_LOCK_FILE = import_node_path25.default.join(DEVROUTER_HOME, "router-reload.lock");
9846
+ DEFAULT_INITIAL_TIMEOUT_MS = 3e3;
9847
+ DEFAULT_RECOVERY_TIMEOUT_MS = 1e4;
9848
+ DEFAULT_POLL_INTERVAL_MS = 250;
9849
+ }
9850
+ });
9851
+
9059
9852
  // src/core/workspace-ensure.ts
9060
9853
  function assertOverlay(container, repoPath) {
9061
9854
  const workingDir = container.labels["com.docker.compose.project.working_dir"];
9062
- if (!workingDir || !sameWorkspacePath(workingDir, import_node_path23.default.join(repoPath, ".devcontainer"))) {
9855
+ if (!workingDir || !sameWorkspacePath(workingDir, import_node_path26.default.join(repoPath, ".devcontainer"))) {
9063
9856
  throw new Error(`Container '${container.id}' does not belong to the exact worktree.`);
9064
9857
  }
9065
9858
  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);
9859
+ const expectedOverlay = import_node_path26.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
9067
9860
  if (!configFiles.some((configFile) => sameWorkspacePath(configFile, expectedOverlay))) {
9068
9861
  throw new Error(`Container '${container.id}' was not started with ${DEVCONTAINER_OVERLAY}.`);
9069
9862
  }
@@ -9137,7 +9930,7 @@ async function waitForManagedServices(plan, repoPath, composeProject, expectedSe
9137
9930
  } catch (error) {
9138
9931
  lastError = error;
9139
9932
  }
9140
- if (Date.now() < deadline) await sleep(POLL_INTERVAL_MS);
9933
+ if (Date.now() < deadline) await sleep2(POLL_INTERVAL_MS);
9141
9934
  } while (Date.now() < deadline);
9142
9935
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
9143
9936
  }
@@ -9239,6 +10032,20 @@ function isWarmWorkspaceActive(repoPath) {
9239
10032
  return true;
9240
10033
  }
9241
10034
  }
10035
+ function hasExactManagedComposeProject(repoPath, state) {
10036
+ try {
10037
+ return inspectWorkspaceContainers({
10038
+ composeProject: state.composeProject
10039
+ }).some((container) => {
10040
+ const workingDir = container.labels["com.docker.compose.project.working_dir"];
10041
+ return Boolean(
10042
+ workingDir && sameWorkspacePath(workingDir, import_node_path26.default.join(repoPath, ".devcontainer"))
10043
+ );
10044
+ });
10045
+ } catch {
10046
+ return true;
10047
+ }
10048
+ }
9242
10049
  function captureFirstTransitionBaseline(options) {
9243
10050
  const containers = inspectWorkspaceContainers();
9244
10051
  const runningPrimary = workspaceAppContainers(containers, options.repoPath).filter(
@@ -9326,7 +10133,7 @@ function validateWorkspaceContainers(containers, options) {
9326
10133
  }
9327
10134
  return { id: appContainer.id, workspacePath: repoMount.Destination };
9328
10135
  }
9329
- function sleep(ms) {
10136
+ function sleep2(ms) {
9330
10137
  return new Promise((resolve) => setTimeout(resolve, ms));
9331
10138
  }
9332
10139
  async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeoutMs) {
@@ -9340,7 +10147,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
9340
10147
  target
9341
10148
  });
9342
10149
  if (target.kind === "linked") {
9343
- const workspaceEnv = (0, import_node_child_process19.spawnSync)(
10150
+ const workspaceEnv = (0, import_node_child_process21.spawnSync)(
9344
10151
  "docker",
9345
10152
  ["exec", appContainer.id, "printenv", "WORKSPACE"],
9346
10153
  { encoding: "utf-8" }
@@ -9350,7 +10157,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
9350
10157
  `Workspace app container must expose WORKSPACE='${target.workspace}' (got '${workspaceEnv.stdout.trim() || "(empty)"}').`
9351
10158
  );
9352
10159
  }
9353
- const devrouterWorkspaceEnv = (0, import_node_child_process19.spawnSync)(
10160
+ const devrouterWorkspaceEnv = (0, import_node_child_process21.spawnSync)(
9354
10161
  "docker",
9355
10162
  ["exec", appContainer.id, "printenv", "DEVROUTER_WORKSPACE"],
9356
10163
  { encoding: "utf-8" }
@@ -9361,7 +10168,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
9361
10168
  );
9362
10169
  }
9363
10170
  }
9364
- const gitCheck = (0, import_node_child_process19.spawnSync)(
10171
+ const gitCheck = (0, import_node_child_process21.spawnSync)(
9365
10172
  "docker",
9366
10173
  [
9367
10174
  "exec",
@@ -9382,7 +10189,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
9382
10189
  lastError = error;
9383
10190
  }
9384
10191
  if (Date.now() < deadline) {
9385
- await sleep(POLL_INTERVAL_MS);
10192
+ await sleep2(POLL_INTERVAL_MS);
9386
10193
  }
9387
10194
  } while (Date.now() < deadline);
9388
10195
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
@@ -9407,7 +10214,7 @@ async function waitForHttpRoutes(repoPath, apps, timeoutMs) {
9407
10214
  return;
9408
10215
  }
9409
10216
  if (Date.now() < deadline) {
9410
- await sleep(POLL_INTERVAL_MS);
10217
+ await sleep2(POLL_INTERVAL_MS);
9411
10218
  }
9412
10219
  } while (Date.now() < deadline);
9413
10220
  throw new Error(
@@ -9453,14 +10260,14 @@ function resolvePrimaryTarget(repoPath) {
9453
10260
  }
9454
10261
  function isPrimaryCheckout(repoPath) {
9455
10262
  try {
9456
- return import_node_fs23.default.statSync(import_node_path23.default.join(repoPath, ".git")).isDirectory();
10263
+ return import_node_fs25.default.statSync(import_node_path26.default.join(repoPath, ".git")).isDirectory();
9457
10264
  } catch {
9458
10265
  return false;
9459
10266
  }
9460
10267
  }
9461
10268
  function openUrls(urls) {
9462
10269
  for (const url of urls) {
9463
- const opened = (0, import_node_child_process19.spawnSync)("open", [url], { encoding: "utf-8" });
10270
+ const opened = (0, import_node_child_process21.spawnSync)("open", [url], { encoding: "utf-8" });
9464
10271
  if (opened.status !== 0) {
9465
10272
  process.stderr.write(`Warning: could not open '${url}'.
9466
10273
  `);
@@ -9499,12 +10306,16 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
9499
10306
  let managedConfigWritten = false;
9500
10307
  let managedWorkspaceEnv;
9501
10308
  let firstTransitionBaseline;
10309
+ const routeLoadOptions = {
10310
+ initialTimeoutMs: Math.min(options.httpTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS, 3e3),
10311
+ recoveryTimeoutMs: Math.min(options.httpTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS, 1e4)
10312
+ };
9502
10313
  try {
9503
10314
  const target = linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
9504
10315
  let devpodId = target.devpodId;
9505
10316
  if (target.kind === "linked") {
9506
- const overlayPath = import_node_path23.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
9507
- if (!import_node_fs23.default.existsSync(overlayPath)) {
10317
+ const overlayPath = import_node_path26.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
10318
+ if (!import_node_fs25.default.existsSync(overlayPath)) {
9508
10319
  throw new Error(`Missing required DevPod compose overlay: ${overlayPath}`);
9509
10320
  }
9510
10321
  }
@@ -9521,6 +10332,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
9521
10332
  const desiredProcesses = managedRuntime ? desiredManagedProcesses(managedRuntime, runtime.resolvedProfile) : [];
9522
10333
  if (managedRuntime) {
9523
10334
  previousManagedState = readManagedRuntimeState(repoPath, target.workspace);
10335
+ if (previousManagedState && !hasExactManagedComposeProject(repoPath, previousManagedState)) {
10336
+ previousManagedState = void 0;
10337
+ }
9524
10338
  if (previousManagedState?.status === "degraded") {
9525
10339
  throw new Error(
9526
10340
  "Managed runtime state is degraded; refusing a new profile transition until drift is repaired."
@@ -9571,7 +10385,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
9571
10385
  }
9572
10386
  const apps = proxyAppsFromConfig(runtime.config);
9573
10387
  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";
10388
+ const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path26.default.basename(repoPath)) ?? "app";
9575
10389
  for (const [index, app] of apps.entries()) {
9576
10390
  if (!parsedUpstreams[index].host.startsWith(`${aliasPrefix}-`)) {
9577
10391
  const owner = target.kind === "linked" ? "workspace" : "checkout";
@@ -9586,10 +10400,10 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
9586
10400
  }
9587
10401
  const upstreamHosts = parsedUpstreams.map((upstream) => upstream.host);
9588
10402
  const currentTarget = () => target.kind === "linked" ? target : { ...target, devpodId };
9589
- const startAndProveAttachment = (recreate = false) => {
10403
+ const startAndProveAttachment = async (recreate = false) => {
9590
10404
  const requestedTarget = currentTarget();
9591
10405
  try {
9592
- devpodId = startDevpodWorkspace({
10406
+ devpodId = await startDevpodWorkspace({
9593
10407
  repoPath,
9594
10408
  devpodId: requestedTarget.devpodId,
9595
10409
  devcontainerPath: managedPlan?.generatedRelativePath,
@@ -9610,13 +10424,13 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
9610
10424
  };
9611
10425
  const preflight = (timeoutMs) => waitForContainerPreflight(repoPath, currentTarget(), upstreamHosts, timeoutMs);
9612
10426
  const recreateAndPreflight = async () => {
9613
- startAndProveAttachment(true);
10427
+ await startAndProveAttachment(true);
9614
10428
  return preflight(options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS);
9615
10429
  };
9616
10430
  let recreated = false;
9617
10431
  let container;
9618
10432
  try {
9619
- startAndProveAttachment();
10433
+ await startAndProveAttachment();
9620
10434
  } catch (error) {
9621
10435
  if (managedPlan || !target.hadExactDevpod) {
9622
10436
  throw error;
@@ -9735,6 +10549,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
9735
10549
  target.workspace
9736
10550
  );
9737
10551
  candidateRoutesPublished = true;
10552
+ await ensureTraefikRoutesLoaded(publication.routes, routeLoadOptions);
9738
10553
  try {
9739
10554
  await waitForHttpRoutes(
9740
10555
  repoPath,
@@ -9758,6 +10573,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
9758
10573
  });
9759
10574
  recreated = true;
9760
10575
  replaceHostRoutesForRepo(repoPath, publication.routes);
10576
+ await ensureTraefikRoutesLoaded(publication.routes, routeLoadOptions);
9761
10577
  await waitForHttpRoutes(
9762
10578
  repoPath,
9763
10579
  apps,
@@ -9931,7 +10747,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
9931
10747
  }
9932
10748
  if (candidateRoutesPublished) {
9933
10749
  try {
9934
- replaceHostRoutesForRepo(repoPath, previousRoutes.map(routeInputFromState));
10750
+ const rollbackRoutes = previousRoutes.map(routeInputFromState);
10751
+ replaceHostRoutesForRepo(repoPath, rollbackRoutes);
10752
+ await ensureTraefikRoutesLoaded(rollbackRoutes, routeLoadOptions);
9935
10753
  } catch (rollbackError) {
9936
10754
  rollbackErrors.push(
9937
10755
  `routes: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
@@ -9974,7 +10792,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
9974
10792
  }
9975
10793
  if (candidateRoutesPublished) {
9976
10794
  try {
9977
- replaceHostRoutesForRepo(repoPath, previousRoutes.map(routeInputFromState));
10795
+ const rollbackRoutes = previousRoutes.map(routeInputFromState);
10796
+ replaceHostRoutesForRepo(repoPath, rollbackRoutes);
10797
+ await ensureTraefikRoutesLoaded(rollbackRoutes, routeLoadOptions);
9978
10798
  } catch (rollbackError) {
9979
10799
  rollbackErrors.push(
9980
10800
  `routes: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
@@ -10003,13 +10823,13 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
10003
10823
  }
10004
10824
  });
10005
10825
  }
10006
- var import_node_child_process19, import_node_fs23, import_node_path23, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
10826
+ var import_node_child_process21, import_node_fs25, import_node_path26, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
10007
10827
  var init_workspace_ensure = __esm({
10008
10828
  "src/core/workspace-ensure.ts"() {
10009
10829
  "use strict";
10010
- import_node_child_process19 = require("child_process");
10011
- import_node_fs23 = __toESM(require("fs"));
10012
- import_node_path23 = __toESM(require("path"));
10830
+ import_node_child_process21 = require("child_process");
10831
+ import_node_fs25 = __toESM(require("fs"));
10832
+ import_node_path26 = __toESM(require("path"));
10013
10833
  init_devcontainer_profile();
10014
10834
  init_devpod_environment();
10015
10835
  init_devpod_mutation();
@@ -10022,6 +10842,7 @@ var init_workspace_ensure = __esm({
10022
10842
  init_repo_config();
10023
10843
  init_route_publication();
10024
10844
  init_router();
10845
+ init_traefik_route_health();
10025
10846
  init_workspace();
10026
10847
  init_workspace_ownership();
10027
10848
  init_workspace_runtime();
@@ -10095,20 +10916,20 @@ function warnMissingWorkspaceOwnership(repoPath) {
10095
10916
  );
10096
10917
  }
10097
10918
  function defaultWorktreePath(mainRepo, ws) {
10098
- return import_node_path24.default.join(mainRepo, "trees", ws);
10919
+ return import_node_path27.default.join(mainRepo, "trees", ws);
10099
10920
  }
10100
10921
  function assertDefaultWorktreeRootIgnored(mainRepo) {
10101
- const ignored = (0, import_node_child_process20.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
10922
+ const ignored = (0, import_node_child_process22.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
10102
10923
  encoding: "utf-8"
10103
10924
  });
10104
10925
  if (ignored.status !== 0) {
10105
10926
  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.`
10927
+ `Default worktree root '${import_node_path27.default.join(mainRepo, "trees")}' is not ignored. Add 'trees/' to '${import_node_path27.default.join(mainRepo, ".gitignore")}' or use --path.`
10107
10928
  );
10108
10929
  }
10109
10930
  }
10110
10931
  function legacyDefaultWorktreePath(mainRepo, ws) {
10111
- return import_node_path24.default.join(import_node_path24.default.dirname(mainRepo), `${import_node_path24.default.basename(mainRepo)}-${ws}`);
10932
+ return import_node_path27.default.join(import_node_path27.default.dirname(mainRepo), `${import_node_path27.default.basename(mainRepo)}-${ws}`);
10112
10933
  }
10113
10934
  function teardownFallbackPath(mainRepo, workspace) {
10114
10935
  const candidates = [
@@ -10220,13 +11041,13 @@ function assertFullDownPreflight(mainRepo, target) {
10220
11041
  if (sameWorkspacePath(target.worktreePath, mainRepo)) {
10221
11042
  throw new Error("Refusing to remove the primary Git checkout.");
10222
11043
  }
10223
- if (!target.worktree || target.worktree.prunable || !import_node_fs24.default.existsSync(target.worktreePath)) return;
11044
+ if (!target.worktree || target.worktree.prunable || !import_node_fs26.default.existsSync(target.worktreePath)) return;
10224
11045
  if (target.worktree.locked) {
10225
11046
  throw new Error(
10226
11047
  `Worktree '${target.worktreePath}' is locked; unlock it before workspace down.`
10227
11048
  );
10228
11049
  }
10229
- const status = (0, import_node_child_process20.spawnSync)(
11050
+ const status = (0, import_node_child_process22.spawnSync)(
10230
11051
  "git",
10231
11052
  ["-C", target.worktreePath, "status", "--porcelain", "--untracked-files=normal"],
10232
11053
  { encoding: "utf-8" }
@@ -10260,9 +11081,9 @@ async function workspaceUp(branch, opts = {}) {
10260
11081
  (candidate) => defaultWorktreePath(mainRepo, candidate)
10261
11082
  );
10262
11083
  const generatedPath = candidatePaths.find(
10263
- (candidate) => !import_node_fs24.default.existsSync(candidate) && !worktrees.some((worktree) => sameWorkspacePath(worktree.path, candidate))
11084
+ (candidate) => !import_node_fs26.default.existsSync(candidate) && !worktrees.some((worktree) => sameWorkspacePath(worktree.path, candidate))
10264
11085
  );
10265
- const selectedPath = opts.path ? import_node_path24.default.resolve(opts.path) : existingBranch?.path ?? generatedPath;
11086
+ const selectedPath = opts.path ? import_node_path27.default.resolve(opts.path) : existingBranch?.path ?? generatedPath;
10266
11087
  if (!selectedPath) {
10267
11088
  throw new Error(`Could not allocate a collision-safe worktree path for branch '${branch}'.`);
10268
11089
  }
@@ -10271,7 +11092,7 @@ async function workspaceUp(branch, opts = {}) {
10271
11092
  `Branch '${branch}' already uses worktree '${existingBranch.path}', not '${selectedPath}'.`
10272
11093
  );
10273
11094
  }
10274
- if (import_node_fs24.default.existsSync(selectedPath)) {
11095
+ if (import_node_fs26.default.existsSync(selectedPath)) {
10275
11096
  const registered = worktrees.find(
10276
11097
  (worktree) => sameWorkspacePath(worktree.path, selectedPath)
10277
11098
  );
@@ -10290,11 +11111,11 @@ async function workspaceUp(branch, opts = {}) {
10290
11111
  return selectedPath;
10291
11112
  }
10292
11113
  if (!opts.path) assertDefaultWorktreeRootIgnored(mainRepo);
10293
- const add = (0, import_node_child_process20.spawnSync)("git", ["-C", mainRepo, "worktree", "add", selectedPath, branch], {
11114
+ const add = (0, import_node_child_process22.spawnSync)("git", ["-C", mainRepo, "worktree", "add", selectedPath, branch], {
10294
11115
  encoding: "utf-8"
10295
11116
  });
10296
11117
  if (add.status !== 0) {
10297
- const addNew = (0, import_node_child_process20.spawnSync)(
11118
+ const addNew = (0, import_node_child_process22.spawnSync)(
10298
11119
  "git",
10299
11120
  ["-C", mainRepo, "worktree", "add", "-b", branch, selectedPath],
10300
11121
  { encoding: "utf-8" }
@@ -10396,8 +11217,8 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
10396
11217
  opts.quiet
10397
11218
  );
10398
11219
  if (removeWorktree) {
10399
- if (resolved.worktree && !resolved.worktree.prunable && import_node_fs24.default.existsSync(resolved.worktreePath)) {
10400
- const rm = (0, import_node_child_process20.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
11220
+ if (resolved.worktree && !resolved.worktree.prunable && import_node_fs26.default.existsSync(resolved.worktreePath)) {
11221
+ const rm = (0, import_node_child_process22.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
10401
11222
  encoding: "utf-8"
10402
11223
  });
10403
11224
  if (rm.status !== 0) {
@@ -10415,7 +11236,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
10415
11236
  }
10416
11237
  return result;
10417
11238
  };
10418
- return resolved.worktree && !resolved.worktree.prunable && import_node_fs24.default.existsSync(resolved.worktreePath) ? withWorkspaceLifecycleLock(resolved.worktreePath, operation) : operation();
11239
+ return resolved.worktree && !resolved.worktree.prunable && import_node_fs26.default.existsSync(resolved.worktreePath) ? withWorkspaceLifecycleLock(resolved.worktreePath, operation) : operation();
10419
11240
  }
10420
11241
  async function mutateWorkspaceOwnedPath(action2, worktreePath, opts = {}) {
10421
11242
  const mainRepo = resolveRepoPath(opts.repoPath);
@@ -10450,13 +11271,13 @@ async function workspaceStop(target, opts = {}) {
10450
11271
  async function workspaceDown(target, opts = {}) {
10451
11272
  return runWorkspaceLifecycle("down", target, opts);
10452
11273
  }
10453
- var import_node_child_process20, import_node_fs24, import_node_path24, WORKSPACE_ALLOCATION_LOCK_WAIT_MS;
11274
+ var import_node_child_process22, import_node_fs26, import_node_path27, WORKSPACE_ALLOCATION_LOCK_WAIT_MS;
10454
11275
  var init_workspace_lifecycle = __esm({
10455
11276
  "src/core/workspace-lifecycle.ts"() {
10456
11277
  "use strict";
10457
- import_node_child_process20 = require("child_process");
10458
- import_node_fs24 = __toESM(require("fs"));
10459
- import_node_path24 = __toESM(require("path"));
11278
+ import_node_child_process22 = require("child_process");
11279
+ import_node_fs26 = __toESM(require("fs"));
11280
+ import_node_path27 = __toESM(require("path"));
10460
11281
  init_devpod_mutation();
10461
11282
  init_devpod_workspaces();
10462
11283
  init_repo_config();
@@ -10585,17 +11406,17 @@ async function devsyExec(repoPath, command) {
10585
11406
  }
10586
11407
  const args = ["workspace", "exec", "--result-format", "plain", workspace.id, "--", ...command];
10587
11408
  return new Promise((resolve, reject) => {
10588
- const child = (0, import_node_child_process21.spawn)("devsy", args, { stdio: "inherit" });
11409
+ const child = (0, import_node_child_process23.spawn)("devsy", args, { stdio: "inherit" });
10589
11410
  child.once("error", (error) => reject(new Error(`devsy exec failed: ${error.message}`)));
10590
11411
  child.once("close", (code) => resolve(code ?? 1));
10591
11412
  });
10592
11413
  });
10593
11414
  }
10594
- var import_node_child_process21;
11415
+ var import_node_child_process23;
10595
11416
  var init_devsy_exec = __esm({
10596
11417
  "src/core/devsy-exec.ts"() {
10597
11418
  "use strict";
10598
- import_node_child_process21 = require("child_process");
11419
+ import_node_child_process23 = require("child_process");
10599
11420
  init_devsy_workspaces();
10600
11421
  init_workspace();
10601
11422
  }
@@ -10617,7 +11438,7 @@ function ensureGuidance2(repoPath) {
10617
11438
  return `Run 'devrouter ensure ${repoPath}' first.`;
10618
11439
  }
10619
11440
  function assertRunningDevpod(devpodId, repoPath) {
10620
- const result = (0, import_node_child_process22.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
11441
+ const result = (0, import_node_child_process24.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
10621
11442
  encoding: "utf-8"
10622
11443
  });
10623
11444
  if (result.status !== 0) {
@@ -10658,7 +11479,7 @@ async function devpodExec(repoPath, command) {
10658
11479
  }
10659
11480
  assertRunningDevpod(devpod.id, repoPath);
10660
11481
  const workspaceDirectory = resolveWorkspaceDirectory(repoPath);
10661
- const statusMarker = `__DEVROUTER_EXIT_${(0, import_node_crypto8.randomUUID)()}__:`;
11482
+ const statusMarker = `__DEVROUTER_EXIT_${(0, import_node_crypto9.randomUUID)()}__:`;
10662
11483
  const statusMarkerBytes = Buffer.from(statusMarker, "ascii");
10663
11484
  const literalCommand = command.map(quotePosixArg).join(" ");
10664
11485
  const wrappedCommand = `${literalCommand}; __devrouter_status=$?; printf '${statusMarker}%s\\n' "$__devrouter_status" >&2; exit 0`;
@@ -10676,7 +11497,7 @@ async function devpodExec(repoPath, command) {
10676
11497
  wrappedCommand
10677
11498
  ];
10678
11499
  return new Promise((resolve, reject) => {
10679
- const child = (0, import_node_child_process22.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
11500
+ const child = (0, import_node_child_process24.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
10680
11501
  let pending = Buffer.alloc(0);
10681
11502
  let statusBytes = Buffer.alloc(0);
10682
11503
  let readingStatus = false;
@@ -10735,12 +11556,12 @@ async function devpodExec(repoPath, command) {
10735
11556
  });
10736
11557
  });
10737
11558
  }
10738
- var import_node_child_process22, import_node_crypto8, DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC;
11559
+ var import_node_child_process24, import_node_crypto9, DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC;
10739
11560
  var init_devpod_exec = __esm({
10740
11561
  "src/core/devpod-exec.ts"() {
10741
11562
  "use strict";
10742
- import_node_child_process22 = require("child_process");
10743
- import_node_crypto8 = require("crypto");
11563
+ import_node_child_process24 = require("child_process");
11564
+ import_node_crypto9 = require("crypto");
10744
11565
  init_devpod_environment();
10745
11566
  init_devpod_workspaces();
10746
11567
  init_devsy_exec();
@@ -10926,7 +11747,7 @@ async function runOpenCommand(name) {
10926
11747
  );
10927
11748
  return;
10928
11749
  }
10929
- const result = (0, import_node_child_process23.spawnSync)("open", [url], { encoding: "utf-8" });
11750
+ const result = (0, import_node_child_process25.spawnSync)("open", [url], { encoding: "utf-8" });
10930
11751
  if (result.status !== 0) {
10931
11752
  const details = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
10932
11753
  throw new Error(`Unable to open '${url}': ${details || "unknown error"}`);
@@ -10934,11 +11755,11 @@ async function runOpenCommand(name) {
10934
11755
  process.stdout.write(`Opened ${url}
10935
11756
  `);
10936
11757
  }
10937
- var import_node_child_process23;
11758
+ var import_node_child_process25;
10938
11759
  var init_open = __esm({
10939
11760
  "src/commands/open.ts"() {
10940
11761
  "use strict";
10941
- import_node_child_process23 = require("child_process");
11762
+ import_node_child_process25 = require("child_process");
10942
11763
  init_docker();
10943
11764
  init_host_routes();
10944
11765
  init_repo_config();
@@ -10961,7 +11782,7 @@ async function runLogsCommand(options) {
10961
11782
  const args = ["logs", "--tail", tail, ROUTER_CONTAINER_NAME];
10962
11783
  if (options.follow) {
10963
11784
  args.splice(1, 0, "-f");
10964
- const child = (0, import_node_child_process24.spawn)("docker", args, { stdio: "inherit" });
11785
+ const child = (0, import_node_child_process26.spawn)("docker", args, { stdio: "inherit" });
10965
11786
  const onSignal = () => {
10966
11787
  child.kill("SIGTERM");
10967
11788
  };
@@ -10975,17 +11796,17 @@ async function runLogsCommand(options) {
10975
11796
  });
10976
11797
  });
10977
11798
  } else {
10978
- const result = (0, import_node_child_process24.spawnSync)("docker", args, { stdio: "inherit" });
11799
+ const result = (0, import_node_child_process26.spawnSync)("docker", args, { stdio: "inherit" });
10979
11800
  if (result.status !== 0) {
10980
11801
  throw new Error("Failed to retrieve router logs.");
10981
11802
  }
10982
11803
  }
10983
11804
  }
10984
- var import_node_child_process24;
11805
+ var import_node_child_process26;
10985
11806
  var init_logs = __esm({
10986
11807
  "src/commands/logs.ts"() {
10987
11808
  "use strict";
10988
- import_node_child_process24 = require("child_process");
11809
+ import_node_child_process26 = require("child_process");
10989
11810
  init_docker();
10990
11811
  init_router();
10991
11812
  }
@@ -11025,17 +11846,17 @@ function asRecord3(value) {
11025
11846
  return value;
11026
11847
  }
11027
11848
  function readJson(filePath) {
11028
- if (!import_node_fs25.default.existsSync(filePath)) {
11849
+ if (!import_node_fs27.default.existsSync(filePath)) {
11029
11850
  return void 0;
11030
11851
  }
11031
11852
  try {
11032
- return JSON.parse(import_node_fs25.default.readFileSync(filePath, "utf-8"));
11853
+ return JSON.parse(import_node_fs27.default.readFileSync(filePath, "utf-8"));
11033
11854
  } catch {
11034
11855
  return void 0;
11035
11856
  }
11036
11857
  }
11037
11858
  function relative(repoPath, filePath) {
11038
- return import_node_path25.default.relative(repoPath, filePath) || ".";
11859
+ return import_node_path28.default.relative(repoPath, filePath) || ".";
11039
11860
  }
11040
11861
  function redactEnvAssignments(value) {
11041
11862
  return value.replace(
@@ -11076,7 +11897,7 @@ function inspectPackageManager(repoPath, pkg) {
11076
11897
  ["bun.lock", "bun"]
11077
11898
  ];
11078
11899
  for (const [fileName, name] of lockfiles) {
11079
- if (import_node_fs25.default.existsSync(import_node_path25.default.join(repoPath, fileName))) {
11900
+ if (import_node_fs27.default.existsSync(import_node_path28.default.join(repoPath, fileName))) {
11080
11901
  return { name, source: fileName };
11081
11902
  }
11082
11903
  }
@@ -11091,9 +11912,9 @@ function inspectNode(repoPath, pkg) {
11091
11912
  if (typeof engines?.node === "string") {
11092
11913
  return { version: engines.node, source: "package.json:engines.node" };
11093
11914
  }
11094
- const nvmrc = import_node_path25.default.join(repoPath, ".nvmrc");
11095
- if (import_node_fs25.default.existsSync(nvmrc)) {
11096
- const version = import_node_fs25.default.readFileSync(nvmrc, "utf-8").trim();
11915
+ const nvmrc = import_node_path28.default.join(repoPath, ".nvmrc");
11916
+ if (import_node_fs27.default.existsSync(nvmrc)) {
11917
+ const version = import_node_fs27.default.readFileSync(nvmrc, "utf-8").trim();
11097
11918
  return { version, source: ".nvmrc" };
11098
11919
  }
11099
11920
  return void 0;
@@ -11154,7 +11975,7 @@ function configuredComposeFiles(repoPath) {
11154
11975
  const files = config.apps.filter(
11155
11976
  (app) => app.runtime === "docker"
11156
11977
  ).flatMap((app) => app.docker.composeFiles).filter(
11157
- (fileName) => !import_node_path25.default.isAbsolute(fileName) && !import_node_path25.default.normalize(fileName).startsWith("..")
11978
+ (fileName) => !import_node_path28.default.isAbsolute(fileName) && !import_node_path28.default.normalize(fileName).startsWith("..")
11158
11979
  );
11159
11980
  return Array.from(new Set(files));
11160
11981
  } catch {
@@ -11172,7 +11993,7 @@ function composeFiles(repoPath) {
11172
11993
  ...configuredComposeFiles(repoPath)
11173
11994
  ];
11174
11995
  return Array.from(new Set(candidates)).filter(
11175
- (fileName) => import_node_fs25.default.existsSync(import_node_path25.default.join(repoPath, fileName))
11996
+ (fileName) => import_node_fs27.default.existsSync(import_node_path28.default.join(repoPath, fileName))
11176
11997
  );
11177
11998
  }
11178
11999
  function stringArray2(value) {
@@ -11210,7 +12031,7 @@ function inspectServices(repoPath) {
11210
12031
  const services = [];
11211
12032
  for (const fileName of composeFiles(repoPath)) {
11212
12033
  try {
11213
- const parsed = import_yaml6.default.parse(import_node_fs25.default.readFileSync(import_node_path25.default.join(repoPath, fileName), "utf-8"));
12034
+ const parsed = import_yaml6.default.parse(import_node_fs27.default.readFileSync(import_node_path28.default.join(repoPath, fileName), "utf-8"));
11214
12035
  const serviceMap = asRecord3(asRecord3(parsed)?.services);
11215
12036
  for (const [name, value] of Object.entries(serviceMap ?? {})) {
11216
12037
  const service = asRecord3(value);
@@ -11245,8 +12066,8 @@ function inspectServices(repoPath) {
11245
12066
  return services;
11246
12067
  }
11247
12068
  function inspectEnvFiles(repoPath) {
11248
- 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");
12069
+ const files = import_node_fs27.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
12070
+ const content = import_node_fs27.default.readFileSync(import_node_path28.default.join(repoPath, fileName), "utf-8");
11250
12071
  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
12072
  return { path: fileName, names };
11252
12073
  });
@@ -11260,18 +12081,18 @@ function inspectEnvFiles(repoPath) {
11260
12081
  };
11261
12082
  }
11262
12083
  function inspectDevcontainer(repoPath) {
11263
- const dir = import_node_path25.default.join(repoPath, ".devcontainer");
11264
- if (!import_node_fs25.default.existsSync(dir)) {
12084
+ const dir = import_node_path28.default.join(repoPath, ".devcontainer");
12085
+ if (!import_node_fs27.default.existsSync(dir)) {
11265
12086
  return { exists: false, files: [] };
11266
12087
  }
11267
12088
  return {
11268
12089
  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}`)
12090
+ files: import_node_fs27.default.readdirSync(dir).filter((fileName) => import_node_fs27.default.statSync(import_node_path28.default.join(dir, fileName)).isFile()).sort().map((fileName) => `.devcontainer/${fileName}`)
11270
12091
  };
11271
12092
  }
11272
12093
  function inspectDevrouter(repoPath) {
11273
12094
  const configPath = getRepoConfigPath(repoPath);
11274
- if (!import_node_fs25.default.existsSync(configPath)) {
12095
+ if (!import_node_fs27.default.existsSync(configPath)) {
11275
12096
  return {
11276
12097
  exists: false,
11277
12098
  configPath,
@@ -11315,15 +12136,15 @@ function inspectAgentGuidance(repoPath) {
11315
12136
  ["AGENTS.md", "agents"],
11316
12137
  ["CLAUDE.md", "claude"]
11317
12138
  ]) {
11318
- if (import_node_fs25.default.existsSync(import_node_path25.default.join(repoPath, fileName))) {
12139
+ if (import_node_fs27.default.existsSync(import_node_path28.default.join(repoPath, fileName))) {
11319
12140
  results.push({ path: fileName, kind });
11320
12141
  }
11321
12142
  }
11322
- const skillsDir = import_node_path25.default.join(repoPath, ".agents", "skills");
11323
- if (import_node_fs25.default.existsSync(skillsDir)) {
11324
- for (const name of import_node_fs25.default.readdirSync(skillsDir).sort()) {
11325
- const skillPath = import_node_path25.default.join(skillsDir, name, "SKILL.md");
11326
- if (import_node_fs25.default.existsSync(skillPath)) {
12143
+ const skillsDir = import_node_path28.default.join(repoPath, ".agents", "skills");
12144
+ if (import_node_fs27.default.existsSync(skillsDir)) {
12145
+ for (const name of import_node_fs27.default.readdirSync(skillsDir).sort()) {
12146
+ const skillPath = import_node_path28.default.join(skillsDir, name, "SKILL.md");
12147
+ if (import_node_fs27.default.existsSync(skillPath)) {
11327
12148
  results.push({ path: relative(repoPath, skillPath), kind: "skill" });
11328
12149
  }
11329
12150
  }
@@ -11366,7 +12187,7 @@ function buildIssues(report) {
11366
12187
  }
11367
12188
  function inspectRepo(options = {}) {
11368
12189
  const repoPath = resolveRepoPath(options.repo);
11369
- const pkg = readJson(import_node_path25.default.join(repoPath, "package.json"));
12190
+ const pkg = readJson(import_node_path28.default.join(repoPath, "package.json"));
11370
12191
  const scripts = inspectScripts(pkg);
11371
12192
  const reportWithoutIssues = {
11372
12193
  repoPath,
@@ -11385,12 +12206,12 @@ function inspectRepo(options = {}) {
11385
12206
  issues: buildIssues(reportWithoutIssues)
11386
12207
  };
11387
12208
  }
11388
- var import_node_fs25, import_node_path25, import_yaml6;
12209
+ var import_node_fs27, import_node_path28, import_yaml6;
11389
12210
  var init_repo_inspect = __esm({
11390
12211
  "src/core/repo-inspect.ts"() {
11391
12212
  "use strict";
11392
- import_node_fs25 = __toESM(require("fs"));
11393
- import_node_path25 = __toESM(require("path"));
12213
+ import_node_fs27 = __toESM(require("fs"));
12214
+ import_node_path28 = __toESM(require("path"));
11394
12215
  import_yaml6 = __toESM(require("yaml"));
11395
12216
  init_repo_config();
11396
12217
  }
@@ -11495,7 +12316,7 @@ function requiredFileChecks(repoPath) {
11495
12316
  ".devcontainer/docker-compose.yml",
11496
12317
  ".devrouter.yml"
11497
12318
  ];
11498
- const missing = required.filter((fileName) => !import_node_fs26.default.existsSync(import_node_path26.default.join(repoPath, fileName)));
12319
+ const missing = required.filter((fileName) => !import_node_fs28.default.existsSync(import_node_path29.default.join(repoPath, fileName)));
11499
12320
  return {
11500
12321
  id: "repo.devcontainer.verify-files",
11501
12322
  level: missing.length === 0 ? "ok" : "error",
@@ -11714,12 +12535,12 @@ async function verifyDevcontainer(options = {}) {
11714
12535
  nextSteps: collectNextSteps3(checks)
11715
12536
  };
11716
12537
  }
11717
- var import_node_fs26, import_node_path26;
12538
+ var import_node_fs28, import_node_path29;
11718
12539
  var init_devcontainer_verify = __esm({
11719
12540
  "src/core/devcontainer-verify.ts"() {
11720
12541
  "use strict";
11721
- import_node_fs26 = __toESM(require("fs"));
11722
- import_node_path26 = __toESM(require("path"));
12542
+ import_node_fs28 = __toESM(require("fs"));
12543
+ import_node_path29 = __toESM(require("path"));
11723
12544
  init_capabilities();
11724
12545
  init_doctor();
11725
12546
  init_host_routes();
@@ -12016,7 +12837,7 @@ function packageManagerIssues(repo) {
12016
12837
  }
12017
12838
  function plannedFiles(repoPath, version) {
12018
12839
  const repo = inspectRepo({ repo: repoPath });
12019
- const projectName = sanitizeProjectName(import_node_path27.default.basename(repo.repoPath));
12840
+ const projectName = sanitizeProjectName(import_node_path30.default.basename(repo.repoPath));
12020
12841
  const nodeMajor = majorVersion(repo.node?.version, "24");
12021
12842
  const pnpmVersion = repo.packageManager?.name === "pnpm" && repo.packageManager.version ? repo.packageManager.version : DEFAULT_PNPM_VERSION;
12022
12843
  const port = inferPort2(repo);
@@ -12061,8 +12882,8 @@ function plannedFiles(repoPath, version) {
12061
12882
  };
12062
12883
  }
12063
12884
  function classifyFile(repoPath, file) {
12064
- const absolutePath = import_node_path27.default.join(repoPath, file.relativePath);
12065
- if (!import_node_fs27.default.existsSync(absolutePath)) {
12885
+ const absolutePath = import_node_path30.default.join(repoPath, file.relativePath);
12886
+ if (!import_node_fs29.default.existsSync(absolutePath)) {
12066
12887
  return {
12067
12888
  path: file.relativePath,
12068
12889
  action: "create",
@@ -12070,7 +12891,7 @@ function classifyFile(repoPath, file) {
12070
12891
  bytes: Buffer.byteLength(file.content)
12071
12892
  };
12072
12893
  }
12073
- const current = import_node_fs27.default.readFileSync(absolutePath, "utf-8");
12894
+ const current = import_node_fs29.default.readFileSync(absolutePath, "utf-8");
12074
12895
  if (!current.includes(MANAGED_MARKER2)) {
12075
12896
  return {
12076
12897
  path: file.relativePath,
@@ -12127,11 +12948,11 @@ function buildPlan(repoPath, dryRun, version) {
12127
12948
  };
12128
12949
  }
12129
12950
  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 });
12132
- import_node_fs27.default.writeFileSync(absolutePath, file.content, "utf-8");
12951
+ const absolutePath = import_node_path30.default.join(repoPath, file.relativePath);
12952
+ import_node_fs29.default.mkdirSync(import_node_path30.default.dirname(absolutePath), { recursive: true });
12953
+ import_node_fs29.default.writeFileSync(absolutePath, file.content, "utf-8");
12133
12954
  if (file.executable) {
12134
- import_node_fs27.default.chmodSync(absolutePath, 493);
12955
+ import_node_fs29.default.chmodSync(absolutePath, 493);
12135
12956
  }
12136
12957
  }
12137
12958
  function writeDevcontainer(options = {}) {
@@ -12169,12 +12990,12 @@ function writeDevcontainer(options = {}) {
12169
12990
  nextSteps: postWriteNextSteps(repoPath)
12170
12991
  };
12171
12992
  }
12172
- var import_node_fs27, import_node_path27, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
12993
+ var import_node_fs29, import_node_path30, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
12173
12994
  var init_devcontainer_write = __esm({
12174
12995
  "src/core/devcontainer-write.ts"() {
12175
12996
  "use strict";
12176
- import_node_fs27 = __toESM(require("fs"));
12177
- import_node_path27 = __toESM(require("path"));
12997
+ import_node_fs29 = __toESM(require("fs"));
12998
+ import_node_path30 = __toESM(require("path"));
12178
12999
  init_repo_config();
12179
13000
  init_repo_inspect();
12180
13001
  MANAGED_MARKER2 = "devrouter:managed devcontainer";
@@ -12283,6 +13104,119 @@ var init_repo_devcontainer = __esm({
12283
13104
  }
12284
13105
  });
12285
13106
 
13107
+ // src/core/profile-resolution.ts
13108
+ function sortedUnique2(values) {
13109
+ return Array.from(new Set(values)).sort();
13110
+ }
13111
+ function expandsAll(values) {
13112
+ return values?.length === 1 && values[0] === "*";
13113
+ }
13114
+ function selectedManagedResources(profile, selected, registry) {
13115
+ if (!profile || expandsAll(selected)) return sortedUnique2(registry);
13116
+ return sortedUnique2(selected ?? []);
13117
+ }
13118
+ function selectedReadiness(config, profileName, profile, routedApps) {
13119
+ const selectedNames = profileName.split(",");
13120
+ if (selectedNames.length === 1) {
13121
+ return sortedUnique2(
13122
+ profile?.readiness ?? routedApps.filter((app) => app.protocol === "http").map((app) => app.name)
13123
+ );
13124
+ }
13125
+ const readiness = /* @__PURE__ */ new Set();
13126
+ for (const selectedName of selectedNames) {
13127
+ const selected = resolveProfile(config, selectedName);
13128
+ const selectedApps = applyProfile(config, selected.profile).apps.filter(
13129
+ (app) => app.kind !== "dependency"
13130
+ );
13131
+ const selectedTargets = selected.profile?.readiness ?? selectedApps.filter((app) => app.protocol === "http").map((app) => app.name);
13132
+ for (const target of selectedTargets) readiness.add(target);
13133
+ }
13134
+ return sortedUnique2(readiness);
13135
+ }
13136
+ function buildProfileResolutionReport(config, repoPath, profileOverride) {
13137
+ const resolved = resolveProfile(config, profileOverride);
13138
+ const filtered = applyProfile(config, resolved.profile);
13139
+ const routedApps = filtered.apps.filter((app) => app.kind !== "dependency");
13140
+ const dependencies = filtered.apps.filter((app) => app.kind === "dependency");
13141
+ const readiness = selectedReadiness(config, resolved.name, resolved.profile, routedApps);
13142
+ const managedRuntime = config.managedRuntime;
13143
+ const baseServices = sortedUnique2(managedRuntime?.devcontainer.baseServices ?? []);
13144
+ const profileServices = managedRuntime ? selectedManagedResources(
13145
+ resolved.profile,
13146
+ resolved.profile?.devcontainerServices,
13147
+ managedRuntime.devcontainer.profileServices
13148
+ ) : [];
13149
+ const processes = managedRuntime ? selectedManagedResources(
13150
+ resolved.profile,
13151
+ resolved.profile?.processes,
13152
+ managedRuntime.processes
13153
+ ) : [];
13154
+ return {
13155
+ schemaVersion: 1,
13156
+ repoPath,
13157
+ profile: resolved.name,
13158
+ apps: sortedUnique2(routedApps.map((app) => app.name)),
13159
+ dependencies: sortedUnique2(dependencies.map((app) => app.name)),
13160
+ readiness: sortedUnique2(readiness),
13161
+ managedRuntime: {
13162
+ baseServices,
13163
+ profileServices,
13164
+ services: sortedUnique2([...baseServices, ...profileServices]),
13165
+ processes
13166
+ }
13167
+ };
13168
+ }
13169
+ function resolveProfileReport(options) {
13170
+ const repoPath = resolveRepoPath(options.repo);
13171
+ return buildProfileResolutionReport(loadRepoConfig(repoPath), repoPath, options.profile);
13172
+ }
13173
+ var init_profile_resolution = __esm({
13174
+ "src/core/profile-resolution.ts"() {
13175
+ "use strict";
13176
+ init_repo_config();
13177
+ }
13178
+ });
13179
+
13180
+ // src/commands/profile.ts
13181
+ var profile_exports = {};
13182
+ __export(profile_exports, {
13183
+ runProfileResolveCommand: () => runProfileResolveCommand
13184
+ });
13185
+ async function runProfileResolveCommand(options) {
13186
+ const report = resolveProfileReport(options);
13187
+ if (options.json) {
13188
+ printJSON(report);
13189
+ return;
13190
+ }
13191
+ printProfileResolutionSummary(report);
13192
+ }
13193
+ function renderValues(values) {
13194
+ return values.join(", ") || "-";
13195
+ }
13196
+ function printProfileResolutionSummary(report) {
13197
+ process.stdout.write(`Repo: ${report.repoPath}
13198
+ `);
13199
+ process.stdout.write(`Profile: ${report.profile}
13200
+ `);
13201
+ process.stdout.write(`Apps: ${renderValues(report.apps)}
13202
+ `);
13203
+ process.stdout.write(`Dependencies: ${renderValues(report.dependencies)}
13204
+ `);
13205
+ process.stdout.write(`Readiness: ${renderValues(report.readiness)}
13206
+ `);
13207
+ process.stdout.write(`Managed services: ${renderValues(report.managedRuntime.services)}
13208
+ `);
13209
+ process.stdout.write(`Managed processes: ${renderValues(report.managedRuntime.processes)}
13210
+ `);
13211
+ }
13212
+ var init_profile = __esm({
13213
+ "src/commands/profile.ts"() {
13214
+ "use strict";
13215
+ init_output();
13216
+ init_profile_resolution();
13217
+ }
13218
+ });
13219
+
12286
13220
  // src/commands/app-add.ts
12287
13221
  var app_add_exports = {};
12288
13222
  __export(app_add_exports, {
@@ -12565,7 +13499,7 @@ function sanitizeRouterId(value) {
12565
13499
  return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
12566
13500
  }
12567
13501
  function repoHash(repoPath) {
12568
- return (0, import_node_crypto9.createHash)("sha1").update(import_node_path28.default.resolve(repoPath)).digest("hex").slice(0, 12);
13502
+ return (0, import_node_crypto10.createHash)("sha1").update(import_node_path31.default.resolve(repoPath)).digest("hex").slice(0, 12);
12569
13503
  }
12570
13504
  function asDockerApp(app) {
12571
13505
  return app.runtime === "docker";
@@ -12644,11 +13578,11 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
12644
13578
  if (dockerApps.length === 0) {
12645
13579
  throw new Error("No docker apps selected to prepare compose overlay.");
12646
13580
  }
12647
- const cachePath = import_node_path28.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
12648
- import_node_fs28.default.mkdirSync(cachePath, { recursive: true });
12649
- const overlayPath = import_node_path28.default.join(cachePath, "compose.devrouter.yml");
13581
+ const cachePath = import_node_path31.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
13582
+ import_node_fs30.default.mkdirSync(cachePath, { recursive: true });
13583
+ const overlayPath = import_node_path31.default.join(cachePath, "compose.devrouter.yml");
12650
13584
  const overlayDocument = buildOverlayDocument(dockerApps, publishTcpPorts);
12651
- import_node_fs28.default.writeFileSync(overlayPath, import_yaml7.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
13585
+ import_node_fs30.default.writeFileSync(overlayPath, import_yaml7.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
12652
13586
  return {
12653
13587
  overlayPath,
12654
13588
  composeFiles: ensureComposeFiles(dockerApps),
@@ -12662,7 +13596,7 @@ function runDockerComposeUp(repoPath, composeFiles2, overlayPath, services) {
12662
13596
  fileArgs.push("-f", resolved);
12663
13597
  }
12664
13598
  const args = ["compose", ...fileArgs, "-f", overlayPath, "up", "-d", "--wait", ...services];
12665
- const result = (0, import_node_child_process25.spawnSync)("docker", args, {
13599
+ const result = (0, import_node_child_process27.spawnSync)("docker", args, {
12666
13600
  encoding: "utf-8",
12667
13601
  cwd: repoPath
12668
13602
  });
@@ -12690,7 +13624,7 @@ function queryRunningComposeServices(repoPath, composeFiles2, overlayPath, servi
12690
13624
  "--services",
12691
13625
  ...services
12692
13626
  ];
12693
- const result = (0, import_node_child_process25.spawnSync)("docker", args, {
13627
+ const result = (0, import_node_child_process27.spawnSync)("docker", args, {
12694
13628
  encoding: "utf-8",
12695
13629
  cwd: repoPath
12696
13630
  });
@@ -12716,7 +13650,7 @@ function runDockerComposeStop(repoPath, composeFiles2, overlayPath, services) {
12716
13650
  fileArgs.push("-f", resolved);
12717
13651
  }
12718
13652
  const args = ["compose", ...fileArgs, "-f", overlayPath, "stop", ...services];
12719
- const result = (0, import_node_child_process25.spawnSync)("docker", args, {
13653
+ const result = (0, import_node_child_process27.spawnSync)("docker", args, {
12720
13654
  encoding: "utf-8",
12721
13655
  cwd: repoPath
12722
13656
  });
@@ -12742,7 +13676,7 @@ function runDockerComposeLogs(repoPath, composeFiles2, overlayPath, services, ta
12742
13676
  String(tail),
12743
13677
  ...services
12744
13678
  ];
12745
- (0, import_node_child_process25.spawnSync)("docker", args, {
13679
+ (0, import_node_child_process27.spawnSync)("docker", args, {
12746
13680
  stdio: "inherit",
12747
13681
  cwd: repoPath
12748
13682
  });
@@ -12754,7 +13688,7 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
12754
13688
  fileArgs.push("-f", resolved);
12755
13689
  }
12756
13690
  const args = ["compose", ...fileArgs, "-f", overlayPath, "port", service, String(internalPort)];
12757
- const result = (0, import_node_child_process25.spawnSync)("docker", args, {
13691
+ const result = (0, import_node_child_process27.spawnSync)("docker", args, {
12758
13692
  encoding: "utf-8",
12759
13693
  cwd: repoPath
12760
13694
  });
@@ -12768,14 +13702,14 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
12768
13702
  const port = Number(match[1]);
12769
13703
  return Number.isInteger(port) && port > 0 ? port : void 0;
12770
13704
  }
12771
- var import_node_child_process25, import_node_crypto9, import_node_fs28, import_node_path28, import_yaml7;
13705
+ var import_node_child_process27, import_node_crypto10, import_node_fs30, import_node_path31, import_yaml7;
12772
13706
  var init_docker_run = __esm({
12773
13707
  "src/core/docker-run.ts"() {
12774
13708
  "use strict";
12775
- import_node_child_process25 = require("child_process");
12776
- import_node_crypto9 = require("crypto");
12777
- import_node_fs28 = __toESM(require("fs"));
12778
- import_node_path28 = __toESM(require("path"));
13709
+ import_node_child_process27 = require("child_process");
13710
+ import_node_crypto10 = require("crypto");
13711
+ import_node_fs30 = __toESM(require("fs"));
13712
+ import_node_path31 = __toESM(require("path"));
12779
13713
  import_yaml7 = __toESM(require("yaml"));
12780
13714
  init_docker_error_guidance();
12781
13715
  init_paths();
@@ -12812,7 +13746,7 @@ function resolveSmCommand(command, defaultEnv, overrideEnv) {
12812
13746
  }
12813
13747
  return command.replace(/\{env\}/g, env);
12814
13748
  }
12815
- function sleep2(ms) {
13749
+ function sleep3(ms) {
12816
13750
  return new Promise((resolve) => {
12817
13751
  setTimeout(resolve, ms);
12818
13752
  });
@@ -12865,7 +13799,7 @@ function isProcessRunning(pid) {
12865
13799
  }
12866
13800
  }
12867
13801
  function readProcessTree(rootPid) {
12868
- const result = (0, import_node_child_process26.spawnSync)("ps", ["-ax", "-o", "pid=,ppid="], { encoding: "utf-8" });
13802
+ const result = (0, import_node_child_process28.spawnSync)("ps", ["-ax", "-o", "pid=,ppid="], { encoding: "utf-8" });
12869
13803
  if (result.status !== 0) {
12870
13804
  return [rootPid];
12871
13805
  }
@@ -12923,7 +13857,7 @@ async function terminateProcessTree(rootPid) {
12923
13857
  if (!isProcessRunning(rootPid)) {
12924
13858
  return;
12925
13859
  }
12926
- await sleep2(100);
13860
+ await sleep3(100);
12927
13861
  }
12928
13862
  if (isProcessRunning(rootPid)) {
12929
13863
  killProcessTree(rootPid, "SIGKILL");
@@ -12976,12 +13910,12 @@ function detectListeningPorts(pids) {
12976
13910
  if (pids.length === 0) {
12977
13911
  return [];
12978
13912
  }
12979
- const result = (0, import_node_child_process26.spawnSync)("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", pids.join(",")], {
13913
+ const result = (0, import_node_child_process28.spawnSync)("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", pids.join(",")], {
12980
13914
  encoding: "utf-8"
12981
13915
  });
12982
13916
  if (result.error && result.error.code === "ENOENT") {
12983
13917
  if (process.platform === "linux") {
12984
- const ssResult = (0, import_node_child_process26.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
13918
+ const ssResult = (0, import_node_child_process28.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
12985
13919
  if (ssResult.status === 0 && ssResult.stdout) {
12986
13920
  return parseSsListeningPorts(ssResult.stdout, new Set(pids));
12987
13921
  }
@@ -13040,7 +13974,7 @@ async function runHostApp(repoPath, app, extraEnv = {}, secretManager, env, work
13040
13974
  app.hostRun.command,
13041
13975
  true
13042
13976
  ) : app.hostRun.command;
13043
- const child = (0, import_node_child_process26.spawn)(spawnCommand, {
13977
+ const child = (0, import_node_child_process28.spawn)(spawnCommand, {
13044
13978
  cwd: commandCwd,
13045
13979
  stdio: "inherit",
13046
13980
  shell: true,
@@ -13110,7 +14044,7 @@ async function runHostApp(repoPath, app, extraEnv = {}, secretManager, env, work
13110
14044
  );
13111
14045
  }
13112
14046
  }
13113
- await sleep2(POLL_INTERVAL_MS2);
14047
+ await sleep3(POLL_INTERVAL_MS2);
13114
14048
  }
13115
14049
  } catch (error) {
13116
14050
  fatalError = toError(error);
@@ -13408,7 +14342,7 @@ async function execWithAppEnv(options) {
13408
14342
  options.command[0],
13409
14343
  true
13410
14344
  );
13411
- child = (0, import_node_child_process26.spawn)(wrapped, {
14345
+ child = (0, import_node_child_process28.spawn)(wrapped, {
13412
14346
  cwd: deps.repoPath,
13413
14347
  stdio: "inherit",
13414
14348
  shell: true,
@@ -13422,7 +14356,7 @@ async function execWithAppEnv(options) {
13422
14356
  false
13423
14357
  );
13424
14358
  const [cmd, ...wrappedArgs] = wrapped;
13425
- child = (0, import_node_child_process26.spawn)(cmd, wrappedArgs, {
14359
+ child = (0, import_node_child_process28.spawn)(cmd, wrappedArgs, {
13426
14360
  cwd: deps.repoPath,
13427
14361
  stdio: "inherit",
13428
14362
  shell: false,
@@ -13430,7 +14364,7 @@ async function execWithAppEnv(options) {
13430
14364
  });
13431
14365
  }
13432
14366
  } else if (options.shell) {
13433
- child = (0, import_node_child_process26.spawn)(options.command[0], {
14367
+ child = (0, import_node_child_process28.spawn)(options.command[0], {
13434
14368
  cwd: deps.repoPath,
13435
14369
  stdio: "inherit",
13436
14370
  shell: true,
@@ -13438,7 +14372,7 @@ async function execWithAppEnv(options) {
13438
14372
  });
13439
14373
  } else {
13440
14374
  const [command, ...args] = options.command;
13441
- child = (0, import_node_child_process26.spawn)(command, args, {
14375
+ child = (0, import_node_child_process28.spawn)(command, args, {
13442
14376
  cwd: deps.repoPath,
13443
14377
  stdio: "inherit",
13444
14378
  shell: false,
@@ -13459,11 +14393,11 @@ async function execWithAppEnv(options) {
13459
14393
  deps.stopDeps();
13460
14394
  }
13461
14395
  }
13462
- var import_node_child_process26, import_node_net, import_node_process, import_promises, POLL_INTERVAL_MS2, DEFAULT_PORT_TIMEOUT_MS, PROCESS_TERMINATION_GRACE_MS;
14396
+ var import_node_child_process28, import_node_net, import_node_process, import_promises, POLL_INTERVAL_MS2, DEFAULT_PORT_TIMEOUT_MS, PROCESS_TERMINATION_GRACE_MS;
13463
14397
  var init_app_run = __esm({
13464
14398
  "src/core/app-run.ts"() {
13465
14399
  "use strict";
13466
- import_node_child_process26 = require("child_process");
14400
+ import_node_child_process28 = require("child_process");
13467
14401
  import_node_net = __toESM(require("net"));
13468
14402
  import_node_process = require("process");
13469
14403
  import_promises = require("readline/promises");
@@ -13597,13 +14531,13 @@ function measureWorktreeConsumption(worktreePath, options) {
13597
14531
  const startedAt = Date.now();
13598
14532
  let rootStat;
13599
14533
  try {
13600
- rootStat = import_node_fs29.default.lstatSync(worktreePath);
14534
+ rootStat = import_node_fs31.default.lstatSync(worktreePath);
13601
14535
  } catch (error) {
13602
14536
  return { status: "unknown", reason: describeError(error, worktreePath) };
13603
14537
  }
13604
14538
  let rootEntries;
13605
14539
  try {
13606
- rootEntries = import_node_fs29.default.readdirSync(worktreePath, { withFileTypes: true });
14540
+ rootEntries = import_node_fs31.default.readdirSync(worktreePath, { withFileTypes: true });
13607
14541
  } catch (error) {
13608
14542
  return { status: "unknown", reason: describeError(error, worktreePath) };
13609
14543
  }
@@ -13629,10 +14563,10 @@ function measureWorktreeConsumption(worktreePath, options) {
13629
14563
  timedOut = true;
13630
14564
  break;
13631
14565
  }
13632
- const entryPath = import_node_path29.default.join(dirPath, entry.name);
14566
+ const entryPath = import_node_path32.default.join(dirPath, entry.name);
13633
14567
  let entryStat;
13634
14568
  try {
13635
- entryStat = import_node_fs29.default.lstatSync(entryPath);
14569
+ entryStat = import_node_fs31.default.lstatSync(entryPath);
13636
14570
  } catch (error) {
13637
14571
  if (error?.code === "ENOENT") continue;
13638
14572
  unreadableReason = describeIncompleteWalk(error);
@@ -13642,7 +14576,7 @@ function measureWorktreeConsumption(worktreePath, options) {
13642
14576
  if (!entryStat.isDirectory()) continue;
13643
14577
  let childEntries;
13644
14578
  try {
13645
- childEntries = import_node_fs29.default.readdirSync(entryPath, { withFileTypes: true });
14579
+ childEntries = import_node_fs31.default.readdirSync(entryPath, { withFileTypes: true });
13646
14580
  } catch (error) {
13647
14581
  unreadableReason = describeIncompleteWalk(error);
13648
14582
  break;
@@ -13706,12 +14640,12 @@ function describeError(error, worktreePath) {
13706
14640
  }
13707
14641
  return `could not stat worktree path '${worktreePath}': ${error?.message ?? String(error)}`;
13708
14642
  }
13709
- var import_node_fs29, import_node_path29, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
14643
+ var import_node_fs31, import_node_path32, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
13710
14644
  var init_workspace_consumption = __esm({
13711
14645
  "src/core/workspace-consumption.ts"() {
13712
14646
  "use strict";
13713
- import_node_fs29 = __toESM(require("fs"));
13714
- import_node_path29 = __toESM(require("path"));
14647
+ import_node_fs31 = __toESM(require("fs"));
14648
+ import_node_path32 = __toESM(require("path"));
13715
14649
  init_devpod_environment();
13716
14650
  DEFAULT_DEADLINE_MS = 1e4;
13717
14651
  BLOCK_SIZE_BYTES = 512;
@@ -13812,7 +14746,7 @@ function evaluateWorkspaceActivity(evidence, cutoff) {
13812
14746
  }
13813
14747
  function readGitSnapshot(worktree, commandRunner) {
13814
14748
  const comparablePath = comparableWorkspacePath(worktree.path);
13815
- if (worktree.prunable || !import_node_fs30.default.existsSync(comparablePath)) {
14749
+ if (worktree.prunable || !import_node_fs32.default.existsSync(comparablePath)) {
13816
14750
  return { worktree, checkout: "missing", head: null, committerDate: null };
13817
14751
  }
13818
14752
  const head = gitOutput(comparablePath, ["rev-parse", "--verify", "HEAD"], commandRunner);
@@ -14337,7 +15271,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
14337
15271
  containers = measureContainersFn(worktreePaths);
14338
15272
  } catch (error) {
14339
15273
  const reason = `container measurement failed: ${describeCause(error)}`;
14340
- containers = new Map(worktreePaths.map((path29) => [path29, unknownContainers(reason)]));
15274
+ containers = new Map(worktreePaths.map((path32) => [path32, unknownContainers(reason)]));
14341
15275
  }
14342
15276
  }
14343
15277
  const rows = records.map((record) => {
@@ -14386,12 +15320,12 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
14386
15320
  workspaces: rows
14387
15321
  };
14388
15322
  }
14389
- var import_node_child_process27, import_node_fs30, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
15323
+ var import_node_child_process29, import_node_fs32, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
14390
15324
  var init_workspace_cleanup = __esm({
14391
15325
  "src/core/workspace-cleanup.ts"() {
14392
15326
  "use strict";
14393
- import_node_child_process27 = require("child_process");
14394
- import_node_fs30 = __toESM(require("fs"));
15327
+ import_node_child_process29 = require("child_process");
15328
+ import_node_fs32 = __toESM(require("fs"));
14395
15329
  init_devpod_workspaces();
14396
15330
  init_host_routes();
14397
15331
  init_repo_config();
@@ -14408,7 +15342,7 @@ var init_workspace_cleanup = __esm({
14408
15342
  ];
14409
15343
  defaultCommandRunner = (command, args, input2) => {
14410
15344
  const environment = command === "git" ? READ_ONLY_GIT_ENV2 : { ...process.env, LC_ALL: "C" };
14411
- const result = (0, import_node_child_process27.spawnSync)(command, args, {
15345
+ const result = (0, import_node_child_process29.spawnSync)(command, args, {
14412
15346
  encoding: "utf-8",
14413
15347
  env: environment,
14414
15348
  ...input2 === void 0 ? {} : { input: input2 }
@@ -14568,7 +15502,7 @@ var init_version = __esm({
14568
15502
 
14569
15503
  // src/cli.ts
14570
15504
  var import_commander = require("commander");
14571
- var CLI_VERSION = true ? "0.0.45" : "0.0.0-dev";
15505
+ var CLI_VERSION = true ? "0.0.48" : "0.0.0-dev";
14572
15506
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
14573
15507
  function withErrorHandling(action2) {
14574
15508
  return async (...args) => {
@@ -14726,6 +15660,16 @@ repoDevcontainerCommand.command("verify").description("Verify devcontainer onboa
14726
15660
  }
14727
15661
  )
14728
15662
  );
15663
+ var profileCommand = program.command("profile").description("Resolve profiles from `.devrouter.yml` without starting a runtime");
15664
+ profileCommand.command("resolve").description("Print the exact apps, dependencies, services, and processes for a profile").option("--repo <path>", "Repository path (defaults to current directory)").option(
15665
+ "--profile <name>",
15666
+ "Profile name or comma-separated selection (defaults to configured profile)"
15667
+ ).option("--json", "Output stable JSON").action(
15668
+ withErrorHandling(async (options) => {
15669
+ const { runProfileResolveCommand: runProfileResolveCommand2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
15670
+ await runProfileResolveCommand2(options);
15671
+ })
15672
+ );
14729
15673
  var appCommand = program.command("app").description("Manage app entries and runtime actions from `.devrouter.yml`");
14730
15674
  appCommand.command("add").description("Add or update one app definition in `.devrouter.yml`").requiredOption("--name <name>", "App name").option("--kind <kind>", "app or dependency", "app").option("--host <host>", "Hostname ending with .localhost (required for --kind app)").option("--protocol <protocol>", "http or tcp (required for --kind app)").option(
14731
15675
  "--runtime <runtime>",