@devrouter/cli 0.0.46 → 0.0.49

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, which serializes provider mutations in a fair arrival-order queue and lets contenders wait up to thirty minutes with throttled stderr progress before failing with the queue position or holder PID and true durations. Runtime selection is path-aware: \`DEVROUTER_WORKSPACE_RUNTIME=devpod|devsy\` forces one runtime, an exact-path registry owner wins next, then the machine preference from \`devrouter setup --workspace-runtime\`, then installed-CLI auto-detection. \`workspace up\` creates linked worktrees; destructive worktree removal and GC remain ledger-scoped. Dirty or locked full down fails before side effects. A failed Devsy start that reports the agent-binary acquisition failure names the \`DEVSY_AGENT_BINARY\` remediation.
326
+ - **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
  }
@@ -1406,6 +1414,12 @@ function hostRouteFromInput(input2, existing, now) {
1406
1414
  function sanitizeKey(value) {
1407
1415
  return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1408
1416
  }
1417
+ function buildHostRouteRouterKey(routeId) {
1418
+ return `host-${sanitizeKey(routeId)}`;
1419
+ }
1420
+ function buildHostRouteRouterName(repoPath, name) {
1421
+ return `${buildHostRouteRouterKey(buildHostRouteId(repoPath, name))}@file`;
1422
+ }
1409
1423
  function tcpTlsOptionName(tcpProtocol) {
1410
1424
  return `devrouter-tcp-${tcpProtocol}`;
1411
1425
  }
@@ -1416,7 +1430,7 @@ function buildHostRoutesDocument(routes, tlsEnabled) {
1416
1430
  const tcpServices = {};
1417
1431
  const tlsOptions = {};
1418
1432
  for (const route of routes) {
1419
- const key = `host-${sanitizeKey(route.id)}`;
1433
+ const key = buildHostRouteRouterKey(route.id);
1420
1434
  if (route.protocol === "tcp") {
1421
1435
  const alpn = route.tcpProtocol ? TCP_TLS_ALPN_PROTOCOLS[route.tcpProtocol] : void 0;
1422
1436
  let tls = {};
@@ -2654,7 +2668,7 @@ function loadRepoConfig(repoPath) {
2654
2668
  const config = parseConfig(parsed ?? {}, configPath);
2655
2669
  const requiredVersion = config.devrouter?.version;
2656
2670
  if (requiredVersion && !hasWarnedVersionMismatch) {
2657
- const cliVersion = true ? "0.0.46" : "0.0.0-dev";
2671
+ const cliVersion = true ? "0.0.49" : "0.0.0-dev";
2658
2672
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
2659
2673
  hasWarnedVersionMismatch = true;
2660
2674
  process.stderr.write(
@@ -3185,6 +3199,7 @@ function buildOnboardingPrompt(options = {}) {
3185
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.`,
3186
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.",
3187
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.",
3188
3203
  "- A managed adapter plus `postCreateCommand` requires `waitFor` exactly `postCreateCommand` or `postStartCommand`; managed selective config preserves lifecycle fields and changes only `runServices`.",
3189
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.",
3190
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.",
@@ -3216,7 +3231,7 @@ function buildOnboardingPrompt(options = {}) {
3216
3231
  "- Warning: Do not run migration/seed until env probe confirms expected DB variables and values.",
3217
3232
  "",
3218
3233
  "Required workflow:",
3219
- "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.",
3220
3235
  "2) Run `devrouter repo inspect --repo <REPO_PATH> --json` before editing files.",
3221
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`.",
3222
3237
  "4) For unsupported shapes or custom existing files, make minimal manual edits and explain the assumptions.",
@@ -3226,6 +3241,7 @@ function buildOnboardingPrompt(options = {}) {
3226
3241
  "",
3227
3242
  "Validation commands to run/report for the devcontainer path:",
3228
3243
  "- devrouter setup --yes --json",
3244
+ "- Devsy runtime setup: devrouter setup --yes --workspace-runtime devsy --json",
3229
3245
  "- devrouter doctor --repo <REPO_PATH> --json",
3230
3246
  "- devrouter repo inspect --repo <REPO_PATH> --json",
3231
3247
  "- devrouter repo devcontainer write --repo <REPO_PATH> --dry-run --json",
@@ -3353,6 +3369,10 @@ var init_ai_prompt = __esm({
3353
3369
  command: "devrouter ensure [path] [--profile <name>] [--open] [--json]",
3354
3370
  purpose: "Canonical startup and proof for an exact primary or linked checkout; atomically publishes routes after readiness."
3355
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
+ },
3356
3376
  {
3357
3377
  command: "devrouter stop [path] [--delete] [--json]",
3358
3378
  purpose: "Stop the exact checkout workspace runtime and routes; --delete explicitly deletes the ownership-proven runtime without removing the checkout."
@@ -4055,6 +4075,384 @@ var init_upgrade2 = __esm({
4055
4075
  }
4056
4076
  });
4057
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
+
4058
4456
  // src/core/docker.ts
4059
4457
  async function getDockerodeConstructor() {
4060
4458
  if (!dockerodeConstructorPromise) {
@@ -4066,7 +4464,7 @@ async function getDockerodeConstructor() {
4066
4464
  return dockerodeConstructorPromise;
4067
4465
  }
4068
4466
  function runDockerContextCommand(args) {
4069
- const result = (0, import_node_child_process4.spawnSync)("docker", ["context", ...args], {
4467
+ const result = (0, import_node_child_process5.spawnSync)("docker", ["context", ...args], {
4070
4468
  encoding: "utf-8"
4071
4469
  });
4072
4470
  if (result.status !== 0) {
@@ -4079,7 +4477,7 @@ function getCurrentDockerContext() {
4079
4477
  return runDockerContextCommand(["show"]);
4080
4478
  }
4081
4479
  function getDockerHostFromContext(context) {
4082
- const result = (0, import_node_child_process4.spawnSync)(
4480
+ const result = (0, import_node_child_process5.spawnSync)(
4083
4481
  "docker",
4084
4482
  ["context", "inspect", context, "--format", "{{ .Endpoints.docker.Host }}"],
4085
4483
  { encoding: "utf-8" }
@@ -4147,11 +4545,11 @@ async function networkExists(name) {
4147
4545
  return false;
4148
4546
  }
4149
4547
  }
4150
- var import_node_child_process4, dockerodeConstructorPromise;
4548
+ var import_node_child_process5, dockerodeConstructorPromise;
4151
4549
  var init_docker = __esm({
4152
4550
  "src/core/docker.ts"() {
4153
4551
  "use strict";
4154
- import_node_child_process4 = require("child_process");
4552
+ import_node_child_process5 = require("child_process");
4155
4553
  dockerodeConstructorPromise = null;
4156
4554
  }
4157
4555
  });
@@ -4178,11 +4576,11 @@ function parseDevcontainerConfig(contents, sourcePath) {
4178
4576
  return parsed;
4179
4577
  }
4180
4578
  function readDevcontainerConfig(repoPath) {
4181
- const sourcePath = import_node_path9.default.join(repoPath, ".devcontainer", "devcontainer.json");
4182
- 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()) {
4183
4581
  throw new Error(`Managed Dev Container source config does not exist: ${sourcePath}`);
4184
4582
  }
4185
- const sourceContents = import_node_fs9.default.readFileSync(sourcePath, "utf-8");
4583
+ const sourceContents = import_node_fs10.default.readFileSync(sourcePath, "utf-8");
4186
4584
  return {
4187
4585
  sourcePath,
4188
4586
  sourceContents,
@@ -4198,12 +4596,12 @@ function assertManagedDevcontainerLifecycle(repoPath) {
4198
4596
  `Managed Dev Container source '${sourcePath}' defines postCreateCommand, but waitFor ${actual}. Set waitFor to 'postCreateCommand' or 'postStartCommand' before retrying devrouter ensure.`
4199
4597
  );
4200
4598
  }
4201
- var import_node_fs9, import_node_path9, import_jsonc_parser;
4599
+ var import_node_fs10, import_node_path10, import_jsonc_parser;
4202
4600
  var init_devcontainer_config = __esm({
4203
4601
  "src/core/devcontainer-config.ts"() {
4204
4602
  "use strict";
4205
- import_node_fs9 = __toESM(require("fs"));
4206
- import_node_path9 = __toESM(require("path"));
4603
+ import_node_fs10 = __toESM(require("fs"));
4604
+ import_node_path10 = __toESM(require("path"));
4207
4605
  import_jsonc_parser = require("jsonc-parser");
4208
4606
  }
4209
4607
  });
@@ -4213,7 +4611,7 @@ function renderDeliveryScript(targetPath) {
4213
4611
  if (!/^\/tmp\/devrouter\/bin\/[a-zA-Z0-9._-]+$/.test(targetPath)) {
4214
4612
  throw new Error(`Unsafe managed runtime delivery path: ${targetPath}`);
4215
4613
  }
4216
- const temporaryPrefix = import_node_path10.default.posix.basename(targetPath);
4614
+ const temporaryPrefix = import_node_path11.default.posix.basename(targetPath);
4217
4615
  return `set -eu
4218
4616
  umask 077
4219
4617
  runtime_root=/tmp/devrouter
@@ -4237,7 +4635,7 @@ function commandFailure(result) {
4237
4635
  return [result.error?.message, result.stderr, result.stdout].filter(Boolean).map((value) => String(value).trim()).filter(Boolean).join("\n");
4238
4636
  }
4239
4637
  function deliverRuntimeFile(containerId, targetPath, contents, label) {
4240
- const result = (0, import_node_child_process5.spawnSync)(
4638
+ const result = (0, import_node_child_process6.spawnSync)(
4241
4639
  "docker",
4242
4640
  ["exec", "-i", containerId, "sh", "-c", renderDeliveryScript(targetPath)],
4243
4641
  { input: contents, encoding: "utf-8" }
@@ -4248,32 +4646,32 @@ function deliverRuntimeFile(containerId, targetPath, contents, label) {
4248
4646
  }
4249
4647
  }
4250
4648
  function readRegularFileBytes(filePath) {
4251
- if (!import_node_fs10.default.existsSync(filePath) || !import_node_fs10.default.lstatSync(filePath).isFile()) return void 0;
4252
- 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);
4253
4651
  }
4254
4652
  function readRegularFile(filePath) {
4255
4653
  return readRegularFileBytes(filePath)?.toString("utf-8");
4256
4654
  }
4257
4655
  function adapterFingerprint(adapter) {
4258
- return (0, import_node_crypto4.createHash)("sha256").update(adapter).digest("hex");
4656
+ return (0, import_node_crypto5.createHash)("sha256").update(adapter).digest("hex");
4259
4657
  }
4260
4658
  function resolveProcessHelperPath() {
4261
4659
  const candidates = [
4262
- import_node_path10.default.resolve(__dirname, "..", "bin", "devrouter-process"),
4263
- import_node_path10.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")
4264
4662
  ];
4265
- const helperPath = candidates.find((candidate) => import_node_fs10.default.existsSync(candidate));
4663
+ const helperPath = candidates.find((candidate) => import_node_fs11.default.existsSync(candidate));
4266
4664
  if (!helperPath) {
4267
4665
  throw new Error("Could not locate the packaged devrouter-process helper.");
4268
4666
  }
4269
4667
  return helperPath;
4270
4668
  }
4271
4669
  function resolveManagedPostStartPlan(repoPath) {
4272
- const adapterPath = import_node_path10.default.join(repoPath, MANAGED_ADAPTER_PATH);
4670
+ const adapterPath = import_node_path11.default.join(repoPath, MANAGED_ADAPTER_PATH);
4273
4671
  const adapterBytes = readRegularFileBytes(adapterPath);
4274
4672
  const adapterText = adapterBytes?.toString("utf-8");
4275
- const dockerfilePath = import_node_path10.default.join(repoPath, ".devcontainer", "Dockerfile");
4276
- const devcontainerPath = import_node_path10.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");
4277
4675
  const dockerfile = readRegularFile(dockerfilePath) ?? "";
4278
4676
  const devcontainer = readRegularFile(devcontainerPath) ?? "";
4279
4677
  const devrouterPattern = [adapterText, dockerfile, devcontainer].filter((value) => value !== void 0).some(
@@ -4314,7 +4712,7 @@ function resolveManagedPostStartPlan(repoPath) {
4314
4712
  }
4315
4713
  function runManagedPostStart(options) {
4316
4714
  if (options.plan.kind !== "runtime") return;
4317
- const helper = import_node_fs10.default.readFileSync(resolveProcessHelperPath());
4715
+ const helper = import_node_fs11.default.readFileSync(resolveProcessHelperPath());
4318
4716
  deliverRuntimeFile(
4319
4717
  options.container.id,
4320
4718
  RUNTIME_HELPER_PATH,
@@ -4328,7 +4726,7 @@ function runManagedPostStart(options) {
4328
4726
  options.plan.adapterContents,
4329
4727
  "the managed post-start adapter"
4330
4728
  );
4331
- const started = (0, import_node_child_process5.spawnSync)(
4729
+ const started = (0, import_node_child_process6.spawnSync)(
4332
4730
  "docker",
4333
4731
  [
4334
4732
  "exec",
@@ -4366,7 +4764,7 @@ function assertSafeProcessName(name) {
4366
4764
  }
4367
4765
  function runManagedProcessAction(options) {
4368
4766
  assertSafeProcessName(options.name);
4369
- const result = (0, import_node_child_process5.spawnSync)(
4767
+ const result = (0, import_node_child_process6.spawnSync)(
4370
4768
  "docker",
4371
4769
  [
4372
4770
  "exec",
@@ -4402,14 +4800,14 @@ function runManagedProcessAction(options) {
4402
4800
  }
4403
4801
  return "drifted";
4404
4802
  }
4405
- var import_node_child_process5, import_node_crypto4, import_node_fs10, import_node_path10, MANAGED_MARKER, MANAGED_ADAPTER_PATH, RUNTIME_HELPER_PATH, ADAPTER_WRAPPER;
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;
4406
4804
  var init_managed_post_start = __esm({
4407
4805
  "src/core/managed-post-start.ts"() {
4408
4806
  "use strict";
4409
- import_node_child_process5 = require("child_process");
4410
- import_node_crypto4 = require("crypto");
4411
- import_node_fs10 = __toESM(require("fs"));
4412
- import_node_path10 = __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"));
4413
4811
  init_devcontainer_config();
4414
4812
  MANAGED_MARKER = "devrouter:managed devcontainer";
4415
4813
  MANAGED_ADAPTER_PATH = ".devcontainer/post-start.sh";
@@ -4476,7 +4874,7 @@ function isLoopbackHost(host) {
4476
4874
  return host === "127.0.0.1" || host === "localhost" || host === "host.docker.internal";
4477
4875
  }
4478
4876
  function inspectCompose(composeFile, _workspace) {
4479
- if (!import_node_fs11.default.existsSync(composeFile)) {
4877
+ if (!import_node_fs12.default.existsSync(composeFile)) {
4480
4878
  return {
4481
4879
  aliases: [],
4482
4880
  publishedPorts: [],
@@ -4485,7 +4883,7 @@ function inspectCompose(composeFile, _workspace) {
4485
4883
  };
4486
4884
  }
4487
4885
  try {
4488
- const raw = import_node_fs11.default.readFileSync(composeFile, "utf-8");
4886
+ const raw = import_node_fs12.default.readFileSync(composeFile, "utf-8");
4489
4887
  const parsed = import_yaml3.default.parse(raw);
4490
4888
  const root = asRecord(parsed);
4491
4889
  const services = asRecord(root?.services);
@@ -4542,11 +4940,11 @@ function routedProxyApps(config) {
4542
4940
  );
4543
4941
  }
4544
4942
  function buildDevcontainerChecks(repoPath, config, workspace) {
4545
- const devcontainerDir = import_node_path11.default.join(repoPath, ".devcontainer");
4546
- 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)) {
4547
4945
  return [];
4548
4946
  }
4549
- const compose = inspectCompose(import_node_path11.default.join(devcontainerDir, "docker-compose.yml"), workspace);
4947
+ const compose = inspectCompose(import_node_path12.default.join(devcontainerDir, "docker-compose.yml"), workspace);
4550
4948
  const checks = [];
4551
4949
  if (compose.parseError) {
4552
4950
  checks.push({
@@ -4573,9 +4971,9 @@ function buildDevcontainerChecks(repoPath, config, workspace) {
4573
4971
  details: compose.publishedPorts.length > 0 ? compose.publishedPorts.join(", ") : void 0,
4574
4972
  suggestion: compose.parseError || compose.publishedPorts.length > 0 ? "Remove published ports and route services through devnet aliases with devrouter proxy apps." : void 0
4575
4973
  });
4576
- const dockerfilePath = import_node_path11.default.join(devcontainerDir, "Dockerfile");
4577
- const dockerfileExists = import_node_fs11.default.existsSync(dockerfilePath);
4578
- 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") : "";
4579
4977
  const devrouterArtifacts = ["@devrouter/cli", "devrouter-process"].filter(
4580
4978
  (artifact) => dockerfile.includes(artifact)
4581
4979
  );
@@ -4630,12 +5028,12 @@ function buildDevcontainerChecks(repoPath, config, workspace) {
4630
5028
  });
4631
5029
  return checks;
4632
5030
  }
4633
- var import_node_fs11, import_node_path11, import_yaml3;
5031
+ var import_node_fs12, import_node_path12, import_yaml3;
4634
5032
  var init_devcontainer_diagnostics = __esm({
4635
5033
  "src/core/devcontainer-diagnostics.ts"() {
4636
5034
  "use strict";
4637
- import_node_fs11 = __toESM(require("fs"));
4638
- import_node_path11 = __toESM(require("path"));
5035
+ import_node_fs12 = __toESM(require("fs"));
5036
+ import_node_path12 = __toESM(require("path"));
4639
5037
  import_yaml3 = __toESM(require("yaml"));
4640
5038
  init_managed_post_start();
4641
5039
  }
@@ -4643,18 +5041,18 @@ var init_devcontainer_diagnostics = __esm({
4643
5041
 
4644
5042
  // src/core/paths.ts
4645
5043
  function assertPathWithinRepo(filePath, repoRoot, label) {
4646
- const resolvedRoot = import_node_path12.default.resolve(repoRoot);
4647
- const resolved = import_node_path12.default.resolve(repoRoot, filePath);
4648
- if (resolved !== resolvedRoot && !resolved.startsWith(resolvedRoot + import_node_path12.default.sep)) {
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)) {
4649
5047
  throw new Error(`${label} path '${filePath}' escapes the repository root.`);
4650
5048
  }
4651
5049
  return resolved;
4652
5050
  }
4653
- var import_node_path12;
5051
+ var import_node_path13;
4654
5052
  var init_paths = __esm({
4655
5053
  "src/core/paths.ts"() {
4656
5054
  "use strict";
4657
- import_node_path12 = __toESM(require("path"));
5055
+ import_node_path13 = __toESM(require("path"));
4658
5056
  }
4659
5057
  });
4660
5058
 
@@ -4739,11 +5137,11 @@ function reconcileRouteRunConflict(repoPath, app) {
4739
5137
  }
4740
5138
  }
4741
5139
  }
4742
- var import_node_fs12;
5140
+ var import_node_fs13;
4743
5141
  var init_route_state = __esm({
4744
5142
  "src/core/route-state.ts"() {
4745
5143
  "use strict";
4746
- import_node_fs12 = __toESM(require("fs"));
5144
+ import_node_fs13 = __toESM(require("fs"));
4747
5145
  init_host_routes();
4748
5146
  init_workspace();
4749
5147
  }
@@ -4909,10 +5307,10 @@ var init_routes = __esm({
4909
5307
 
4910
5308
  // src/core/managed-runtime-state.ts
4911
5309
  function stateKey(repoPath, workspace) {
4912
- 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");
4913
5311
  }
4914
5312
  function managedRuntimeStatePath(repoPath, workspace) {
4915
- return import_node_path13.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`);
4916
5314
  }
4917
5315
  function isStringArray(value) {
4918
5316
  return Array.isArray(value) && value.every((item) => typeof item === "string") && new Set(value).size === value.length;
@@ -4975,10 +5373,10 @@ function validateState(value, repoPath, workspace) {
4975
5373
  }
4976
5374
  function readManagedRuntimeState(repoPath, workspace) {
4977
5375
  const statePath = managedRuntimeStatePath(repoPath, workspace);
4978
- if (!import_node_fs13.default.existsSync(statePath)) return void 0;
5376
+ if (!import_node_fs14.default.existsSync(statePath)) return void 0;
4979
5377
  let parsed;
4980
5378
  try {
4981
- parsed = JSON.parse(import_node_fs13.default.readFileSync(statePath, "utf-8"));
5379
+ parsed = JSON.parse(import_node_fs14.default.readFileSync(statePath, "utf-8"));
4982
5380
  } catch (error) {
4983
5381
  throw new Error(`Could not parse managed runtime state: ${String(error)}`);
4984
5382
  }
@@ -5000,13 +5398,13 @@ function markManagedRuntimeDegraded(state, transitionPhase) {
5000
5398
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
5001
5399
  });
5002
5400
  }
5003
- var import_node_crypto5, import_node_fs13, import_node_path13;
5401
+ var import_node_crypto6, import_node_fs14, import_node_path14;
5004
5402
  var init_managed_runtime_state = __esm({
5005
5403
  "src/core/managed-runtime-state.ts"() {
5006
5404
  "use strict";
5007
- import_node_crypto5 = require("crypto");
5008
- import_node_fs13 = __toESM(require("fs"));
5009
- import_node_path13 = __toESM(require("path"));
5405
+ import_node_crypto6 = require("crypto");
5406
+ import_node_fs14 = __toESM(require("fs"));
5407
+ import_node_path14 = __toESM(require("path"));
5010
5408
  init_atomic_file();
5011
5409
  init_router();
5012
5410
  }
@@ -5057,14 +5455,14 @@ function resolveComposeFiles(source, repoPath, linked) {
5057
5455
  "managedRuntime requires .devcontainer/devcontainer.json to define dockerComposeFile."
5058
5456
  );
5059
5457
  }
5060
- const directory = import_node_path14.default.join(repoPath, ".devcontainer");
5458
+ const directory = import_node_path15.default.join(repoPath, ".devcontainer");
5061
5459
  const files = references.map((reference) => {
5062
5460
  const resolved2 = assertPathWithinRepo(
5063
5461
  resolveComposeReference(reference, linked),
5064
5462
  directory,
5065
5463
  "dockerComposeFile"
5066
5464
  );
5067
- 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()) {
5068
5466
  throw new Error(`Managed Dev Container compose file does not exist: ${resolved2}`);
5069
5467
  }
5070
5468
  return resolved2;
@@ -5072,7 +5470,7 @@ function resolveComposeFiles(source, repoPath, linked) {
5072
5470
  for (const file of files) {
5073
5471
  let parsed;
5074
5472
  try {
5075
- 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"));
5076
5474
  } catch (error) {
5077
5475
  throw new Error(
5078
5476
  `Could not parse managed Dev Container compose file '${file}': ${String(error)}`
@@ -5083,7 +5481,7 @@ function resolveComposeFiles(source, repoPath, linked) {
5083
5481
  }
5084
5482
  }
5085
5483
  const fileArgs = files.flatMap((file) => ["-f", file]);
5086
- const resolved = (0, import_node_child_process6.spawnSync)(
5484
+ const resolved = (0, import_node_child_process7.spawnSync)(
5087
5485
  "docker",
5088
5486
  [
5089
5487
  "compose",
@@ -5109,7 +5507,7 @@ function resolveComposeFiles(source, repoPath, linked) {
5109
5507
  return { directory, files, services: services.sort() };
5110
5508
  }
5111
5509
  function assertIgnoredGeneratedPath(repoPath, generatedPath) {
5112
- const ignored = (0, import_node_child_process6.spawnSync)(
5510
+ const ignored = (0, import_node_child_process7.spawnSync)(
5113
5511
  "git",
5114
5512
  ["-C", repoPath, "check-ignore", "--quiet", "--no-index", "--", generatedPath],
5115
5513
  { encoding: "utf-8" }
@@ -5121,12 +5519,12 @@ function assertIgnoredGeneratedPath(repoPath, generatedPath) {
5121
5519
  }
5122
5520
  }
5123
5521
  function assertGeneratedPathOwnership(generatedPath) {
5124
- if (!import_node_fs14.default.existsSync(generatedPath)) return;
5125
- 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);
5126
5524
  if (!stat.isFile() || stat.isSymbolicLink()) {
5127
5525
  throw new Error(`Managed Dev Container path '${generatedPath}' is not a regular file.`);
5128
5526
  }
5129
- 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];
5130
5528
  if (firstLine2 !== MANAGED_DEVCONTAINER_MARKER) {
5131
5529
  throw new Error(
5132
5530
  `Managed Dev Container path '${generatedPath}' exists without the devrouter ownership marker.`
@@ -5137,7 +5535,7 @@ function isWildcard(values) {
5137
5535
  return values?.length === 1 && values[0] === "*";
5138
5536
  }
5139
5537
  function sha256(contents) {
5140
- 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");
5141
5539
  }
5142
5540
  function safeComposeProject(value) {
5143
5541
  if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(value)) {
@@ -5202,7 +5600,7 @@ function inspectManagedDevcontainerConfig(options) {
5202
5600
  const contents = `${MANAGED_DEVCONTAINER_MARKER}
5203
5601
  ${JSON.stringify(effective, null, 2)}
5204
5602
  `;
5205
- const generatedPath = import_node_path14.default.join(options.repoPath, MANAGED_DEVCONTAINER_PATH);
5603
+ const generatedPath = import_node_path15.default.join(options.repoPath, MANAGED_DEVCONTAINER_PATH);
5206
5604
  assertIgnoredGeneratedPath(options.repoPath, generatedPath);
5207
5605
  assertGeneratedPathOwnership(generatedPath);
5208
5606
  return {
@@ -5229,13 +5627,13 @@ function writeManagedDevcontainerConfig(plan) {
5229
5627
  function inspectManagedDevcontainerGeneratedConfig(plan) {
5230
5628
  let stat;
5231
5629
  try {
5232
- stat = import_node_fs14.default.lstatSync(plan.generatedPath);
5630
+ stat = import_node_fs15.default.lstatSync(plan.generatedPath);
5233
5631
  } catch (error) {
5234
5632
  if (error.code === "ENOENT") return { status: "missing" };
5235
5633
  throw error;
5236
5634
  }
5237
5635
  if (!stat.isFile() || stat.isSymbolicLink()) return { status: "foreign" };
5238
- const contents = import_node_fs14.default.readFileSync(plan.generatedPath, "utf-8");
5636
+ const contents = import_node_fs15.default.readFileSync(plan.generatedPath, "utf-8");
5239
5637
  if (!contents.startsWith(`${MANAGED_DEVCONTAINER_MARKER}
5240
5638
  `)) {
5241
5639
  return { status: "foreign" };
@@ -5247,17 +5645,17 @@ function inspectManagedDevcontainerGeneratedConfig(plan) {
5247
5645
  };
5248
5646
  }
5249
5647
  function removeManagedDevcontainerConfig(plan) {
5250
- if (!import_node_fs14.default.existsSync(plan.generatedPath)) return;
5251
- 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);
5252
5650
  if (!stat.isFile()) {
5253
5651
  throw new Error(`Managed Dev Container path '${plan.generatedPath}' is not a regular file.`);
5254
5652
  }
5255
- const contents = import_node_fs14.default.readFileSync(plan.generatedPath, "utf-8");
5653
+ const contents = import_node_fs15.default.readFileSync(plan.generatedPath, "utf-8");
5256
5654
  if (!contents.startsWith(`${MANAGED_DEVCONTAINER_MARKER}
5257
5655
  `)) {
5258
5656
  throw new Error(`Managed Dev Container path '${plan.generatedPath}' is not devrouter-owned.`);
5259
5657
  }
5260
- import_node_fs14.default.unlinkSync(plan.generatedPath);
5658
+ import_node_fs15.default.unlinkSync(plan.generatedPath);
5261
5659
  }
5262
5660
  function assertSafeContainerId(containerId) {
5263
5661
  if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(containerId)) {
@@ -5284,7 +5682,7 @@ function startExactManagedServices(options) {
5284
5682
  delete env.DEVROUTER_GIT_COMMON_DIR;
5285
5683
  delete env.DEVCONTAINER_COMPOSE_OVERLAY;
5286
5684
  }
5287
- const result = (0, import_node_child_process6.spawnSync)(
5685
+ const result = (0, import_node_child_process7.spawnSync)(
5288
5686
  "docker",
5289
5687
  [
5290
5688
  "compose",
@@ -5314,7 +5712,7 @@ function startExactManagedServices(options) {
5314
5712
  }
5315
5713
  function stopExactManagedService(containerId, service) {
5316
5714
  assertSafeContainerId(containerId);
5317
- const result = (0, import_node_child_process6.spawnSync)("docker", ["stop", containerId], {
5715
+ const result = (0, import_node_child_process7.spawnSync)("docker", ["stop", containerId], {
5318
5716
  encoding: "utf-8",
5319
5717
  stdio: "inherit"
5320
5718
  });
@@ -5322,14 +5720,14 @@ function stopExactManagedService(containerId, service) {
5322
5720
  throw new Error(`Could not stop exact managed service '${service}' (${containerId}).`);
5323
5721
  }
5324
5722
  }
5325
- var import_node_child_process6, import_node_crypto6, import_node_fs14, import_node_path14, 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;
5326
5724
  var init_devcontainer_profile = __esm({
5327
5725
  "src/core/devcontainer-profile.ts"() {
5328
5726
  "use strict";
5329
- import_node_child_process6 = require("child_process");
5330
- import_node_crypto6 = require("crypto");
5331
- import_node_fs14 = __toESM(require("fs"));
5332
- import_node_path14 = __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"));
5333
5731
  import_yaml4 = __toESM(require("yaml"));
5334
5732
  init_atomic_file();
5335
5733
  init_devcontainer_config();
@@ -5343,7 +5741,7 @@ var init_devcontainer_profile = __esm({
5343
5741
  function inspectWorkspaceContainers(options) {
5344
5742
  let ids = options?.ids;
5345
5743
  if (!ids) {
5346
- const listed = (0, import_node_child_process7.spawnSync)(
5744
+ const listed = (0, import_node_child_process8.spawnSync)(
5347
5745
  "docker",
5348
5746
  [
5349
5747
  "ps",
@@ -5363,7 +5761,7 @@ function inspectWorkspaceContainers(options) {
5363
5761
  }
5364
5762
  if (ids.length === 0) return [];
5365
5763
  const template = options?.withSize ? SIZE_INSPECT_TEMPLATE : SAFE_INSPECT_TEMPLATE;
5366
- const inspected = (0, import_node_child_process7.spawnSync)(
5764
+ const inspected = (0, import_node_child_process8.spawnSync)(
5367
5765
  "docker",
5368
5766
  ["inspect", ...options?.withSize ? ["--size"] : [], "--format", template, ...ids],
5369
5767
  { encoding: "utf-8" }
@@ -5378,7 +5776,7 @@ function inspectWorkspaceContainers(options) {
5378
5776
  function workspaceAppContainers(containers, repoPath) {
5379
5777
  return containers.filter((container) => {
5380
5778
  const workingDir = container.labels["com.docker.compose.project.working_dir"];
5381
- return Boolean(workingDir && sameWorkspacePath(workingDir, import_node_path15.default.join(repoPath, ".devcontainer"))) && container.mounts.some(
5779
+ return Boolean(workingDir && sameWorkspacePath(workingDir, import_node_path16.default.join(repoPath, ".devcontainer"))) && container.mounts.some(
5382
5780
  (mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath)
5383
5781
  );
5384
5782
  });
@@ -5386,7 +5784,7 @@ function workspaceAppContainers(containers, repoPath) {
5386
5784
  function hasExactComposeIdentity(container, options) {
5387
5785
  if (options.composeProject !== void 0 && container.labels["com.docker.compose.project"] !== options.composeProject || container.labels["com.docker.compose.service"] !== options.service || !sameWorkspacePath(
5388
5786
  container.labels["com.docker.compose.project.working_dir"] ?? "",
5389
- import_node_path15.default.join(options.repoPath, ".devcontainer")
5787
+ import_node_path16.default.join(options.repoPath, ".devcontainer")
5390
5788
  )) {
5391
5789
  return false;
5392
5790
  }
@@ -5423,12 +5821,12 @@ function resolveRunningWorkspaceContainer(repoPath) {
5423
5821
  }
5424
5822
  return { id: container.id, workspacePath: repoMount.Destination };
5425
5823
  }
5426
- var import_node_child_process7, import_node_path15, SAFE_INSPECT_TEMPLATE, SIZE_INSPECT_TEMPLATE;
5824
+ var import_node_child_process8, import_node_path16, SAFE_INSPECT_TEMPLATE, SIZE_INSPECT_TEMPLATE;
5427
5825
  var init_devpod_environment = __esm({
5428
5826
  "src/core/devpod-environment.ts"() {
5429
5827
  "use strict";
5430
- import_node_child_process7 = require("child_process");
5431
- import_node_path15 = __toESM(require("path"));
5828
+ import_node_child_process8 = require("child_process");
5829
+ import_node_path16 = __toESM(require("path"));
5432
5830
  init_workspace();
5433
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}}}';
5434
5832
  SIZE_INSPECT_TEMPLATE = SAFE_INSPECT_TEMPLATE.replace(
@@ -5882,7 +6280,7 @@ function toRepoStatus(repoPath) {
5882
6280
  const resolvedRepoPath = resolveRepoPath(repoPath);
5883
6281
  const configPath = getRepoConfigPath(resolvedRepoPath);
5884
6282
  const explicitRepo = typeof repoPath === "string" && repoPath.trim().length > 0;
5885
- const configExists = import_node_fs15.default.existsSync(configPath);
6283
+ const configExists = import_node_fs16.default.existsSync(configPath);
5886
6284
  if (!explicitRepo && !configExists) {
5887
6285
  return void 0;
5888
6286
  }
@@ -6030,11 +6428,11 @@ async function collectRouterStatus(repoPath) {
6030
6428
  }
6031
6429
  };
6032
6430
  }
6033
- var import_node_fs15;
6431
+ var import_node_fs16;
6034
6432
  var init_status = __esm({
6035
6433
  "src/core/status.ts"() {
6036
6434
  "use strict";
6037
- import_node_fs15 = __toESM(require("fs"));
6435
+ import_node_fs16 = __toESM(require("fs"));
6038
6436
  init_docker();
6039
6437
  init_managed_runtime_state();
6040
6438
  init_managed_runtime_status();
@@ -6045,7 +6443,7 @@ var init_status = __esm({
6045
6443
 
6046
6444
  // src/core/tls.ts
6047
6445
  function runOrThrow(command, args) {
6048
- const result = (0, import_node_child_process8.spawnSync)(command, args, {
6446
+ const result = (0, import_node_child_process9.spawnSync)(command, args, {
6049
6447
  encoding: "utf-8"
6050
6448
  });
6051
6449
  if (result.status !== 0) {
@@ -6055,7 +6453,7 @@ function runOrThrow(command, args) {
6055
6453
  return result.stdout.trim();
6056
6454
  }
6057
6455
  function commandExists(command) {
6058
- 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" });
6059
6457
  return result.status === 0;
6060
6458
  }
6061
6459
  function ensureMkcert() {
@@ -6073,8 +6471,8 @@ function tlsSetupCommand(repoPath) {
6073
6471
  }
6074
6472
  function getMkcertRootCAPath(options = {}) {
6075
6473
  ensureMkcert();
6076
- const rootCAPath = import_node_path16.default.join(runOrThrow("mkcert", ["-CAROOT"]), "rootCA.pem");
6077
- 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)) {
6078
6476
  throw new Error(
6079
6477
  `mkcert root CA was not found at '${rootCAPath}'. Run: ${tlsSetupCommand(options.repoPath)}`
6080
6478
  );
@@ -6107,7 +6505,7 @@ function parseDnsHostsFromSubjectAltName(subjectAltName) {
6107
6505
  return normalizeUniqueHosts(names);
6108
6506
  }
6109
6507
  function parseCertificateDnsHosts(pem) {
6110
- const certificate = new import_node_crypto7.X509Certificate(pem);
6508
+ const certificate = new import_node_crypto8.X509Certificate(pem);
6111
6509
  const subjectAltName = certificate.subjectAltName ?? "";
6112
6510
  if (subjectAltName.length === 0) {
6113
6511
  return [];
@@ -6175,11 +6573,11 @@ function compactTLSCertificateHosts(hosts) {
6175
6573
  );
6176
6574
  }
6177
6575
  function readCurrentCertificateHosts(options = {}) {
6178
- if (!import_node_fs16.default.existsSync(CERT_FILE)) {
6576
+ if (!import_node_fs17.default.existsSync(CERT_FILE)) {
6179
6577
  return [];
6180
6578
  }
6181
6579
  try {
6182
- const pem = import_node_fs16.default.readFileSync(CERT_FILE, "utf-8");
6580
+ const pem = import_node_fs17.default.readFileSync(CERT_FILE, "utf-8");
6183
6581
  return parseCertificateDnsHosts(pem);
6184
6582
  } catch (error) {
6185
6583
  if (options.replaceMalformed) {
@@ -6307,20 +6705,20 @@ Run: ${tlsSetupCommand(options.repoPath)}`
6307
6705
  );
6308
6706
  }
6309
6707
  }
6310
- var import_node_child_process8, import_node_crypto7, import_node_fs16, import_node_path16, DEFAULT_TLS_CERT_HOSTS, TLS_CERTIFICATE_LOCK_FILE, TLS_CERTIFICATE_LOCK_WAIT_MS, TLS_CERTIFICATE_WRITE_ATTEMPTS;
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;
6311
6709
  var init_tls = __esm({
6312
6710
  "src/core/tls.ts"() {
6313
6711
  "use strict";
6314
- import_node_child_process8 = require("child_process");
6315
- import_node_crypto7 = require("crypto");
6316
- import_node_fs16 = __toESM(require("fs"));
6317
- import_node_path16 = __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"));
6318
6716
  init_docker();
6319
6717
  init_file_lock();
6320
6718
  init_host_routes();
6321
6719
  init_router();
6322
6720
  DEFAULT_TLS_CERT_HOSTS = ["localhost", "*.localhost"];
6323
- TLS_CERTIFICATE_LOCK_FILE = import_node_path16.default.join(DEVROUTER_HOME, "tls-certificate.lock");
6721
+ TLS_CERTIFICATE_LOCK_FILE = import_node_path17.default.join(DEVROUTER_HOME, "tls-certificate.lock");
6324
6722
  TLS_CERTIFICATE_LOCK_WAIT_MS = 6e4;
6325
6723
  TLS_CERTIFICATE_WRITE_ATTEMPTS = 2;
6326
6724
  }
@@ -6328,7 +6726,7 @@ var init_tls = __esm({
6328
6726
 
6329
6727
  // src/core/devpod-registry.ts
6330
6728
  function listDevpodWorkspacesRaw() {
6331
- 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"], {
6332
6730
  encoding: "utf-8"
6333
6731
  });
6334
6732
  if (result.status !== 0) {
@@ -6363,17 +6761,17 @@ function listDevpodWorkspacesRaw() {
6363
6761
  return workspace;
6364
6762
  });
6365
6763
  }
6366
- var import_node_child_process9;
6764
+ var import_node_child_process10;
6367
6765
  var init_devpod_registry = __esm({
6368
6766
  "src/core/devpod-registry.ts"() {
6369
6767
  "use strict";
6370
- import_node_child_process9 = require("child_process");
6768
+ import_node_child_process10 = require("child_process");
6371
6769
  }
6372
6770
  });
6373
6771
 
6374
6772
  // src/core/devsy-workspaces.ts
6375
6773
  function listDevsyWorkspaces() {
6376
- const result = (0, import_node_child_process10.spawnSync)(
6774
+ const result = (0, import_node_child_process11.spawnSync)(
6377
6775
  "devsy",
6378
6776
  ["workspace", "list", "--result-format", "json", "--skip-pro"],
6379
6777
  {
@@ -6439,7 +6837,7 @@ function parseDevsyRuntimeStatus(output2, expectedId) {
6439
6837
  }
6440
6838
  }
6441
6839
  function inspectDevsyRuntimeStatus(devsyId) {
6442
- 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"], {
6443
6841
  encoding: "utf-8",
6444
6842
  timeout: 1e4
6445
6843
  });
@@ -6472,11 +6870,11 @@ function selectDevsyWorkspace(workspaces, repoPath) {
6472
6870
  }
6473
6871
  return matches[0];
6474
6872
  }
6475
- var import_node_child_process10;
6873
+ var import_node_child_process11;
6476
6874
  var init_devsy_workspaces = __esm({
6477
6875
  "src/core/devsy-workspaces.ts"() {
6478
6876
  "use strict";
6479
- import_node_child_process10 = require("child_process");
6877
+ import_node_child_process11 = require("child_process");
6480
6878
  init_workspace();
6481
6879
  }
6482
6880
  });
@@ -6493,13 +6891,13 @@ function parseWorkspaceRuntime(value) {
6493
6891
  }
6494
6892
  function isRuntimeInstalled(runtime) {
6495
6893
  const probeArgs = runtime === "devsy" ? ["--version"] : ["version"];
6496
- 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" });
6497
6895
  return probe.status === 0 && !probe.error;
6498
6896
  }
6499
6897
  function readWorkspaceRuntimeConfig() {
6500
6898
  let raw;
6501
6899
  try {
6502
- 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"));
6503
6901
  } catch {
6504
6902
  return {};
6505
6903
  }
@@ -6520,7 +6918,7 @@ function readWorkspaceRuntimeConfig() {
6520
6918
  function inspectWorkspaceRuntimeConfig() {
6521
6919
  let rawText;
6522
6920
  try {
6523
- rawText = import_node_fs17.default.readFileSync(RUNTIME_CONFIG_FILE, "utf-8");
6921
+ rawText = import_node_fs18.default.readFileSync(RUNTIME_CONFIG_FILE, "utf-8");
6524
6922
  } catch {
6525
6923
  return { exists: false, config: {}, problems: [] };
6526
6924
  }
@@ -6571,7 +6969,7 @@ function writeWorkspaceRuntimeConfig(config) {
6571
6969
  }
6572
6970
  next.devsyInactivityTimeout = timeout;
6573
6971
  }
6574
- import_node_fs17.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
6972
+ import_node_fs18.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
6575
6973
  writeFileAtomically(RUNTIME_CONFIG_FILE, `${JSON.stringify(next, null, 2)}
6576
6974
  `);
6577
6975
  }
@@ -6679,20 +7077,20 @@ function resetWorkspaceRuntimeCaches() {
6679
7077
  cachedRuntime = void 0;
6680
7078
  cachedSnapshots = void 0;
6681
7079
  }
6682
- var import_node_child_process11, import_node_fs17, import_node_path17, 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;
6683
7081
  var init_workspace_runtime = __esm({
6684
7082
  "src/core/workspace-runtime.ts"() {
6685
7083
  "use strict";
6686
- import_node_child_process11 = require("child_process");
6687
- import_node_fs17 = __toESM(require("fs"));
6688
- import_node_path17 = __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"));
6689
7087
  init_atomic_file();
6690
7088
  init_devpod_registry();
6691
7089
  init_devsy_workspaces();
6692
7090
  init_router();
6693
7091
  init_workspace();
6694
7092
  SUPPORTED_RUNTIMES2 = ["devpod", "devsy"];
6695
- RUNTIME_CONFIG_FILE = import_node_path17.default.join(DEVROUTER_HOME, "workspace-runtime.json");
7093
+ RUNTIME_CONFIG_FILE = import_node_path18.default.join(DEVROUTER_HOME, "workspace-runtime.json");
6696
7094
  INACTIVITY_TIMEOUT_PATTERN = /^(?:\d+(?:ms|s|m|h))+$/;
6697
7095
  UnsupportedWorkspaceRuntimeError = class extends Error {
6698
7096
  };
@@ -6713,7 +7111,7 @@ function outputFromResult(result) {
6713
7111
  return output2.length > 0 ? output2 : void 0;
6714
7112
  }
6715
7113
  function runTool(command, args = []) {
6716
- const result = (0, import_node_child_process12.spawnSync)(command, args, {
7114
+ const result = (0, import_node_child_process13.spawnSync)(command, args, {
6717
7115
  encoding: "utf-8"
6718
7116
  });
6719
7117
  if (result.error) {
@@ -6770,12 +7168,12 @@ function parseMinimumNodeMajor(value) {
6770
7168
  return Number(match[1]);
6771
7169
  }
6772
7170
  function readPackageJson(repoPath) {
6773
- const packagePath = import_node_path18.default.join(repoPath, "package.json");
6774
- 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)) {
6775
7173
  return void 0;
6776
7174
  }
6777
7175
  try {
6778
- return JSON.parse(import_node_fs18.default.readFileSync(packagePath, "utf-8"));
7176
+ return JSON.parse(import_node_fs19.default.readFileSync(packagePath, "utf-8"));
6779
7177
  } catch {
6780
7178
  return void 0;
6781
7179
  }
@@ -6840,7 +7238,44 @@ function nodeToolchainCheck(repoPath) {
6840
7238
  details: details.join(", ")
6841
7239
  };
6842
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
+ }
6843
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) {
6844
7279
  const checks = [];
6845
7280
  const compose = runTool("docker", ["compose", "version"]);
6846
7281
  checks.push({
@@ -6894,6 +7329,13 @@ function buildGlobalToolChecks(repoPath) {
6894
7329
  ].filter(Boolean).join(", "),
6895
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"
6896
7331
  });
7332
+ if (workspaceRuntime === "devsy") {
7333
+ checks.push(
7334
+ devsyAgentCheck(
7335
+ inspectDevsyAgent({ versionOutput: runtimeTool.ok ? runtimeTool.output : "" })
7336
+ )
7337
+ );
7338
+ }
6897
7339
  const configProblems = [...configInspection.problems];
6898
7340
  const machineResolvedDevpod = workspaceRuntime === "devpod" && (runtimeResolution.source === "auto-detect" || runtimeResolution.source === "default");
6899
7341
  if (machineConfig.devsyInactivityTimeout && machineResolvedDevpod) {
@@ -6915,14 +7357,16 @@ function buildGlobalToolChecks(repoPath) {
6915
7357
  checks.push(nodeToolchainCheck(repoPath));
6916
7358
  return checks;
6917
7359
  }
6918
- var import_node_child_process12, import_node_fs18, import_node_path18;
7360
+ var import_node_child_process13, import_node_fs19, import_node_path19, DEVSY_DISABLE_TELEMETRY;
6919
7361
  var init_tool_diagnostics = __esm({
6920
7362
  "src/core/tool-diagnostics.ts"() {
6921
7363
  "use strict";
6922
- import_node_child_process12 = require("child_process");
6923
- import_node_fs18 = __toESM(require("fs"));
6924
- import_node_path18 = __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();
6925
7368
  init_workspace_runtime();
7369
+ DEVSY_DISABLE_TELEMETRY = "DEVSY_DISABLE_TELEMETRY";
6926
7370
  }
6927
7371
  });
6928
7372
 
@@ -6991,7 +7435,7 @@ function inspectDevpodRuntimeStatus(devpodId, repoPath) {
6991
7435
  if (resolveWorkspaceRuntimeOrDefault(repoPath) === "devsy") {
6992
7436
  return inspectDevsyRuntimeStatus(devpodId);
6993
7437
  }
6994
- 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"], {
6995
7439
  encoding: "utf-8"
6996
7440
  });
6997
7441
  if (result.status !== 0 || result.error) return "unknown";
@@ -7024,11 +7468,11 @@ function selectDevpodWorkspace(workspaces, repoPath) {
7024
7468
  }
7025
7469
  return matches[0];
7026
7470
  }
7027
- var import_node_child_process13;
7471
+ var import_node_child_process14;
7028
7472
  var init_devpod_workspaces = __esm({
7029
7473
  "src/core/devpod-workspaces.ts"() {
7030
7474
  "use strict";
7031
- import_node_child_process13 = require("child_process");
7475
+ import_node_child_process14 = require("child_process");
7032
7476
  init_devpod_registry();
7033
7477
  init_devsy_workspaces();
7034
7478
  init_workspace();
@@ -7048,7 +7492,7 @@ function failedStartMayHaveAttached(devsyId, repoPath) {
7048
7492
  }
7049
7493
  }
7050
7494
  function withMutationLock(activity, target, operation) {
7051
- import_node_fs19.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
7495
+ import_node_fs20.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
7052
7496
  return withFileLockSync(
7053
7497
  DEVSY_MUTATION_LOCK_FILE,
7054
7498
  {
@@ -7062,7 +7506,7 @@ function withMutationLock(activity, target, operation) {
7062
7506
  );
7063
7507
  }
7064
7508
  function withMutationLockAsync(activity, target, operation) {
7065
- import_node_fs19.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
7509
+ import_node_fs20.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
7066
7510
  return withFileLock(
7067
7511
  DEVSY_MUTATION_LOCK_FILE,
7068
7512
  {
@@ -7080,7 +7524,7 @@ function commandFailure2(result) {
7080
7524
  }
7081
7525
  function runDevsyUp(args, env, quiet) {
7082
7526
  return new Promise((resolve) => {
7083
- const child = (0, import_node_child_process14.spawn)("devsy", args, {
7527
+ const child = (0, import_node_child_process15.spawn)("devsy", args, {
7084
7528
  stdio: ["inherit", quiet ? 2 : "inherit", "pipe"],
7085
7529
  env
7086
7530
  });
@@ -7112,7 +7556,7 @@ function runDevsyUp(args, env, quiet) {
7112
7556
  }
7113
7557
  function runDevsyAction(action2, devsyId, force = false) {
7114
7558
  const args = action2 === "delete" ? ["delete", devsyId, ...force ? ["--force"] : [], "--ignore-not-found"] : ["stop", devsyId];
7115
- 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" });
7116
7560
  if (result.status !== 0) {
7117
7561
  throw new Error(
7118
7562
  `devsy workspace ${action2}${force ? " --force" : ""} failed for '${devsyId}': ${commandFailure2(result) || "unknown error"}`
@@ -7172,8 +7616,16 @@ function assertDevsyTarget(devsyId, repoPath) {
7172
7616
  }
7173
7617
  return id;
7174
7618
  }
7175
- function startDevsyWorkspace(options) {
7619
+ async function startDevsyWorkspace(options) {
7176
7620
  const activity = options.recreate ? "Devsy recreate" : "Devsy start";
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
+ }
7177
7629
  return withMutationLockAsync(activity, options.repoPath, async () => {
7178
7630
  let devsyId = assertDevsyTarget(options.devsyId, options.repoPath);
7179
7631
  if (devsyId && options.recreate) {
@@ -7203,6 +7655,7 @@ function startDevsyWorkspace(options) {
7203
7655
  }
7204
7656
  if (options.recreate) args.push("--recreate");
7205
7657
  const env = { ...process.env };
7658
+ env.DEVSY_AGENT_BINARY = agent.binaryPath;
7206
7659
  if (options.workspace) {
7207
7660
  env.WORKSPACE = options.workspace.token;
7208
7661
  env.DEVROUTER_WORKSPACE = options.workspace.token;
@@ -7219,7 +7672,7 @@ function startDevsyWorkspace(options) {
7219
7672
  let message = `devsy workspace up failed for '${devsyId ?? options.repoPath}'.`;
7220
7673
  if (result.error?.message) message += ` ${result.error.message}`;
7221
7674
  if (AGENT_ACQUISITION_RE.test(result.stderrTail)) {
7222
- message += " Devsy could not obtain its agent binary; allow Devsy to download it or set DEVSY_AGENT_BINARY to a verified official Devsy agent binary matching this platform and Devsy version.";
7675
+ message += ` Devsy rejected the verified agent source. Run: ${DEVSY_AGENT_SETUP_COMMAND}`;
7223
7676
  }
7224
7677
  if (failedStartMayHaveAttached(devsyId, options.repoPath)) {
7225
7678
  throw new DevsyStartPostconditionError(message);
@@ -7246,17 +7699,18 @@ function startDevsyWorkspace(options) {
7246
7699
  }
7247
7700
  });
7248
7701
  }
7249
- var import_node_child_process14, import_node_fs19, import_node_path19, DEVSY_MUTATION_LOCK_FILE, DEVSY_MUTATION_WAIT_MS, DevsyStartPostconditionError, AGENT_ACQUISITION_RE, DEVSY_STDERR_TAIL_BYTES;
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;
7250
7703
  var init_devsy_mutation = __esm({
7251
7704
  "src/core/devsy-mutation.ts"() {
7252
7705
  "use strict";
7253
- import_node_child_process14 = require("child_process");
7254
- import_node_fs19 = __toESM(require("fs"));
7255
- import_node_path19 = __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();
7256
7710
  init_devsy_workspaces();
7257
7711
  init_file_lock();
7258
7712
  init_router();
7259
- DEVSY_MUTATION_LOCK_FILE = import_node_path19.default.join(DEVROUTER_HOME, "devsy-mutation.lock");
7713
+ DEVSY_MUTATION_LOCK_FILE = import_node_path20.default.join(DEVROUTER_HOME, "devsy-mutation.lock");
7260
7714
  DEVSY_MUTATION_WAIT_MS = 18e5;
7261
7715
  DevsyStartPostconditionError = class extends Error {
7262
7716
  };
@@ -7267,7 +7721,7 @@ var init_devsy_mutation = __esm({
7267
7721
 
7268
7722
  // src/core/devpod-mutation.ts
7269
7723
  function withMutationLock2(activity, target, operation) {
7270
- import_node_fs20.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
7724
+ import_node_fs21.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
7271
7725
  return withFileLockSync(
7272
7726
  DEVPOD_MUTATION_LOCK_FILE,
7273
7727
  {
@@ -7285,7 +7739,7 @@ function commandFailure3(result) {
7285
7739
  }
7286
7740
  function runDevpodAction(action2, devpodId, force = false) {
7287
7741
  const args = action2 === "delete" ? [action2, devpodId, ...force ? ["--force"] : [], "--ignore-not-found"] : [action2, devpodId];
7288
- 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" });
7289
7743
  if (result.status !== 0) {
7290
7744
  throw new Error(
7291
7745
  `devpod ${action2}${force ? " --force" : ""} failed for '${devpodId}': ${commandFailure3(result) || "unknown error"}`
@@ -7413,7 +7867,7 @@ async function startDevpodWorkspace(options) {
7413
7867
  delete env.DEVROUTER_GIT_COMMON_DIR;
7414
7868
  delete env.DEVCONTAINER_COMPOSE_OVERLAY;
7415
7869
  }
7416
- const result = (0, import_node_child_process15.spawnSync)("devpod", args, {
7870
+ const result = (0, import_node_child_process16.spawnSync)("devpod", args, {
7417
7871
  stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
7418
7872
  env
7419
7873
  });
@@ -7441,19 +7895,19 @@ async function startDevpodWorkspace(options) {
7441
7895
  }
7442
7896
  });
7443
7897
  }
7444
- var import_node_child_process15, import_node_fs20, import_node_path20, 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;
7445
7899
  var init_devpod_mutation = __esm({
7446
7900
  "src/core/devpod-mutation.ts"() {
7447
7901
  "use strict";
7448
- import_node_child_process15 = require("child_process");
7449
- import_node_fs20 = __toESM(require("fs"));
7450
- import_node_path20 = __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"));
7451
7905
  init_devpod_workspaces();
7452
7906
  init_devsy_mutation();
7453
7907
  init_file_lock();
7454
7908
  init_router();
7455
7909
  init_workspace_runtime();
7456
- DEVPOD_MUTATION_LOCK_FILE = import_node_path20.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
7910
+ DEVPOD_MUTATION_LOCK_FILE = import_node_path21.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
7457
7911
  DEVPOD_MUTATION_WAIT_MS = 18e5;
7458
7912
  DevpodStartPostconditionError = class extends Error {
7459
7913
  };
@@ -7467,7 +7921,7 @@ function commandError(command, repoPath, stderr) {
7467
7921
  );
7468
7922
  }
7469
7923
  function resolveGitCommonDir(repoPath) {
7470
- 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"], {
7471
7925
  encoding: "utf-8",
7472
7926
  env: READ_ONLY_GIT_ENV
7473
7927
  });
@@ -7475,10 +7929,10 @@ function resolveGitCommonDir(repoPath) {
7475
7929
  if (result.status !== 0 || !output2) {
7476
7930
  throw commandError("Could not resolve the Git common directory", repoPath, result.stderr);
7477
7931
  }
7478
- return comparableWorkspacePath(import_node_path21.default.isAbsolute(output2) ? output2 : import_node_path21.default.resolve(repoPath, output2));
7932
+ return comparableWorkspacePath(import_node_path22.default.isAbsolute(output2) ? output2 : import_node_path22.default.resolve(repoPath, output2));
7479
7933
  }
7480
7934
  function resolveGitTopLevel(repoPath) {
7481
- 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"], {
7482
7936
  encoding: "utf-8",
7483
7937
  env: READ_ONLY_GIT_ENV
7484
7938
  });
@@ -7489,7 +7943,7 @@ function resolveGitTopLevel(repoPath) {
7489
7943
  return comparableWorkspacePath(output2);
7490
7944
  }
7491
7945
  function listGitWorktrees(repoPath) {
7492
- 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"], {
7493
7947
  encoding: "utf-8",
7494
7948
  env: READ_ONLY_GIT_ENV
7495
7949
  });
@@ -7526,7 +7980,7 @@ function listGitWorktrees(repoPath) {
7526
7980
  return worktrees;
7527
7981
  }
7528
7982
  function ownershipDirectory(repoPath) {
7529
- return import_node_path21.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
7983
+ return import_node_path22.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
7530
7984
  }
7531
7985
  function validateWorkspace(value, label) {
7532
7986
  if (typeof value !== "string" || wsFromBranch(value) !== value) {
@@ -7554,7 +8008,7 @@ function validateRecord(value, expectedWorkspace) {
7554
8008
  `workspace ownership file '${expectedWorkspace}' contains identity '${workspace}'`
7555
8009
  );
7556
8010
  }
7557
- if (typeof candidate.worktreePath !== "string" || !import_node_path21.default.isAbsolute(candidate.worktreePath)) {
8011
+ if (typeof candidate.worktreePath !== "string" || !import_node_path22.default.isAbsolute(candidate.worktreePath)) {
7558
8012
  throw new Error("invalid workspace ownership worktreePath");
7559
8013
  }
7560
8014
  if (candidate.branch !== null && typeof candidate.branch !== "string") {
@@ -7574,7 +8028,7 @@ function validateRecord(value, expectedWorkspace) {
7574
8028
  function readRecordFile(filePath, expectedWorkspace) {
7575
8029
  let parsed;
7576
8030
  try {
7577
- parsed = JSON.parse(import_node_fs21.default.readFileSync(filePath, "utf-8"));
8031
+ parsed = JSON.parse(import_node_fs22.default.readFileSync(filePath, "utf-8"));
7578
8032
  } catch (error) {
7579
8033
  if (error instanceof SyntaxError) {
7580
8034
  throw new Error(`invalid workspace ownership JSON at '${filePath}'`);
@@ -7590,7 +8044,7 @@ function listWorkspaceOwnership(repoPath) {
7590
8044
  function listWorkspaceOwnershipInDirectory(directory) {
7591
8045
  let entries;
7592
8046
  try {
7593
- entries = import_node_fs21.default.readdirSync(directory, { withFileTypes: true });
8047
+ entries = import_node_fs22.default.readdirSync(directory, { withFileTypes: true });
7594
8048
  } catch (error) {
7595
8049
  if (error.code === "ENOENT") return [];
7596
8050
  throw error;
@@ -7598,14 +8052,14 @@ function listWorkspaceOwnershipInDirectory(directory) {
7598
8052
  return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
7599
8053
  const workspace = entry.name.slice(0, -".json".length);
7600
8054
  validateWorkspace(workspace, "filename");
7601
- return readRecordFile(import_node_path21.default.join(directory, entry.name), workspace);
8055
+ return readRecordFile(import_node_path22.default.join(directory, entry.name), workspace);
7602
8056
  });
7603
8057
  }
7604
8058
  function writeWorkspaceOwnershipInDirectory(directory, input2) {
7605
8059
  const workspace = validateWorkspace(input2.workspace, "workspace");
7606
8060
  const devpodId = validateWorkspace(input2.devpodId, "devpodId");
7607
8061
  const worktreePath = comparableWorkspacePath(input2.worktreePath);
7608
- const filePath = import_node_path21.default.join(directory, `${workspace}.json`);
8062
+ const filePath = import_node_path22.default.join(directory, `${workspace}.json`);
7609
8063
  const now = (/* @__PURE__ */ new Date()).toISOString();
7610
8064
  const records = listWorkspaceOwnershipInDirectory(directory);
7611
8065
  const existing = records.find((record2) => record2.workspace === workspace);
@@ -7641,9 +8095,9 @@ function writeWorkspaceOwnershipInDirectory(directory, input2) {
7641
8095
  return record;
7642
8096
  }
7643
8097
  function removeWorkspaceOwnershipInDirectory(directory, workspace) {
7644
- const filePath = import_node_path21.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
8098
+ const filePath = import_node_path22.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
7645
8099
  try {
7646
- import_node_fs21.default.rmSync(filePath);
8100
+ import_node_fs22.default.rmSync(filePath);
7647
8101
  return true;
7648
8102
  } catch (error) {
7649
8103
  if (error.code === "ENOENT") return false;
@@ -7654,7 +8108,7 @@ function sameOwnershipRecord(left, right) {
7654
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;
7655
8109
  }
7656
8110
  function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
7657
- const filePath = import_node_path21.default.join(
8111
+ const filePath = import_node_path22.default.join(
7658
8112
  directory,
7659
8113
  `${validateWorkspace(expected.workspace, "workspace")}.json`
7660
8114
  );
@@ -7666,14 +8120,14 @@ function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
7666
8120
  throw error;
7667
8121
  }
7668
8122
  if (!sameOwnershipRecord(current, expected)) return "changed";
7669
- import_node_fs21.default.rmSync(filePath);
8123
+ import_node_fs22.default.rmSync(filePath);
7670
8124
  return "removed";
7671
8125
  }
7672
8126
  function withWorkspaceOwnershipTransaction(repoPath, operation, options = {}) {
7673
8127
  const directory = ownershipDirectory(repoPath);
7674
- import_node_fs21.default.mkdirSync(directory, { recursive: true });
8128
+ import_node_fs22.default.mkdirSync(directory, { recursive: true });
7675
8129
  return withFileLockSync(
7676
- import_node_path21.default.join(directory, ".lock"),
8130
+ import_node_path22.default.join(directory, ".lock"),
7677
8131
  {
7678
8132
  activity: "workspace ownership transaction",
7679
8133
  target: `'${repoPath}'`,
@@ -7706,7 +8160,7 @@ function persistedWorkspaceOwners(repoPath, worktreePath) {
7706
8160
  try {
7707
8161
  workspace = readPersistedWorkspace(worktree.path);
7708
8162
  } catch (error) {
7709
- if (!import_node_fs21.default.existsSync(worktree.path)) continue;
8163
+ if (!import_node_fs22.default.existsSync(worktree.path)) continue;
7710
8164
  throw error;
7711
8165
  }
7712
8166
  if (!workspace) continue;
@@ -7869,7 +8323,7 @@ function inspectWorkspaceOwnership(record, worktrees, devpods) {
7869
8323
  if (worktree?.locked) {
7870
8324
  return { ownerStatus: "locked", devpodStatus, worktree };
7871
8325
  }
7872
- if ((!worktree || worktree.prunable) && import_node_fs21.default.existsSync(record.worktreePath)) {
8326
+ if ((!worktree || worktree.prunable) && import_node_fs22.default.existsSync(record.worktreePath)) {
7873
8327
  return { ownerStatus: "conflict", devpodStatus, worktree };
7874
8328
  }
7875
8329
  if (!worktree || worktree.prunable) {
@@ -7893,20 +8347,20 @@ function listMissingWorkspaceOwnership(repoPath) {
7893
8347
  (record) => inspectWorkspaceOwnership(record, worktrees, void 0).ownerStatus === "missing"
7894
8348
  );
7895
8349
  }
7896
- var import_node_child_process16, import_node_fs21, import_node_path21, 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;
7897
8351
  var init_workspace_ownership = __esm({
7898
8352
  "src/core/workspace-ownership.ts"() {
7899
8353
  "use strict";
7900
- import_node_child_process16 = require("child_process");
7901
- import_node_fs21 = __toESM(require("fs"));
7902
- import_node_path21 = __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"));
7903
8357
  init_atomic_file();
7904
8358
  init_devpod_workspaces();
7905
8359
  init_file_lock();
7906
8360
  init_workspace();
7907
8361
  READ_ONLY_GIT_ENV = { ...process.env, GIT_OPTIONAL_LOCKS: "0" };
7908
8362
  OWNERSHIP_VERSION = 1;
7909
- OWNERSHIP_DIR = import_node_path21.default.join("devrouter", "workspaces");
8363
+ OWNERSHIP_DIR = import_node_path22.default.join("devrouter", "workspaces");
7910
8364
  }
7911
8365
  });
7912
8366
 
@@ -7920,10 +8374,10 @@ function inRepositoryWorkspaceScope(repoPath, worktreePath, livePaths) {
7920
8374
  if (livePaths.some((candidate) => sameWorkspacePath(candidate, worktreePath))) return true;
7921
8375
  const comparableRepo = comparableWorkspacePath(repoPath);
7922
8376
  const comparableWorktree = comparableWorkspacePath(worktreePath);
7923
- const localRoot = import_node_path22.default.join(comparableRepo, "trees") + import_node_path22.default.sep;
8377
+ const localRoot = import_node_path23.default.join(comparableRepo, "trees") + import_node_path23.default.sep;
7924
8378
  if (comparableWorktree.startsWith(localRoot)) return true;
7925
- const legacyPrefix = `${import_node_path22.default.basename(comparableRepo)}-`;
7926
- return import_node_path22.default.dirname(comparableWorktree) === import_node_path22.default.dirname(comparableRepo) && import_node_path22.default.basename(comparableWorktree).startsWith(legacyPrefix);
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);
7927
8381
  }
7928
8382
  function previewActions(devpodStatus, routeCount, includeRecord) {
7929
8383
  const actions = [
@@ -8175,11 +8629,11 @@ function applyWorkspaceGc(plan) {
8175
8629
  candidates
8176
8630
  };
8177
8631
  }
8178
- var import_node_path22;
8632
+ var import_node_path23;
8179
8633
  var init_workspace_gc = __esm({
8180
8634
  "src/core/workspace-gc.ts"() {
8181
8635
  "use strict";
8182
- import_node_path22 = __toESM(require("path"));
8636
+ import_node_path23 = __toESM(require("path"));
8183
8637
  init_devpod_mutation();
8184
8638
  init_devpod_workspaces();
8185
8639
  init_host_routes();
@@ -8249,11 +8703,11 @@ function inspectPostgresCredentials(repoPath, config) {
8249
8703
  parseErrors.push(`${app.name}: ${composeFile} (${message})`);
8250
8704
  continue;
8251
8705
  }
8252
- if (!import_node_fs22.default.existsSync(absolutePath)) {
8706
+ if (!import_node_fs23.default.existsSync(absolutePath)) {
8253
8707
  continue;
8254
8708
  }
8255
8709
  try {
8256
- const raw = import_node_fs22.default.readFileSync(absolutePath, "utf-8");
8710
+ const raw = import_node_fs23.default.readFileSync(absolutePath, "utf-8");
8257
8711
  const parsed = import_yaml5.default.parse(raw);
8258
8712
  const root = asRecord2(parsed);
8259
8713
  const services = asRecord2(root?.services);
@@ -8532,7 +8986,7 @@ async function buildDoctorReport(options = {}) {
8532
8986
  const config = runtimeConfig.config;
8533
8987
  loadedConfig = config;
8534
8988
  loadedWorkspace = runtimeConfig.workspace;
8535
- const cliVersion = true ? "0.0.46" : "0.0.0-dev";
8989
+ const cliVersion = true ? "0.0.49" : "0.0.0-dev";
8536
8990
  const configVersion = config.devrouter?.version;
8537
8991
  if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
8538
8992
  addCheck(checks, {
@@ -8562,9 +9016,9 @@ async function buildDoctorReport(options = {}) {
8562
9016
  (app) => app.docker.composeFiles.map((filePath) => ({
8563
9017
  app: app.name,
8564
9018
  filePath,
8565
- absolutePath: import_node_path23.default.resolve(repo.path, filePath)
9019
+ absolutePath: import_node_path24.default.resolve(repo.path, filePath)
8566
9020
  }))
8567
- ).filter((entry) => !import_node_fs22.default.existsSync(entry.absolutePath));
9021
+ ).filter((entry) => !import_node_fs23.default.existsSync(entry.absolutePath));
8568
9022
  addCheck(checks, {
8569
9023
  id: "repo.compose-files",
8570
9024
  level: missingComposeFiles.length === 0 ? "ok" : "error",
@@ -8607,8 +9061,8 @@ async function buildDoctorReport(options = {}) {
8607
9061
  const missingHostCwds = config.apps.filter((app) => app.runtime === "host").map((app) => ({
8608
9062
  app: app.name,
8609
9063
  cwd: app.hostRun.cwd,
8610
- absolutePath: import_node_path23.default.resolve(repo.path, app.hostRun.cwd)
8611
- })).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));
8612
9066
  addCheck(checks, {
8613
9067
  id: "repo.host-cwd",
8614
9068
  level: missingHostCwds.length === 0 ? "ok" : "error",
@@ -8752,12 +9206,12 @@ async function buildDoctorReport(options = {}) {
8752
9206
  nextSteps
8753
9207
  };
8754
9208
  }
8755
- var import_node_fs22, import_node_path23, import_yaml5, POSTGRES_DEFAULTS;
9209
+ var import_node_fs23, import_node_path24, import_yaml5, POSTGRES_DEFAULTS;
8756
9210
  var init_doctor = __esm({
8757
9211
  "src/core/doctor.ts"() {
8758
9212
  "use strict";
8759
- import_node_fs22 = __toESM(require("fs"));
8760
- import_node_path23 = __toESM(require("path"));
9213
+ import_node_fs23 = __toESM(require("fs"));
9214
+ import_node_path24 = __toESM(require("path"));
8761
9215
  import_yaml5 = __toESM(require("yaml"));
8762
9216
  init_docker();
8763
9217
  init_host_routes();
@@ -8864,11 +9318,13 @@ async function runSetup(options = {}) {
8864
9318
  })
8865
9319
  );
8866
9320
  }
9321
+ let requestedWorkspaceRuntime;
8867
9322
  if (options.workspaceRuntime !== void 0 || options.devsyInactivityTimeout !== void 0) {
8868
9323
  try {
8869
9324
  const existing = readWorkspaceRuntimeConfig();
9325
+ requestedWorkspaceRuntime = options.workspaceRuntime ? parseWorkspaceRuntime(options.workspaceRuntime) : void 0;
8870
9326
  const next = {
8871
- runtime: options.workspaceRuntime ? parseWorkspaceRuntime(options.workspaceRuntime) : existing.runtime,
9327
+ runtime: requestedWorkspaceRuntime ?? existing.runtime,
8872
9328
  devsyInactivityTimeout: options.devsyInactivityTimeout ?? existing.devsyInactivityTimeout
8873
9329
  };
8874
9330
  writeWorkspaceRuntimeConfig(next);
@@ -8890,6 +9346,28 @@ async function runSetup(options = {}) {
8890
9346
  );
8891
9347
  }
8892
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
+ }
8893
9371
  try {
8894
9372
  const existed = await networkExists(DEVNET_NAME);
8895
9373
  await ensureNetwork(DEVNET_NAME);
@@ -8979,6 +9457,7 @@ async function runSetup(options = {}) {
8979
9457
  var init_setup = __esm({
8980
9458
  "src/core/setup.ts"() {
8981
9459
  "use strict";
9460
+ init_devsy_agent();
8982
9461
  init_docker();
8983
9462
  init_doctor();
8984
9463
  init_repo_config();
@@ -9047,12 +9526,12 @@ function parseSsPortListeners(stdout, port) {
9047
9526
  return listeners;
9048
9527
  }
9049
9528
  function findPortListeners(port) {
9050
- 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"], {
9051
9530
  encoding: "utf-8"
9052
9531
  });
9053
9532
  if (result.error && result.error.code === "ENOENT") {
9054
9533
  if (process.platform === "linux") {
9055
- 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" });
9056
9535
  if (ssResult.status === 0 && ssResult.stdout) {
9057
9536
  return parseSsPortListeners(ssResult.stdout, port);
9058
9537
  }
@@ -9081,11 +9560,11 @@ function findPortListeners(port) {
9081
9560
  };
9082
9561
  });
9083
9562
  }
9084
- var import_node_child_process17;
9563
+ var import_node_child_process18;
9085
9564
  var init_ports = __esm({
9086
9565
  "src/util/ports.ts"() {
9087
9566
  "use strict";
9088
- import_node_child_process17 = require("child_process");
9567
+ import_node_child_process18 = require("child_process");
9089
9568
  }
9090
9569
  });
9091
9570
 
@@ -9162,18 +9641,18 @@ function probeHttpRoute(host, options = {}) {
9162
9641
  args.push("--cacert", CERT_FILE);
9163
9642
  }
9164
9643
  args.push(url);
9165
- 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" });
9166
9645
  const stdout = result.stdout?.trim() ?? "";
9167
9646
  const status = /^\d{3}$/.test(stdout) ? Number(stdout) : void 0;
9168
9647
  const ok = result.status === 0 && status !== void 0 && status >= 100 && status < 500;
9169
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"}`;
9170
9649
  return { ok, status, details };
9171
9650
  }
9172
- var import_node_child_process18;
9651
+ var import_node_child_process19;
9173
9652
  var init_http_route_probe = __esm({
9174
9653
  "src/core/http-route-probe.ts"() {
9175
9654
  "use strict";
9176
- import_node_child_process18 = require("child_process");
9655
+ import_node_child_process19 = require("child_process");
9177
9656
  init_router();
9178
9657
  init_tls();
9179
9658
  }
@@ -9249,14 +9728,143 @@ var init_route_publication = __esm({
9249
9728
  }
9250
9729
  });
9251
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?per_page=${ROUTER_API_PAGE_SIZE}`
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
+ reachedPageLimit: value.length >= ROUTER_API_PAGE_SIZE,
9766
+ names: new Set(
9767
+ value.flatMap((item) => {
9768
+ if (!item || typeof item !== "object") return [];
9769
+ const name = item.name;
9770
+ return typeof name === "string" ? [name] : [];
9771
+ })
9772
+ )
9773
+ };
9774
+ }
9775
+ function inspectExpectedRoutes(routes) {
9776
+ const missing = [];
9777
+ const failures = [];
9778
+ for (const protocol of ["http", "tcp"]) {
9779
+ const expected = routes.filter((route) => (route.protocol ?? "http") === protocol).map((route) => buildHostRouteRouterName(route.repoPath, route.name));
9780
+ if (expected.length === 0) continue;
9781
+ const result = inspectRouterApi(protocol);
9782
+ if (!result.ok) {
9783
+ failures.push(result.details);
9784
+ missing.push(...expected);
9785
+ continue;
9786
+ }
9787
+ const absent = expected.filter((name) => !result.names.has(name));
9788
+ if (absent.length > 0 && result.reachedPageLimit) {
9789
+ failures.push(
9790
+ `${protocol.toUpperCase()} router API reached the ${ROUTER_API_PAGE_SIZE}-entry safety limit`
9791
+ );
9792
+ }
9793
+ missing.push(...absent);
9794
+ }
9795
+ return missing.length === 0 ? { ok: true } : {
9796
+ ok: false,
9797
+ missing,
9798
+ details: failures.length > 0 ? failures.join("; ") : "router names are absent"
9799
+ };
9800
+ }
9801
+ async function waitForExpectedRoutes(routes, timeoutMs, pollIntervalMs) {
9802
+ const deadline = Date.now() + timeoutMs;
9803
+ let result;
9804
+ do {
9805
+ result = inspectExpectedRoutes(routes);
9806
+ if (result.ok) return result;
9807
+ if (Date.now() < deadline) {
9808
+ await sleep(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())));
9809
+ }
9810
+ } while (Date.now() < deadline);
9811
+ return result;
9812
+ }
9813
+ async function ensureTraefikRoutesLoaded(routes, options = {}) {
9814
+ if (routes.length === 0) return { restarted: false };
9815
+ const initialTimeoutMs = options.initialTimeoutMs ?? DEFAULT_INITIAL_TIMEOUT_MS;
9816
+ const recoveryTimeoutMs = options.recoveryTimeoutMs ?? DEFAULT_RECOVERY_TIMEOUT_MS;
9817
+ const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
9818
+ const initial = await waitForExpectedRoutes(routes, initialTimeoutMs, pollIntervalMs);
9819
+ if (initial.ok) return { restarted: false };
9820
+ import_node_fs24.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
9821
+ return withFileLock(
9822
+ ROUTER_RELOAD_LOCK_FILE,
9823
+ {
9824
+ activity: "Traefik route reload recovery",
9825
+ waitMs: initialTimeoutMs + recoveryTimeoutMs + 1e4,
9826
+ fair: true,
9827
+ onWait: createStderrWaitReporter("Traefik route reload recovery", "shared router")
9828
+ },
9829
+ async () => {
9830
+ const recheck = await waitForExpectedRoutes(routes, 0, pollIntervalMs);
9831
+ if (recheck.ok) return { restarted: false };
9832
+ restartRouterStack();
9833
+ const recovered = await waitForExpectedRoutes(routes, recoveryTimeoutMs, pollIntervalMs);
9834
+ if (recovered.ok) return { restarted: true };
9835
+ throw new Error(
9836
+ `Traefik did not load file-provider routes after one restart: ${recovered.missing.join(", ")} (${recovered.details}). Inspect: devrouter logs --tail 100`
9837
+ );
9838
+ }
9839
+ );
9840
+ }
9841
+ 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, ROUTER_API_PAGE_SIZE;
9842
+ var init_traefik_route_health = __esm({
9843
+ "src/core/traefik-route-health.ts"() {
9844
+ "use strict";
9845
+ import_node_child_process20 = require("child_process");
9846
+ import_node_fs24 = __toESM(require("fs"));
9847
+ import_node_path25 = __toESM(require("path"));
9848
+ init_file_lock();
9849
+ init_host_routes();
9850
+ init_router();
9851
+ ROUTER_API_BASE = "http://127.0.0.1:8080/api";
9852
+ ROUTER_RELOAD_LOCK_FILE = import_node_path25.default.join(DEVROUTER_HOME, "router-reload.lock");
9853
+ DEFAULT_INITIAL_TIMEOUT_MS = 3e3;
9854
+ DEFAULT_RECOVERY_TIMEOUT_MS = 1e4;
9855
+ DEFAULT_POLL_INTERVAL_MS = 250;
9856
+ ROUTER_API_PAGE_SIZE = 1e3;
9857
+ }
9858
+ });
9859
+
9252
9860
  // src/core/workspace-ensure.ts
9253
9861
  function assertOverlay(container, repoPath) {
9254
9862
  const workingDir = container.labels["com.docker.compose.project.working_dir"];
9255
- if (!workingDir || !sameWorkspacePath(workingDir, import_node_path24.default.join(repoPath, ".devcontainer"))) {
9863
+ if (!workingDir || !sameWorkspacePath(workingDir, import_node_path26.default.join(repoPath, ".devcontainer"))) {
9256
9864
  throw new Error(`Container '${container.id}' does not belong to the exact worktree.`);
9257
9865
  }
9258
9866
  const configFiles = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").filter(Boolean);
9259
- const expectedOverlay = import_node_path24.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
9867
+ const expectedOverlay = import_node_path26.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
9260
9868
  if (!configFiles.some((configFile) => sameWorkspacePath(configFile, expectedOverlay))) {
9261
9869
  throw new Error(`Container '${container.id}' was not started with ${DEVCONTAINER_OVERLAY}.`);
9262
9870
  }
@@ -9330,7 +9938,7 @@ async function waitForManagedServices(plan, repoPath, composeProject, expectedSe
9330
9938
  } catch (error) {
9331
9939
  lastError = error;
9332
9940
  }
9333
- if (Date.now() < deadline) await sleep(POLL_INTERVAL_MS);
9941
+ if (Date.now() < deadline) await sleep2(POLL_INTERVAL_MS);
9334
9942
  } while (Date.now() < deadline);
9335
9943
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
9336
9944
  }
@@ -9439,7 +10047,7 @@ function hasExactManagedComposeProject(repoPath, state) {
9439
10047
  }).some((container) => {
9440
10048
  const workingDir = container.labels["com.docker.compose.project.working_dir"];
9441
10049
  return Boolean(
9442
- workingDir && sameWorkspacePath(workingDir, import_node_path24.default.join(repoPath, ".devcontainer"))
10050
+ workingDir && sameWorkspacePath(workingDir, import_node_path26.default.join(repoPath, ".devcontainer"))
9443
10051
  );
9444
10052
  });
9445
10053
  } catch {
@@ -9533,7 +10141,7 @@ function validateWorkspaceContainers(containers, options) {
9533
10141
  }
9534
10142
  return { id: appContainer.id, workspacePath: repoMount.Destination };
9535
10143
  }
9536
- function sleep(ms) {
10144
+ function sleep2(ms) {
9537
10145
  return new Promise((resolve) => setTimeout(resolve, ms));
9538
10146
  }
9539
10147
  async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeoutMs) {
@@ -9547,7 +10155,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
9547
10155
  target
9548
10156
  });
9549
10157
  if (target.kind === "linked") {
9550
- const workspaceEnv = (0, import_node_child_process19.spawnSync)(
10158
+ const workspaceEnv = (0, import_node_child_process21.spawnSync)(
9551
10159
  "docker",
9552
10160
  ["exec", appContainer.id, "printenv", "WORKSPACE"],
9553
10161
  { encoding: "utf-8" }
@@ -9557,7 +10165,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
9557
10165
  `Workspace app container must expose WORKSPACE='${target.workspace}' (got '${workspaceEnv.stdout.trim() || "(empty)"}').`
9558
10166
  );
9559
10167
  }
9560
- const devrouterWorkspaceEnv = (0, import_node_child_process19.spawnSync)(
10168
+ const devrouterWorkspaceEnv = (0, import_node_child_process21.spawnSync)(
9561
10169
  "docker",
9562
10170
  ["exec", appContainer.id, "printenv", "DEVROUTER_WORKSPACE"],
9563
10171
  { encoding: "utf-8" }
@@ -9568,7 +10176,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
9568
10176
  );
9569
10177
  }
9570
10178
  }
9571
- const gitCheck = (0, import_node_child_process19.spawnSync)(
10179
+ const gitCheck = (0, import_node_child_process21.spawnSync)(
9572
10180
  "docker",
9573
10181
  [
9574
10182
  "exec",
@@ -9589,7 +10197,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
9589
10197
  lastError = error;
9590
10198
  }
9591
10199
  if (Date.now() < deadline) {
9592
- await sleep(POLL_INTERVAL_MS);
10200
+ await sleep2(POLL_INTERVAL_MS);
9593
10201
  }
9594
10202
  } while (Date.now() < deadline);
9595
10203
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
@@ -9614,7 +10222,7 @@ async function waitForHttpRoutes(repoPath, apps, timeoutMs) {
9614
10222
  return;
9615
10223
  }
9616
10224
  if (Date.now() < deadline) {
9617
- await sleep(POLL_INTERVAL_MS);
10225
+ await sleep2(POLL_INTERVAL_MS);
9618
10226
  }
9619
10227
  } while (Date.now() < deadline);
9620
10228
  throw new Error(
@@ -9660,14 +10268,14 @@ function resolvePrimaryTarget(repoPath) {
9660
10268
  }
9661
10269
  function isPrimaryCheckout(repoPath) {
9662
10270
  try {
9663
- return import_node_fs23.default.statSync(import_node_path24.default.join(repoPath, ".git")).isDirectory();
10271
+ return import_node_fs25.default.statSync(import_node_path26.default.join(repoPath, ".git")).isDirectory();
9664
10272
  } catch {
9665
10273
  return false;
9666
10274
  }
9667
10275
  }
9668
10276
  function openUrls(urls) {
9669
10277
  for (const url of urls) {
9670
- const opened = (0, import_node_child_process19.spawnSync)("open", [url], { encoding: "utf-8" });
10278
+ const opened = (0, import_node_child_process21.spawnSync)("open", [url], { encoding: "utf-8" });
9671
10279
  if (opened.status !== 0) {
9672
10280
  process.stderr.write(`Warning: could not open '${url}'.
9673
10281
  `);
@@ -9706,12 +10314,16 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
9706
10314
  let managedConfigWritten = false;
9707
10315
  let managedWorkspaceEnv;
9708
10316
  let firstTransitionBaseline;
10317
+ const routeLoadOptions = {
10318
+ initialTimeoutMs: Math.min(options.httpTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS, 3e3),
10319
+ recoveryTimeoutMs: Math.min(options.httpTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS, 1e4)
10320
+ };
9709
10321
  try {
9710
10322
  const target = linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
9711
10323
  let devpodId = target.devpodId;
9712
10324
  if (target.kind === "linked") {
9713
- const overlayPath = import_node_path24.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
9714
- if (!import_node_fs23.default.existsSync(overlayPath)) {
10325
+ const overlayPath = import_node_path26.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
10326
+ if (!import_node_fs25.default.existsSync(overlayPath)) {
9715
10327
  throw new Error(`Missing required DevPod compose overlay: ${overlayPath}`);
9716
10328
  }
9717
10329
  }
@@ -9781,7 +10393,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
9781
10393
  }
9782
10394
  const apps = proxyAppsFromConfig(runtime.config);
9783
10395
  const parsedUpstreams = apps.map((app) => parseUpstream(app.upstream));
9784
- const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path24.default.basename(repoPath)) ?? "app";
10396
+ const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path26.default.basename(repoPath)) ?? "app";
9785
10397
  for (const [index, app] of apps.entries()) {
9786
10398
  if (!parsedUpstreams[index].host.startsWith(`${aliasPrefix}-`)) {
9787
10399
  const owner = target.kind === "linked" ? "workspace" : "checkout";
@@ -9945,6 +10557,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
9945
10557
  target.workspace
9946
10558
  );
9947
10559
  candidateRoutesPublished = true;
10560
+ await ensureTraefikRoutesLoaded(publication.routes, routeLoadOptions);
9948
10561
  try {
9949
10562
  await waitForHttpRoutes(
9950
10563
  repoPath,
@@ -9968,6 +10581,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
9968
10581
  });
9969
10582
  recreated = true;
9970
10583
  replaceHostRoutesForRepo(repoPath, publication.routes);
10584
+ await ensureTraefikRoutesLoaded(publication.routes, routeLoadOptions);
9971
10585
  await waitForHttpRoutes(
9972
10586
  repoPath,
9973
10587
  apps,
@@ -10141,7 +10755,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
10141
10755
  }
10142
10756
  if (candidateRoutesPublished) {
10143
10757
  try {
10144
- replaceHostRoutesForRepo(repoPath, previousRoutes.map(routeInputFromState));
10758
+ const rollbackRoutes = previousRoutes.map(routeInputFromState);
10759
+ replaceHostRoutesForRepo(repoPath, rollbackRoutes);
10760
+ await ensureTraefikRoutesLoaded(rollbackRoutes, routeLoadOptions);
10145
10761
  } catch (rollbackError) {
10146
10762
  rollbackErrors.push(
10147
10763
  `routes: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
@@ -10184,7 +10800,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
10184
10800
  }
10185
10801
  if (candidateRoutesPublished) {
10186
10802
  try {
10187
- replaceHostRoutesForRepo(repoPath, previousRoutes.map(routeInputFromState));
10803
+ const rollbackRoutes = previousRoutes.map(routeInputFromState);
10804
+ replaceHostRoutesForRepo(repoPath, rollbackRoutes);
10805
+ await ensureTraefikRoutesLoaded(rollbackRoutes, routeLoadOptions);
10188
10806
  } catch (rollbackError) {
10189
10807
  rollbackErrors.push(
10190
10808
  `routes: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
@@ -10213,13 +10831,13 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
10213
10831
  }
10214
10832
  });
10215
10833
  }
10216
- var import_node_child_process19, import_node_fs23, import_node_path24, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
10834
+ var import_node_child_process21, import_node_fs25, import_node_path26, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
10217
10835
  var init_workspace_ensure = __esm({
10218
10836
  "src/core/workspace-ensure.ts"() {
10219
10837
  "use strict";
10220
- import_node_child_process19 = require("child_process");
10221
- import_node_fs23 = __toESM(require("fs"));
10222
- import_node_path24 = __toESM(require("path"));
10838
+ import_node_child_process21 = require("child_process");
10839
+ import_node_fs25 = __toESM(require("fs"));
10840
+ import_node_path26 = __toESM(require("path"));
10223
10841
  init_devcontainer_profile();
10224
10842
  init_devpod_environment();
10225
10843
  init_devpod_mutation();
@@ -10232,6 +10850,7 @@ var init_workspace_ensure = __esm({
10232
10850
  init_repo_config();
10233
10851
  init_route_publication();
10234
10852
  init_router();
10853
+ init_traefik_route_health();
10235
10854
  init_workspace();
10236
10855
  init_workspace_ownership();
10237
10856
  init_workspace_runtime();
@@ -10305,20 +10924,20 @@ function warnMissingWorkspaceOwnership(repoPath) {
10305
10924
  );
10306
10925
  }
10307
10926
  function defaultWorktreePath(mainRepo, ws) {
10308
- return import_node_path25.default.join(mainRepo, "trees", ws);
10927
+ return import_node_path27.default.join(mainRepo, "trees", ws);
10309
10928
  }
10310
10929
  function assertDefaultWorktreeRootIgnored(mainRepo) {
10311
- const ignored = (0, import_node_child_process20.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
10930
+ const ignored = (0, import_node_child_process22.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
10312
10931
  encoding: "utf-8"
10313
10932
  });
10314
10933
  if (ignored.status !== 0) {
10315
10934
  throw new Error(
10316
- `Default worktree root '${import_node_path25.default.join(mainRepo, "trees")}' is not ignored. Add 'trees/' to '${import_node_path25.default.join(mainRepo, ".gitignore")}' or use --path.`
10935
+ `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.`
10317
10936
  );
10318
10937
  }
10319
10938
  }
10320
10939
  function legacyDefaultWorktreePath(mainRepo, ws) {
10321
- return import_node_path25.default.join(import_node_path25.default.dirname(mainRepo), `${import_node_path25.default.basename(mainRepo)}-${ws}`);
10940
+ return import_node_path27.default.join(import_node_path27.default.dirname(mainRepo), `${import_node_path27.default.basename(mainRepo)}-${ws}`);
10322
10941
  }
10323
10942
  function teardownFallbackPath(mainRepo, workspace) {
10324
10943
  const candidates = [
@@ -10430,13 +11049,13 @@ function assertFullDownPreflight(mainRepo, target) {
10430
11049
  if (sameWorkspacePath(target.worktreePath, mainRepo)) {
10431
11050
  throw new Error("Refusing to remove the primary Git checkout.");
10432
11051
  }
10433
- if (!target.worktree || target.worktree.prunable || !import_node_fs24.default.existsSync(target.worktreePath)) return;
11052
+ if (!target.worktree || target.worktree.prunable || !import_node_fs26.default.existsSync(target.worktreePath)) return;
10434
11053
  if (target.worktree.locked) {
10435
11054
  throw new Error(
10436
11055
  `Worktree '${target.worktreePath}' is locked; unlock it before workspace down.`
10437
11056
  );
10438
11057
  }
10439
- const status = (0, import_node_child_process20.spawnSync)(
11058
+ const status = (0, import_node_child_process22.spawnSync)(
10440
11059
  "git",
10441
11060
  ["-C", target.worktreePath, "status", "--porcelain", "--untracked-files=normal"],
10442
11061
  { encoding: "utf-8" }
@@ -10470,9 +11089,9 @@ async function workspaceUp(branch, opts = {}) {
10470
11089
  (candidate) => defaultWorktreePath(mainRepo, candidate)
10471
11090
  );
10472
11091
  const generatedPath = candidatePaths.find(
10473
- (candidate) => !import_node_fs24.default.existsSync(candidate) && !worktrees.some((worktree) => sameWorkspacePath(worktree.path, candidate))
11092
+ (candidate) => !import_node_fs26.default.existsSync(candidate) && !worktrees.some((worktree) => sameWorkspacePath(worktree.path, candidate))
10474
11093
  );
10475
- const selectedPath = opts.path ? import_node_path25.default.resolve(opts.path) : existingBranch?.path ?? generatedPath;
11094
+ const selectedPath = opts.path ? import_node_path27.default.resolve(opts.path) : existingBranch?.path ?? generatedPath;
10476
11095
  if (!selectedPath) {
10477
11096
  throw new Error(`Could not allocate a collision-safe worktree path for branch '${branch}'.`);
10478
11097
  }
@@ -10481,7 +11100,7 @@ async function workspaceUp(branch, opts = {}) {
10481
11100
  `Branch '${branch}' already uses worktree '${existingBranch.path}', not '${selectedPath}'.`
10482
11101
  );
10483
11102
  }
10484
- if (import_node_fs24.default.existsSync(selectedPath)) {
11103
+ if (import_node_fs26.default.existsSync(selectedPath)) {
10485
11104
  const registered = worktrees.find(
10486
11105
  (worktree) => sameWorkspacePath(worktree.path, selectedPath)
10487
11106
  );
@@ -10500,11 +11119,11 @@ async function workspaceUp(branch, opts = {}) {
10500
11119
  return selectedPath;
10501
11120
  }
10502
11121
  if (!opts.path) assertDefaultWorktreeRootIgnored(mainRepo);
10503
- const add = (0, import_node_child_process20.spawnSync)("git", ["-C", mainRepo, "worktree", "add", selectedPath, branch], {
11122
+ const add = (0, import_node_child_process22.spawnSync)("git", ["-C", mainRepo, "worktree", "add", selectedPath, branch], {
10504
11123
  encoding: "utf-8"
10505
11124
  });
10506
11125
  if (add.status !== 0) {
10507
- const addNew = (0, import_node_child_process20.spawnSync)(
11126
+ const addNew = (0, import_node_child_process22.spawnSync)(
10508
11127
  "git",
10509
11128
  ["-C", mainRepo, "worktree", "add", "-b", branch, selectedPath],
10510
11129
  { encoding: "utf-8" }
@@ -10606,8 +11225,8 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
10606
11225
  opts.quiet
10607
11226
  );
10608
11227
  if (removeWorktree) {
10609
- if (resolved.worktree && !resolved.worktree.prunable && import_node_fs24.default.existsSync(resolved.worktreePath)) {
10610
- const rm = (0, import_node_child_process20.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
11228
+ if (resolved.worktree && !resolved.worktree.prunable && import_node_fs26.default.existsSync(resolved.worktreePath)) {
11229
+ const rm = (0, import_node_child_process22.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
10611
11230
  encoding: "utf-8"
10612
11231
  });
10613
11232
  if (rm.status !== 0) {
@@ -10625,7 +11244,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
10625
11244
  }
10626
11245
  return result;
10627
11246
  };
10628
- return resolved.worktree && !resolved.worktree.prunable && import_node_fs24.default.existsSync(resolved.worktreePath) ? withWorkspaceLifecycleLock(resolved.worktreePath, operation) : operation();
11247
+ return resolved.worktree && !resolved.worktree.prunable && import_node_fs26.default.existsSync(resolved.worktreePath) ? withWorkspaceLifecycleLock(resolved.worktreePath, operation) : operation();
10629
11248
  }
10630
11249
  async function mutateWorkspaceOwnedPath(action2, worktreePath, opts = {}) {
10631
11250
  const mainRepo = resolveRepoPath(opts.repoPath);
@@ -10660,13 +11279,13 @@ async function workspaceStop(target, opts = {}) {
10660
11279
  async function workspaceDown(target, opts = {}) {
10661
11280
  return runWorkspaceLifecycle("down", target, opts);
10662
11281
  }
10663
- var import_node_child_process20, import_node_fs24, import_node_path25, WORKSPACE_ALLOCATION_LOCK_WAIT_MS;
11282
+ var import_node_child_process22, import_node_fs26, import_node_path27, WORKSPACE_ALLOCATION_LOCK_WAIT_MS;
10664
11283
  var init_workspace_lifecycle = __esm({
10665
11284
  "src/core/workspace-lifecycle.ts"() {
10666
11285
  "use strict";
10667
- import_node_child_process20 = require("child_process");
10668
- import_node_fs24 = __toESM(require("fs"));
10669
- import_node_path25 = __toESM(require("path"));
11286
+ import_node_child_process22 = require("child_process");
11287
+ import_node_fs26 = __toESM(require("fs"));
11288
+ import_node_path27 = __toESM(require("path"));
10670
11289
  init_devpod_mutation();
10671
11290
  init_devpod_workspaces();
10672
11291
  init_repo_config();
@@ -10795,17 +11414,17 @@ async function devsyExec(repoPath, command) {
10795
11414
  }
10796
11415
  const args = ["workspace", "exec", "--result-format", "plain", workspace.id, "--", ...command];
10797
11416
  return new Promise((resolve, reject) => {
10798
- const child = (0, import_node_child_process21.spawn)("devsy", args, { stdio: "inherit" });
11417
+ const child = (0, import_node_child_process23.spawn)("devsy", args, { stdio: "inherit" });
10799
11418
  child.once("error", (error) => reject(new Error(`devsy exec failed: ${error.message}`)));
10800
11419
  child.once("close", (code) => resolve(code ?? 1));
10801
11420
  });
10802
11421
  });
10803
11422
  }
10804
- var import_node_child_process21;
11423
+ var import_node_child_process23;
10805
11424
  var init_devsy_exec = __esm({
10806
11425
  "src/core/devsy-exec.ts"() {
10807
11426
  "use strict";
10808
- import_node_child_process21 = require("child_process");
11427
+ import_node_child_process23 = require("child_process");
10809
11428
  init_devsy_workspaces();
10810
11429
  init_workspace();
10811
11430
  }
@@ -10827,7 +11446,7 @@ function ensureGuidance2(repoPath) {
10827
11446
  return `Run 'devrouter ensure ${repoPath}' first.`;
10828
11447
  }
10829
11448
  function assertRunningDevpod(devpodId, repoPath) {
10830
- const result = (0, import_node_child_process22.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
11449
+ const result = (0, import_node_child_process24.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
10831
11450
  encoding: "utf-8"
10832
11451
  });
10833
11452
  if (result.status !== 0) {
@@ -10868,7 +11487,7 @@ async function devpodExec(repoPath, command) {
10868
11487
  }
10869
11488
  assertRunningDevpod(devpod.id, repoPath);
10870
11489
  const workspaceDirectory = resolveWorkspaceDirectory(repoPath);
10871
- const statusMarker = `__DEVROUTER_EXIT_${(0, import_node_crypto8.randomUUID)()}__:`;
11490
+ const statusMarker = `__DEVROUTER_EXIT_${(0, import_node_crypto9.randomUUID)()}__:`;
10872
11491
  const statusMarkerBytes = Buffer.from(statusMarker, "ascii");
10873
11492
  const literalCommand = command.map(quotePosixArg).join(" ");
10874
11493
  const wrappedCommand = `${literalCommand}; __devrouter_status=$?; printf '${statusMarker}%s\\n' "$__devrouter_status" >&2; exit 0`;
@@ -10886,7 +11505,7 @@ async function devpodExec(repoPath, command) {
10886
11505
  wrappedCommand
10887
11506
  ];
10888
11507
  return new Promise((resolve, reject) => {
10889
- const child = (0, import_node_child_process22.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
11508
+ const child = (0, import_node_child_process24.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
10890
11509
  let pending = Buffer.alloc(0);
10891
11510
  let statusBytes = Buffer.alloc(0);
10892
11511
  let readingStatus = false;
@@ -10945,12 +11564,12 @@ async function devpodExec(repoPath, command) {
10945
11564
  });
10946
11565
  });
10947
11566
  }
10948
- var import_node_child_process22, import_node_crypto8, DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC;
11567
+ var import_node_child_process24, import_node_crypto9, DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC;
10949
11568
  var init_devpod_exec = __esm({
10950
11569
  "src/core/devpod-exec.ts"() {
10951
11570
  "use strict";
10952
- import_node_child_process22 = require("child_process");
10953
- import_node_crypto8 = require("crypto");
11571
+ import_node_child_process24 = require("child_process");
11572
+ import_node_crypto9 = require("crypto");
10954
11573
  init_devpod_environment();
10955
11574
  init_devpod_workspaces();
10956
11575
  init_devsy_exec();
@@ -11136,7 +11755,7 @@ async function runOpenCommand(name) {
11136
11755
  );
11137
11756
  return;
11138
11757
  }
11139
- const result = (0, import_node_child_process23.spawnSync)("open", [url], { encoding: "utf-8" });
11758
+ const result = (0, import_node_child_process25.spawnSync)("open", [url], { encoding: "utf-8" });
11140
11759
  if (result.status !== 0) {
11141
11760
  const details = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
11142
11761
  throw new Error(`Unable to open '${url}': ${details || "unknown error"}`);
@@ -11144,11 +11763,11 @@ async function runOpenCommand(name) {
11144
11763
  process.stdout.write(`Opened ${url}
11145
11764
  `);
11146
11765
  }
11147
- var import_node_child_process23;
11766
+ var import_node_child_process25;
11148
11767
  var init_open = __esm({
11149
11768
  "src/commands/open.ts"() {
11150
11769
  "use strict";
11151
- import_node_child_process23 = require("child_process");
11770
+ import_node_child_process25 = require("child_process");
11152
11771
  init_docker();
11153
11772
  init_host_routes();
11154
11773
  init_repo_config();
@@ -11171,7 +11790,7 @@ async function runLogsCommand(options) {
11171
11790
  const args = ["logs", "--tail", tail, ROUTER_CONTAINER_NAME];
11172
11791
  if (options.follow) {
11173
11792
  args.splice(1, 0, "-f");
11174
- const child = (0, import_node_child_process24.spawn)("docker", args, { stdio: "inherit" });
11793
+ const child = (0, import_node_child_process26.spawn)("docker", args, { stdio: "inherit" });
11175
11794
  const onSignal = () => {
11176
11795
  child.kill("SIGTERM");
11177
11796
  };
@@ -11185,17 +11804,17 @@ async function runLogsCommand(options) {
11185
11804
  });
11186
11805
  });
11187
11806
  } else {
11188
- const result = (0, import_node_child_process24.spawnSync)("docker", args, { stdio: "inherit" });
11807
+ const result = (0, import_node_child_process26.spawnSync)("docker", args, { stdio: "inherit" });
11189
11808
  if (result.status !== 0) {
11190
11809
  throw new Error("Failed to retrieve router logs.");
11191
11810
  }
11192
11811
  }
11193
11812
  }
11194
- var import_node_child_process24;
11813
+ var import_node_child_process26;
11195
11814
  var init_logs = __esm({
11196
11815
  "src/commands/logs.ts"() {
11197
11816
  "use strict";
11198
- import_node_child_process24 = require("child_process");
11817
+ import_node_child_process26 = require("child_process");
11199
11818
  init_docker();
11200
11819
  init_router();
11201
11820
  }
@@ -11235,17 +11854,17 @@ function asRecord3(value) {
11235
11854
  return value;
11236
11855
  }
11237
11856
  function readJson(filePath) {
11238
- if (!import_node_fs25.default.existsSync(filePath)) {
11857
+ if (!import_node_fs27.default.existsSync(filePath)) {
11239
11858
  return void 0;
11240
11859
  }
11241
11860
  try {
11242
- return JSON.parse(import_node_fs25.default.readFileSync(filePath, "utf-8"));
11861
+ return JSON.parse(import_node_fs27.default.readFileSync(filePath, "utf-8"));
11243
11862
  } catch {
11244
11863
  return void 0;
11245
11864
  }
11246
11865
  }
11247
11866
  function relative(repoPath, filePath) {
11248
- return import_node_path26.default.relative(repoPath, filePath) || ".";
11867
+ return import_node_path28.default.relative(repoPath, filePath) || ".";
11249
11868
  }
11250
11869
  function redactEnvAssignments(value) {
11251
11870
  return value.replace(
@@ -11286,7 +11905,7 @@ function inspectPackageManager(repoPath, pkg) {
11286
11905
  ["bun.lock", "bun"]
11287
11906
  ];
11288
11907
  for (const [fileName, name] of lockfiles) {
11289
- if (import_node_fs25.default.existsSync(import_node_path26.default.join(repoPath, fileName))) {
11908
+ if (import_node_fs27.default.existsSync(import_node_path28.default.join(repoPath, fileName))) {
11290
11909
  return { name, source: fileName };
11291
11910
  }
11292
11911
  }
@@ -11301,9 +11920,9 @@ function inspectNode(repoPath, pkg) {
11301
11920
  if (typeof engines?.node === "string") {
11302
11921
  return { version: engines.node, source: "package.json:engines.node" };
11303
11922
  }
11304
- const nvmrc = import_node_path26.default.join(repoPath, ".nvmrc");
11305
- if (import_node_fs25.default.existsSync(nvmrc)) {
11306
- const version = import_node_fs25.default.readFileSync(nvmrc, "utf-8").trim();
11923
+ const nvmrc = import_node_path28.default.join(repoPath, ".nvmrc");
11924
+ if (import_node_fs27.default.existsSync(nvmrc)) {
11925
+ const version = import_node_fs27.default.readFileSync(nvmrc, "utf-8").trim();
11307
11926
  return { version, source: ".nvmrc" };
11308
11927
  }
11309
11928
  return void 0;
@@ -11364,7 +11983,7 @@ function configuredComposeFiles(repoPath) {
11364
11983
  const files = config.apps.filter(
11365
11984
  (app) => app.runtime === "docker"
11366
11985
  ).flatMap((app) => app.docker.composeFiles).filter(
11367
- (fileName) => !import_node_path26.default.isAbsolute(fileName) && !import_node_path26.default.normalize(fileName).startsWith("..")
11986
+ (fileName) => !import_node_path28.default.isAbsolute(fileName) && !import_node_path28.default.normalize(fileName).startsWith("..")
11368
11987
  );
11369
11988
  return Array.from(new Set(files));
11370
11989
  } catch {
@@ -11382,7 +12001,7 @@ function composeFiles(repoPath) {
11382
12001
  ...configuredComposeFiles(repoPath)
11383
12002
  ];
11384
12003
  return Array.from(new Set(candidates)).filter(
11385
- (fileName) => import_node_fs25.default.existsSync(import_node_path26.default.join(repoPath, fileName))
12004
+ (fileName) => import_node_fs27.default.existsSync(import_node_path28.default.join(repoPath, fileName))
11386
12005
  );
11387
12006
  }
11388
12007
  function stringArray2(value) {
@@ -11420,7 +12039,7 @@ function inspectServices(repoPath) {
11420
12039
  const services = [];
11421
12040
  for (const fileName of composeFiles(repoPath)) {
11422
12041
  try {
11423
- const parsed = import_yaml6.default.parse(import_node_fs25.default.readFileSync(import_node_path26.default.join(repoPath, fileName), "utf-8"));
12042
+ const parsed = import_yaml6.default.parse(import_node_fs27.default.readFileSync(import_node_path28.default.join(repoPath, fileName), "utf-8"));
11424
12043
  const serviceMap = asRecord3(asRecord3(parsed)?.services);
11425
12044
  for (const [name, value] of Object.entries(serviceMap ?? {})) {
11426
12045
  const service = asRecord3(value);
@@ -11455,8 +12074,8 @@ function inspectServices(repoPath) {
11455
12074
  return services;
11456
12075
  }
11457
12076
  function inspectEnvFiles(repoPath) {
11458
- const files = import_node_fs25.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
11459
- const content = import_node_fs25.default.readFileSync(import_node_path26.default.join(repoPath, fileName), "utf-8");
12077
+ const files = import_node_fs27.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
12078
+ const content = import_node_fs27.default.readFileSync(import_node_path28.default.join(repoPath, fileName), "utf-8");
11460
12079
  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();
11461
12080
  return { path: fileName, names };
11462
12081
  });
@@ -11470,18 +12089,18 @@ function inspectEnvFiles(repoPath) {
11470
12089
  };
11471
12090
  }
11472
12091
  function inspectDevcontainer(repoPath) {
11473
- const dir = import_node_path26.default.join(repoPath, ".devcontainer");
11474
- if (!import_node_fs25.default.existsSync(dir)) {
12092
+ const dir = import_node_path28.default.join(repoPath, ".devcontainer");
12093
+ if (!import_node_fs27.default.existsSync(dir)) {
11475
12094
  return { exists: false, files: [] };
11476
12095
  }
11477
12096
  return {
11478
12097
  exists: true,
11479
- files: import_node_fs25.default.readdirSync(dir).filter((fileName) => import_node_fs25.default.statSync(import_node_path26.default.join(dir, fileName)).isFile()).sort().map((fileName) => `.devcontainer/${fileName}`)
12098
+ 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}`)
11480
12099
  };
11481
12100
  }
11482
12101
  function inspectDevrouter(repoPath) {
11483
12102
  const configPath = getRepoConfigPath(repoPath);
11484
- if (!import_node_fs25.default.existsSync(configPath)) {
12103
+ if (!import_node_fs27.default.existsSync(configPath)) {
11485
12104
  return {
11486
12105
  exists: false,
11487
12106
  configPath,
@@ -11525,15 +12144,15 @@ function inspectAgentGuidance(repoPath) {
11525
12144
  ["AGENTS.md", "agents"],
11526
12145
  ["CLAUDE.md", "claude"]
11527
12146
  ]) {
11528
- if (import_node_fs25.default.existsSync(import_node_path26.default.join(repoPath, fileName))) {
12147
+ if (import_node_fs27.default.existsSync(import_node_path28.default.join(repoPath, fileName))) {
11529
12148
  results.push({ path: fileName, kind });
11530
12149
  }
11531
12150
  }
11532
- const skillsDir = import_node_path26.default.join(repoPath, ".agents", "skills");
11533
- if (import_node_fs25.default.existsSync(skillsDir)) {
11534
- for (const name of import_node_fs25.default.readdirSync(skillsDir).sort()) {
11535
- const skillPath = import_node_path26.default.join(skillsDir, name, "SKILL.md");
11536
- if (import_node_fs25.default.existsSync(skillPath)) {
12151
+ const skillsDir = import_node_path28.default.join(repoPath, ".agents", "skills");
12152
+ if (import_node_fs27.default.existsSync(skillsDir)) {
12153
+ for (const name of import_node_fs27.default.readdirSync(skillsDir).sort()) {
12154
+ const skillPath = import_node_path28.default.join(skillsDir, name, "SKILL.md");
12155
+ if (import_node_fs27.default.existsSync(skillPath)) {
11537
12156
  results.push({ path: relative(repoPath, skillPath), kind: "skill" });
11538
12157
  }
11539
12158
  }
@@ -11576,7 +12195,7 @@ function buildIssues(report) {
11576
12195
  }
11577
12196
  function inspectRepo(options = {}) {
11578
12197
  const repoPath = resolveRepoPath(options.repo);
11579
- const pkg = readJson(import_node_path26.default.join(repoPath, "package.json"));
12198
+ const pkg = readJson(import_node_path28.default.join(repoPath, "package.json"));
11580
12199
  const scripts = inspectScripts(pkg);
11581
12200
  const reportWithoutIssues = {
11582
12201
  repoPath,
@@ -11595,12 +12214,12 @@ function inspectRepo(options = {}) {
11595
12214
  issues: buildIssues(reportWithoutIssues)
11596
12215
  };
11597
12216
  }
11598
- var import_node_fs25, import_node_path26, import_yaml6;
12217
+ var import_node_fs27, import_node_path28, import_yaml6;
11599
12218
  var init_repo_inspect = __esm({
11600
12219
  "src/core/repo-inspect.ts"() {
11601
12220
  "use strict";
11602
- import_node_fs25 = __toESM(require("fs"));
11603
- import_node_path26 = __toESM(require("path"));
12221
+ import_node_fs27 = __toESM(require("fs"));
12222
+ import_node_path28 = __toESM(require("path"));
11604
12223
  import_yaml6 = __toESM(require("yaml"));
11605
12224
  init_repo_config();
11606
12225
  }
@@ -11705,7 +12324,7 @@ function requiredFileChecks(repoPath) {
11705
12324
  ".devcontainer/docker-compose.yml",
11706
12325
  ".devrouter.yml"
11707
12326
  ];
11708
- const missing = required.filter((fileName) => !import_node_fs26.default.existsSync(import_node_path27.default.join(repoPath, fileName)));
12327
+ const missing = required.filter((fileName) => !import_node_fs28.default.existsSync(import_node_path29.default.join(repoPath, fileName)));
11709
12328
  return {
11710
12329
  id: "repo.devcontainer.verify-files",
11711
12330
  level: missing.length === 0 ? "ok" : "error",
@@ -11924,12 +12543,12 @@ async function verifyDevcontainer(options = {}) {
11924
12543
  nextSteps: collectNextSteps3(checks)
11925
12544
  };
11926
12545
  }
11927
- var import_node_fs26, import_node_path27;
12546
+ var import_node_fs28, import_node_path29;
11928
12547
  var init_devcontainer_verify = __esm({
11929
12548
  "src/core/devcontainer-verify.ts"() {
11930
12549
  "use strict";
11931
- import_node_fs26 = __toESM(require("fs"));
11932
- import_node_path27 = __toESM(require("path"));
12550
+ import_node_fs28 = __toESM(require("fs"));
12551
+ import_node_path29 = __toESM(require("path"));
11933
12552
  init_capabilities();
11934
12553
  init_doctor();
11935
12554
  init_host_routes();
@@ -12226,7 +12845,7 @@ function packageManagerIssues(repo) {
12226
12845
  }
12227
12846
  function plannedFiles(repoPath, version) {
12228
12847
  const repo = inspectRepo({ repo: repoPath });
12229
- const projectName = sanitizeProjectName(import_node_path28.default.basename(repo.repoPath));
12848
+ const projectName = sanitizeProjectName(import_node_path30.default.basename(repo.repoPath));
12230
12849
  const nodeMajor = majorVersion(repo.node?.version, "24");
12231
12850
  const pnpmVersion = repo.packageManager?.name === "pnpm" && repo.packageManager.version ? repo.packageManager.version : DEFAULT_PNPM_VERSION;
12232
12851
  const port = inferPort2(repo);
@@ -12271,8 +12890,8 @@ function plannedFiles(repoPath, version) {
12271
12890
  };
12272
12891
  }
12273
12892
  function classifyFile(repoPath, file) {
12274
- const absolutePath = import_node_path28.default.join(repoPath, file.relativePath);
12275
- if (!import_node_fs27.default.existsSync(absolutePath)) {
12893
+ const absolutePath = import_node_path30.default.join(repoPath, file.relativePath);
12894
+ if (!import_node_fs29.default.existsSync(absolutePath)) {
12276
12895
  return {
12277
12896
  path: file.relativePath,
12278
12897
  action: "create",
@@ -12280,7 +12899,7 @@ function classifyFile(repoPath, file) {
12280
12899
  bytes: Buffer.byteLength(file.content)
12281
12900
  };
12282
12901
  }
12283
- const current = import_node_fs27.default.readFileSync(absolutePath, "utf-8");
12902
+ const current = import_node_fs29.default.readFileSync(absolutePath, "utf-8");
12284
12903
  if (!current.includes(MANAGED_MARKER2)) {
12285
12904
  return {
12286
12905
  path: file.relativePath,
@@ -12337,11 +12956,11 @@ function buildPlan(repoPath, dryRun, version) {
12337
12956
  };
12338
12957
  }
12339
12958
  function writeFile(repoPath, file) {
12340
- const absolutePath = import_node_path28.default.join(repoPath, file.relativePath);
12341
- import_node_fs27.default.mkdirSync(import_node_path28.default.dirname(absolutePath), { recursive: true });
12342
- import_node_fs27.default.writeFileSync(absolutePath, file.content, "utf-8");
12959
+ const absolutePath = import_node_path30.default.join(repoPath, file.relativePath);
12960
+ import_node_fs29.default.mkdirSync(import_node_path30.default.dirname(absolutePath), { recursive: true });
12961
+ import_node_fs29.default.writeFileSync(absolutePath, file.content, "utf-8");
12343
12962
  if (file.executable) {
12344
- import_node_fs27.default.chmodSync(absolutePath, 493);
12963
+ import_node_fs29.default.chmodSync(absolutePath, 493);
12345
12964
  }
12346
12965
  }
12347
12966
  function writeDevcontainer(options = {}) {
@@ -12379,12 +12998,12 @@ function writeDevcontainer(options = {}) {
12379
12998
  nextSteps: postWriteNextSteps(repoPath)
12380
12999
  };
12381
13000
  }
12382
- var import_node_fs27, import_node_path28, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
13001
+ var import_node_fs29, import_node_path30, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
12383
13002
  var init_devcontainer_write = __esm({
12384
13003
  "src/core/devcontainer-write.ts"() {
12385
13004
  "use strict";
12386
- import_node_fs27 = __toESM(require("fs"));
12387
- import_node_path28 = __toESM(require("path"));
13005
+ import_node_fs29 = __toESM(require("fs"));
13006
+ import_node_path30 = __toESM(require("path"));
12388
13007
  init_repo_config();
12389
13008
  init_repo_inspect();
12390
13009
  MANAGED_MARKER2 = "devrouter:managed devcontainer";
@@ -12493,6 +13112,119 @@ var init_repo_devcontainer = __esm({
12493
13112
  }
12494
13113
  });
12495
13114
 
13115
+ // src/core/profile-resolution.ts
13116
+ function sortedUnique2(values) {
13117
+ return Array.from(new Set(values)).sort();
13118
+ }
13119
+ function expandsAll(values) {
13120
+ return values?.length === 1 && values[0] === "*";
13121
+ }
13122
+ function selectedManagedResources(profile, selected, registry) {
13123
+ if (!profile || expandsAll(selected)) return sortedUnique2(registry);
13124
+ return sortedUnique2(selected ?? []);
13125
+ }
13126
+ function selectedReadiness(config, profileName, profile, routedApps) {
13127
+ const selectedNames = profileName.split(",");
13128
+ if (selectedNames.length === 1) {
13129
+ return sortedUnique2(
13130
+ profile?.readiness ?? routedApps.filter((app) => app.protocol === "http").map((app) => app.name)
13131
+ );
13132
+ }
13133
+ const readiness = /* @__PURE__ */ new Set();
13134
+ for (const selectedName of selectedNames) {
13135
+ const selected = resolveProfile(config, selectedName);
13136
+ const selectedApps = applyProfile(config, selected.profile).apps.filter(
13137
+ (app) => app.kind !== "dependency"
13138
+ );
13139
+ const selectedTargets = selected.profile?.readiness ?? selectedApps.filter((app) => app.protocol === "http").map((app) => app.name);
13140
+ for (const target of selectedTargets) readiness.add(target);
13141
+ }
13142
+ return sortedUnique2(readiness);
13143
+ }
13144
+ function buildProfileResolutionReport(config, repoPath, profileOverride) {
13145
+ const resolved = resolveProfile(config, profileOverride);
13146
+ const filtered = applyProfile(config, resolved.profile);
13147
+ const routedApps = filtered.apps.filter((app) => app.kind !== "dependency");
13148
+ const dependencies = filtered.apps.filter((app) => app.kind === "dependency");
13149
+ const readiness = selectedReadiness(config, resolved.name, resolved.profile, routedApps);
13150
+ const managedRuntime = config.managedRuntime;
13151
+ const baseServices = sortedUnique2(managedRuntime?.devcontainer.baseServices ?? []);
13152
+ const profileServices = managedRuntime ? selectedManagedResources(
13153
+ resolved.profile,
13154
+ resolved.profile?.devcontainerServices,
13155
+ managedRuntime.devcontainer.profileServices
13156
+ ) : [];
13157
+ const processes = managedRuntime ? selectedManagedResources(
13158
+ resolved.profile,
13159
+ resolved.profile?.processes,
13160
+ managedRuntime.processes
13161
+ ) : [];
13162
+ return {
13163
+ schemaVersion: 1,
13164
+ repoPath,
13165
+ profile: resolved.name,
13166
+ apps: sortedUnique2(routedApps.map((app) => app.name)),
13167
+ dependencies: sortedUnique2(dependencies.map((app) => app.name)),
13168
+ readiness: sortedUnique2(readiness),
13169
+ managedRuntime: {
13170
+ baseServices,
13171
+ profileServices,
13172
+ services: sortedUnique2([...baseServices, ...profileServices]),
13173
+ processes
13174
+ }
13175
+ };
13176
+ }
13177
+ function resolveProfileReport(options) {
13178
+ const repoPath = resolveRepoPath(options.repo);
13179
+ return buildProfileResolutionReport(loadRepoConfig(repoPath), repoPath, options.profile);
13180
+ }
13181
+ var init_profile_resolution = __esm({
13182
+ "src/core/profile-resolution.ts"() {
13183
+ "use strict";
13184
+ init_repo_config();
13185
+ }
13186
+ });
13187
+
13188
+ // src/commands/profile.ts
13189
+ var profile_exports = {};
13190
+ __export(profile_exports, {
13191
+ runProfileResolveCommand: () => runProfileResolveCommand
13192
+ });
13193
+ async function runProfileResolveCommand(options) {
13194
+ const report = resolveProfileReport(options);
13195
+ if (options.json) {
13196
+ printJSON(report);
13197
+ return;
13198
+ }
13199
+ printProfileResolutionSummary(report);
13200
+ }
13201
+ function renderValues(values) {
13202
+ return values.join(", ") || "-";
13203
+ }
13204
+ function printProfileResolutionSummary(report) {
13205
+ process.stdout.write(`Repo: ${report.repoPath}
13206
+ `);
13207
+ process.stdout.write(`Profile: ${report.profile}
13208
+ `);
13209
+ process.stdout.write(`Apps: ${renderValues(report.apps)}
13210
+ `);
13211
+ process.stdout.write(`Dependencies: ${renderValues(report.dependencies)}
13212
+ `);
13213
+ process.stdout.write(`Readiness: ${renderValues(report.readiness)}
13214
+ `);
13215
+ process.stdout.write(`Managed services: ${renderValues(report.managedRuntime.services)}
13216
+ `);
13217
+ process.stdout.write(`Managed processes: ${renderValues(report.managedRuntime.processes)}
13218
+ `);
13219
+ }
13220
+ var init_profile = __esm({
13221
+ "src/commands/profile.ts"() {
13222
+ "use strict";
13223
+ init_output();
13224
+ init_profile_resolution();
13225
+ }
13226
+ });
13227
+
12496
13228
  // src/commands/app-add.ts
12497
13229
  var app_add_exports = {};
12498
13230
  __export(app_add_exports, {
@@ -12775,7 +13507,7 @@ function sanitizeRouterId(value) {
12775
13507
  return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
12776
13508
  }
12777
13509
  function repoHash(repoPath) {
12778
- return (0, import_node_crypto9.createHash)("sha1").update(import_node_path29.default.resolve(repoPath)).digest("hex").slice(0, 12);
13510
+ return (0, import_node_crypto10.createHash)("sha1").update(import_node_path31.default.resolve(repoPath)).digest("hex").slice(0, 12);
12779
13511
  }
12780
13512
  function asDockerApp(app) {
12781
13513
  return app.runtime === "docker";
@@ -12854,11 +13586,11 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
12854
13586
  if (dockerApps.length === 0) {
12855
13587
  throw new Error("No docker apps selected to prepare compose overlay.");
12856
13588
  }
12857
- const cachePath = import_node_path29.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
12858
- import_node_fs28.default.mkdirSync(cachePath, { recursive: true });
12859
- const overlayPath = import_node_path29.default.join(cachePath, "compose.devrouter.yml");
13589
+ const cachePath = import_node_path31.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
13590
+ import_node_fs30.default.mkdirSync(cachePath, { recursive: true });
13591
+ const overlayPath = import_node_path31.default.join(cachePath, "compose.devrouter.yml");
12860
13592
  const overlayDocument = buildOverlayDocument(dockerApps, publishTcpPorts);
12861
- import_node_fs28.default.writeFileSync(overlayPath, import_yaml7.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
13593
+ import_node_fs30.default.writeFileSync(overlayPath, import_yaml7.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
12862
13594
  return {
12863
13595
  overlayPath,
12864
13596
  composeFiles: ensureComposeFiles(dockerApps),
@@ -12872,7 +13604,7 @@ function runDockerComposeUp(repoPath, composeFiles2, overlayPath, services) {
12872
13604
  fileArgs.push("-f", resolved);
12873
13605
  }
12874
13606
  const args = ["compose", ...fileArgs, "-f", overlayPath, "up", "-d", "--wait", ...services];
12875
- const result = (0, import_node_child_process25.spawnSync)("docker", args, {
13607
+ const result = (0, import_node_child_process27.spawnSync)("docker", args, {
12876
13608
  encoding: "utf-8",
12877
13609
  cwd: repoPath
12878
13610
  });
@@ -12900,7 +13632,7 @@ function queryRunningComposeServices(repoPath, composeFiles2, overlayPath, servi
12900
13632
  "--services",
12901
13633
  ...services
12902
13634
  ];
12903
- const result = (0, import_node_child_process25.spawnSync)("docker", args, {
13635
+ const result = (0, import_node_child_process27.spawnSync)("docker", args, {
12904
13636
  encoding: "utf-8",
12905
13637
  cwd: repoPath
12906
13638
  });
@@ -12926,7 +13658,7 @@ function runDockerComposeStop(repoPath, composeFiles2, overlayPath, services) {
12926
13658
  fileArgs.push("-f", resolved);
12927
13659
  }
12928
13660
  const args = ["compose", ...fileArgs, "-f", overlayPath, "stop", ...services];
12929
- const result = (0, import_node_child_process25.spawnSync)("docker", args, {
13661
+ const result = (0, import_node_child_process27.spawnSync)("docker", args, {
12930
13662
  encoding: "utf-8",
12931
13663
  cwd: repoPath
12932
13664
  });
@@ -12952,7 +13684,7 @@ function runDockerComposeLogs(repoPath, composeFiles2, overlayPath, services, ta
12952
13684
  String(tail),
12953
13685
  ...services
12954
13686
  ];
12955
- (0, import_node_child_process25.spawnSync)("docker", args, {
13687
+ (0, import_node_child_process27.spawnSync)("docker", args, {
12956
13688
  stdio: "inherit",
12957
13689
  cwd: repoPath
12958
13690
  });
@@ -12964,7 +13696,7 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
12964
13696
  fileArgs.push("-f", resolved);
12965
13697
  }
12966
13698
  const args = ["compose", ...fileArgs, "-f", overlayPath, "port", service, String(internalPort)];
12967
- const result = (0, import_node_child_process25.spawnSync)("docker", args, {
13699
+ const result = (0, import_node_child_process27.spawnSync)("docker", args, {
12968
13700
  encoding: "utf-8",
12969
13701
  cwd: repoPath
12970
13702
  });
@@ -12978,14 +13710,14 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
12978
13710
  const port = Number(match[1]);
12979
13711
  return Number.isInteger(port) && port > 0 ? port : void 0;
12980
13712
  }
12981
- var import_node_child_process25, import_node_crypto9, import_node_fs28, import_node_path29, import_yaml7;
13713
+ var import_node_child_process27, import_node_crypto10, import_node_fs30, import_node_path31, import_yaml7;
12982
13714
  var init_docker_run = __esm({
12983
13715
  "src/core/docker-run.ts"() {
12984
13716
  "use strict";
12985
- import_node_child_process25 = require("child_process");
12986
- import_node_crypto9 = require("crypto");
12987
- import_node_fs28 = __toESM(require("fs"));
12988
- import_node_path29 = __toESM(require("path"));
13717
+ import_node_child_process27 = require("child_process");
13718
+ import_node_crypto10 = require("crypto");
13719
+ import_node_fs30 = __toESM(require("fs"));
13720
+ import_node_path31 = __toESM(require("path"));
12989
13721
  import_yaml7 = __toESM(require("yaml"));
12990
13722
  init_docker_error_guidance();
12991
13723
  init_paths();
@@ -13022,7 +13754,7 @@ function resolveSmCommand(command, defaultEnv, overrideEnv) {
13022
13754
  }
13023
13755
  return command.replace(/\{env\}/g, env);
13024
13756
  }
13025
- function sleep2(ms) {
13757
+ function sleep3(ms) {
13026
13758
  return new Promise((resolve) => {
13027
13759
  setTimeout(resolve, ms);
13028
13760
  });
@@ -13075,7 +13807,7 @@ function isProcessRunning(pid) {
13075
13807
  }
13076
13808
  }
13077
13809
  function readProcessTree(rootPid) {
13078
- const result = (0, import_node_child_process26.spawnSync)("ps", ["-ax", "-o", "pid=,ppid="], { encoding: "utf-8" });
13810
+ const result = (0, import_node_child_process28.spawnSync)("ps", ["-ax", "-o", "pid=,ppid="], { encoding: "utf-8" });
13079
13811
  if (result.status !== 0) {
13080
13812
  return [rootPid];
13081
13813
  }
@@ -13133,7 +13865,7 @@ async function terminateProcessTree(rootPid) {
13133
13865
  if (!isProcessRunning(rootPid)) {
13134
13866
  return;
13135
13867
  }
13136
- await sleep2(100);
13868
+ await sleep3(100);
13137
13869
  }
13138
13870
  if (isProcessRunning(rootPid)) {
13139
13871
  killProcessTree(rootPid, "SIGKILL");
@@ -13186,12 +13918,12 @@ function detectListeningPorts(pids) {
13186
13918
  if (pids.length === 0) {
13187
13919
  return [];
13188
13920
  }
13189
- const result = (0, import_node_child_process26.spawnSync)("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", pids.join(",")], {
13921
+ const result = (0, import_node_child_process28.spawnSync)("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", pids.join(",")], {
13190
13922
  encoding: "utf-8"
13191
13923
  });
13192
13924
  if (result.error && result.error.code === "ENOENT") {
13193
13925
  if (process.platform === "linux") {
13194
- const ssResult = (0, import_node_child_process26.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
13926
+ const ssResult = (0, import_node_child_process28.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
13195
13927
  if (ssResult.status === 0 && ssResult.stdout) {
13196
13928
  return parseSsListeningPorts(ssResult.stdout, new Set(pids));
13197
13929
  }
@@ -13250,7 +13982,7 @@ async function runHostApp(repoPath, app, extraEnv = {}, secretManager, env, work
13250
13982
  app.hostRun.command,
13251
13983
  true
13252
13984
  ) : app.hostRun.command;
13253
- const child = (0, import_node_child_process26.spawn)(spawnCommand, {
13985
+ const child = (0, import_node_child_process28.spawn)(spawnCommand, {
13254
13986
  cwd: commandCwd,
13255
13987
  stdio: "inherit",
13256
13988
  shell: true,
@@ -13320,7 +14052,7 @@ async function runHostApp(repoPath, app, extraEnv = {}, secretManager, env, work
13320
14052
  );
13321
14053
  }
13322
14054
  }
13323
- await sleep2(POLL_INTERVAL_MS2);
14055
+ await sleep3(POLL_INTERVAL_MS2);
13324
14056
  }
13325
14057
  } catch (error) {
13326
14058
  fatalError = toError(error);
@@ -13618,7 +14350,7 @@ async function execWithAppEnv(options) {
13618
14350
  options.command[0],
13619
14351
  true
13620
14352
  );
13621
- child = (0, import_node_child_process26.spawn)(wrapped, {
14353
+ child = (0, import_node_child_process28.spawn)(wrapped, {
13622
14354
  cwd: deps.repoPath,
13623
14355
  stdio: "inherit",
13624
14356
  shell: true,
@@ -13632,7 +14364,7 @@ async function execWithAppEnv(options) {
13632
14364
  false
13633
14365
  );
13634
14366
  const [cmd, ...wrappedArgs] = wrapped;
13635
- child = (0, import_node_child_process26.spawn)(cmd, wrappedArgs, {
14367
+ child = (0, import_node_child_process28.spawn)(cmd, wrappedArgs, {
13636
14368
  cwd: deps.repoPath,
13637
14369
  stdio: "inherit",
13638
14370
  shell: false,
@@ -13640,7 +14372,7 @@ async function execWithAppEnv(options) {
13640
14372
  });
13641
14373
  }
13642
14374
  } else if (options.shell) {
13643
- child = (0, import_node_child_process26.spawn)(options.command[0], {
14375
+ child = (0, import_node_child_process28.spawn)(options.command[0], {
13644
14376
  cwd: deps.repoPath,
13645
14377
  stdio: "inherit",
13646
14378
  shell: true,
@@ -13648,7 +14380,7 @@ async function execWithAppEnv(options) {
13648
14380
  });
13649
14381
  } else {
13650
14382
  const [command, ...args] = options.command;
13651
- child = (0, import_node_child_process26.spawn)(command, args, {
14383
+ child = (0, import_node_child_process28.spawn)(command, args, {
13652
14384
  cwd: deps.repoPath,
13653
14385
  stdio: "inherit",
13654
14386
  shell: false,
@@ -13669,11 +14401,11 @@ async function execWithAppEnv(options) {
13669
14401
  deps.stopDeps();
13670
14402
  }
13671
14403
  }
13672
- var import_node_child_process26, import_node_net, import_node_process, import_promises, POLL_INTERVAL_MS2, DEFAULT_PORT_TIMEOUT_MS, PROCESS_TERMINATION_GRACE_MS;
14404
+ var import_node_child_process28, import_node_net, import_node_process, import_promises, POLL_INTERVAL_MS2, DEFAULT_PORT_TIMEOUT_MS, PROCESS_TERMINATION_GRACE_MS;
13673
14405
  var init_app_run = __esm({
13674
14406
  "src/core/app-run.ts"() {
13675
14407
  "use strict";
13676
- import_node_child_process26 = require("child_process");
14408
+ import_node_child_process28 = require("child_process");
13677
14409
  import_node_net = __toESM(require("net"));
13678
14410
  import_node_process = require("process");
13679
14411
  import_promises = require("readline/promises");
@@ -13807,13 +14539,13 @@ function measureWorktreeConsumption(worktreePath, options) {
13807
14539
  const startedAt = Date.now();
13808
14540
  let rootStat;
13809
14541
  try {
13810
- rootStat = import_node_fs29.default.lstatSync(worktreePath);
14542
+ rootStat = import_node_fs31.default.lstatSync(worktreePath);
13811
14543
  } catch (error) {
13812
14544
  return { status: "unknown", reason: describeError(error, worktreePath) };
13813
14545
  }
13814
14546
  let rootEntries;
13815
14547
  try {
13816
- rootEntries = import_node_fs29.default.readdirSync(worktreePath, { withFileTypes: true });
14548
+ rootEntries = import_node_fs31.default.readdirSync(worktreePath, { withFileTypes: true });
13817
14549
  } catch (error) {
13818
14550
  return { status: "unknown", reason: describeError(error, worktreePath) };
13819
14551
  }
@@ -13839,10 +14571,10 @@ function measureWorktreeConsumption(worktreePath, options) {
13839
14571
  timedOut = true;
13840
14572
  break;
13841
14573
  }
13842
- const entryPath = import_node_path30.default.join(dirPath, entry.name);
14574
+ const entryPath = import_node_path32.default.join(dirPath, entry.name);
13843
14575
  let entryStat;
13844
14576
  try {
13845
- entryStat = import_node_fs29.default.lstatSync(entryPath);
14577
+ entryStat = import_node_fs31.default.lstatSync(entryPath);
13846
14578
  } catch (error) {
13847
14579
  if (error?.code === "ENOENT") continue;
13848
14580
  unreadableReason = describeIncompleteWalk(error);
@@ -13852,7 +14584,7 @@ function measureWorktreeConsumption(worktreePath, options) {
13852
14584
  if (!entryStat.isDirectory()) continue;
13853
14585
  let childEntries;
13854
14586
  try {
13855
- childEntries = import_node_fs29.default.readdirSync(entryPath, { withFileTypes: true });
14587
+ childEntries = import_node_fs31.default.readdirSync(entryPath, { withFileTypes: true });
13856
14588
  } catch (error) {
13857
14589
  unreadableReason = describeIncompleteWalk(error);
13858
14590
  break;
@@ -13916,12 +14648,12 @@ function describeError(error, worktreePath) {
13916
14648
  }
13917
14649
  return `could not stat worktree path '${worktreePath}': ${error?.message ?? String(error)}`;
13918
14650
  }
13919
- var import_node_fs29, import_node_path30, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
14651
+ var import_node_fs31, import_node_path32, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
13920
14652
  var init_workspace_consumption = __esm({
13921
14653
  "src/core/workspace-consumption.ts"() {
13922
14654
  "use strict";
13923
- import_node_fs29 = __toESM(require("fs"));
13924
- import_node_path30 = __toESM(require("path"));
14655
+ import_node_fs31 = __toESM(require("fs"));
14656
+ import_node_path32 = __toESM(require("path"));
13925
14657
  init_devpod_environment();
13926
14658
  DEFAULT_DEADLINE_MS = 1e4;
13927
14659
  BLOCK_SIZE_BYTES = 512;
@@ -14022,7 +14754,7 @@ function evaluateWorkspaceActivity(evidence, cutoff) {
14022
14754
  }
14023
14755
  function readGitSnapshot(worktree, commandRunner) {
14024
14756
  const comparablePath = comparableWorkspacePath(worktree.path);
14025
- if (worktree.prunable || !import_node_fs30.default.existsSync(comparablePath)) {
14757
+ if (worktree.prunable || !import_node_fs32.default.existsSync(comparablePath)) {
14026
14758
  return { worktree, checkout: "missing", head: null, committerDate: null };
14027
14759
  }
14028
14760
  const head = gitOutput(comparablePath, ["rev-parse", "--verify", "HEAD"], commandRunner);
@@ -14547,7 +15279,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
14547
15279
  containers = measureContainersFn(worktreePaths);
14548
15280
  } catch (error) {
14549
15281
  const reason = `container measurement failed: ${describeCause(error)}`;
14550
- containers = new Map(worktreePaths.map((path30) => [path30, unknownContainers(reason)]));
15282
+ containers = new Map(worktreePaths.map((path32) => [path32, unknownContainers(reason)]));
14551
15283
  }
14552
15284
  }
14553
15285
  const rows = records.map((record) => {
@@ -14596,12 +15328,12 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
14596
15328
  workspaces: rows
14597
15329
  };
14598
15330
  }
14599
- var import_node_child_process27, import_node_fs30, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
15331
+ var import_node_child_process29, import_node_fs32, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
14600
15332
  var init_workspace_cleanup = __esm({
14601
15333
  "src/core/workspace-cleanup.ts"() {
14602
15334
  "use strict";
14603
- import_node_child_process27 = require("child_process");
14604
- import_node_fs30 = __toESM(require("fs"));
15335
+ import_node_child_process29 = require("child_process");
15336
+ import_node_fs32 = __toESM(require("fs"));
14605
15337
  init_devpod_workspaces();
14606
15338
  init_host_routes();
14607
15339
  init_repo_config();
@@ -14618,7 +15350,7 @@ var init_workspace_cleanup = __esm({
14618
15350
  ];
14619
15351
  defaultCommandRunner = (command, args, input2) => {
14620
15352
  const environment = command === "git" ? READ_ONLY_GIT_ENV2 : { ...process.env, LC_ALL: "C" };
14621
- const result = (0, import_node_child_process27.spawnSync)(command, args, {
15353
+ const result = (0, import_node_child_process29.spawnSync)(command, args, {
14622
15354
  encoding: "utf-8",
14623
15355
  env: environment,
14624
15356
  ...input2 === void 0 ? {} : { input: input2 }
@@ -14778,7 +15510,7 @@ var init_version = __esm({
14778
15510
 
14779
15511
  // src/cli.ts
14780
15512
  var import_commander = require("commander");
14781
- var CLI_VERSION = true ? "0.0.46" : "0.0.0-dev";
15513
+ var CLI_VERSION = true ? "0.0.49" : "0.0.0-dev";
14782
15514
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
14783
15515
  function withErrorHandling(action2) {
14784
15516
  return async (...args) => {
@@ -14936,6 +15668,16 @@ repoDevcontainerCommand.command("verify").description("Verify devcontainer onboa
14936
15668
  }
14937
15669
  )
14938
15670
  );
15671
+ var profileCommand = program.command("profile").description("Resolve profiles from `.devrouter.yml` without starting a runtime");
15672
+ 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(
15673
+ "--profile <name>",
15674
+ "Profile name or comma-separated selection (defaults to configured profile)"
15675
+ ).option("--json", "Output stable JSON").action(
15676
+ withErrorHandling(async (options) => {
15677
+ const { runProfileResolveCommand: runProfileResolveCommand2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
15678
+ await runProfileResolveCommand2(options);
15679
+ })
15680
+ );
14939
15681
  var appCommand = program.command("app").description("Manage app entries and runtime actions from `.devrouter.yml`");
14940
15682
  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(
14941
15683
  "--runtime <runtime>",