@devrouter/cli 0.0.40 → 0.0.42

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
@@ -317,10 +317,10 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
317
317
  - **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.
318
318
  - **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.
319
319
  - **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>\`.
320
- - **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 DevPod. Never substitute raw \`devpod up\`, \`stop\`, or \`delete\`: they bypass devrouter's machine-global ownership lock. \`workspace up\` creates linked worktrees; destructive worktree removal and GC remain ledger-scoped. Dirty or locked full down fails before side effects.
320
+ - **Lifecycle**: after one-time \`setup\`, use \`ensure .\` for both primary and linked checkouts; never branch manually on checkout kind or use live verify as startup. \`stop .\` is non-destructive; \`stop . --delete\` is explicit exact-owner cleanup without worktree removal; and \`exec . -- <command...>\` runs one-shot commands only in the exact running workspace runtime (DevPod or Devsy). Never substitute raw \`devpod up\`, \`stop\`, \`delete\`, or the Devsy equivalents: they bypass devrouter's machine-global ownership lock. Runtime selection is path-aware: \`DEVROUTER_WORKSPACE_RUNTIME=devpod|devsy\` forces one runtime, an exact-path registry owner wins next, then the machine preference from \`devrouter setup --workspace-runtime\`, then installed-CLI auto-detection. \`workspace up\` creates linked worktrees; destructive worktree removal and GC remain ledger-scoped. Dirty or locked full down fails before side effects.
321
321
  - **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.
322
322
  - **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.
323
- - **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\`), DevPod registration, runtime (\`running|stopped|busy|not-found|absent|unknown\`), checkout, route, advisory activity, and integration evidence without mutating DevPod, routes, ownership, Git, Docker, applications, worktrees, or branches. Local DevPod 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.
323
+ - **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.
324
324
  - **Sizing**: \`--measure-size\` adds per-workspace storage consumption to that report and stays read-only, but it walks each worktree and runs \`docker ps\` / \`docker inspect --size\`, so leave it off when you only need the evidence states. Each row reports reclaimable \`worktree\` and \`containerWritable\` bytes, non-reclaimable \`imageShared\` bytes, and a \`reclaimable\` total of the first two. \`imageShared\` covers image layers shared with other containers and overlaps across rows, so never sum it. Attribution is the workspace's own app container; sibling compose services such as a database are excluded. Any untrustworthy figure reports \`unknown\` with a reason rather than zero, and a workspace with no container reports a measured \`0\`.
325
325
  - **Boundary**: workspace commands require Git. Normal config, app, status, and doctor flows remain usable from a \`.devrouter.yml\` folder without \`.git\`.
326
326
 
@@ -373,8 +373,8 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
373
373
  - \`devrouter upgrade [version] [--repo .]\`: list upgrade targets or print target Agent Adaptation Prompt
374
374
  - \`devrouter setup --yes [--repo .] [--json]\`: first-run machine setup plus structured diagnostics
375
375
  - \`devrouter ensure [path] [--profile <name>] [--open] [--json]\`: canonical startup/reconciliation for primary and linked checkouts; a managed profile selects its independent resource dimensions
376
- - \`devrouter stop [path] [--delete] [--json]\`: stop the exact DevPod and remove exact routes; \`--delete\` explicitly deletes its ownership-proven data without removing the checkout
377
- - \`devrouter exec [path] -- <command...>\`: literal one-shot command inside the exact running DevPod
376
+ - \`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
377
+ - \`devrouter exec [path] -- <command...>\`: literal one-shot command inside the exact running workspace runtime
378
378
  - \`devrouter up\` / \`devrouter down\`: start/stop shared Traefik router
379
379
  - \`devrouter status\`: router/container/network/TLS health
380
380
  - \`devrouter doctor [--repo .]\`: deep diagnostics (global + repo)
@@ -396,7 +396,7 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
396
396
  - \`devrouter app rm <name> [--keep-config]\`: remove app entry (\`--keep-config\` frees only the live route/hostname, leaves \`.devrouter.yml\` untouched)
397
397
  - \`devrouter workspace up <branch> [--path <dir>] [--no-devpod] [--open]\`: create a worktree and start/prove it unless create-only mode is requested
398
398
  - \`devrouter workspace ensure [path] [--profile <name>] [--open] [--json]\`: compatibility alias of \`devrouter ensure\`
399
- - \`devrouter workspace ls [--json]\`: list ownership, Git, DevPod, route, path, and branch evidence
399
+ - \`devrouter workspace ls [--json]\`: list ownership, Git, workspace runtime, route, path, and branch evidence
400
400
  - \`devrouter workspace cleanup [--repo .] [--inactive-for 30d] [--check-merged] [--measure-size] [--json]\`: report-only cleanup evidence and exact guarded suggestions; no \`--yes\` or apply mode
401
401
  - \`devrouter workspace stop <workspace|branch>\`: stop DevPod and routes; preserve checkout, owner record, and data
402
402
  - \`devrouter workspace down <workspace|branch> [--keep-worktree]\`: delete runtime/routes and optionally remove the clean worktree and record
@@ -2504,7 +2504,7 @@ function loadRepoConfig(repoPath) {
2504
2504
  const config = parseConfig(parsed ?? {}, configPath);
2505
2505
  const requiredVersion = config.devrouter?.version;
2506
2506
  if (requiredVersion && !hasWarnedVersionMismatch) {
2507
- const cliVersion = true ? "0.0.40" : "0.0.0-dev";
2507
+ const cliVersion = true ? "0.0.42" : "0.0.0-dev";
2508
2508
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
2509
2509
  hasWarnedVersionMismatch = true;
2510
2510
  process.stderr.write(
@@ -2922,7 +2922,7 @@ function buildOnboardingPrompt(options = {}) {
2922
2922
  "- Shared Traefik router owns host ports 80 (HTTP), 443 (HTTPS), and 5432 (Postgres TCP).",
2923
2923
  "- Per-repo source of truth is REPO_PATH/.devrouter.yml only.",
2924
2924
  "- Global generated/runtime artifacts are managed under ~/.config/devrouter (do not edit these manually).",
2925
- "- Managed DevPod lifecycle uses devrouter ensure/stop/workspace commands; raw devpod mutations bypass exact checkout ownership checks.",
2925
+ "- Managed workspace runtime lifecycle uses devrouter ensure/stop/workspace commands; raw DevPod/Devsy mutations bypass exact checkout ownership checks. The active runtime is DevPod or Devsy, resolved per checkout: DEVROUTER_WORKSPACE_RUNTIME=devpod|devsy forces one runtime, an exact-path registry owner wins next (mixed fleets stay separated), then the machine preference from 'devrouter setup --workspace-runtime', then installed-CLI auto-detection.",
2926
2926
  "",
2927
2927
  "Inputs:",
2928
2928
  `- REPO_PATH=${repoPath}`,
@@ -3034,13 +3034,13 @@ function buildOnboardingPrompt(options = {}) {
3034
3034
  "- The owner record survives linked-worktree removal and binds the exact path to its DevPod ID. First use reuses an exact-path DevPod or derives a sanitized branch/path identity. Later flags or `DEVROUTER_WORKSPACE` may repeat but cannot rename it. Ambiguous identities fail closed. The primary checkout stays non-namespaced.",
3035
3035
  `- 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.`,
3036
3036
  "- 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.",
3037
- "- 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 mutations; they bypass the machine-global ownership lock. `workspace up` creates linked worktrees; destructive worktree removal and GC remain ledger-scoped.",
3037
+ "- Lifecycle: after one-time setup, use `devrouter ensure .` for both primary and linked checkouts; never branch on checkout kind or use live verify as startup. Managed consumer images contain no devrouter package/helper: ensure delivers its matching helper at runtime and invokes an exact captured snapshot of the repository-owned post-start adapter. Keep `.devrouter.yml` as the only consumer-side version pin. Use `devrouter stop .` for a non-destructive pause, `devrouter stop . --delete` only for explicit exact-owner cleanup without removing the checkout, and `devrouter exec . -- <command...>` for container commands. Never substitute raw DevPod/Devsy mutations; they bypass the machine-global ownership lock. `workspace up` creates linked worktrees; destructive worktree removal and GC remain ledger-scoped.",
3038
3038
  "- 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.",
3039
3039
  "- 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.",
3040
- "- `devrouter workspace cleanup --repo . --inactive-for 30d --check-merged --json` is report-only and never mutates DevPod, routes, ownership, Git, Docker, applications, worktrees, or branches. It reports ownership, DevPod registration, runtime (`running|stopped|busy|not-found|absent|unknown`), checkout, advisory activity, route, and integration evidence. Local DevPod 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. DevPod `lastUsed` remains advisory.",
3040
+ "- `devrouter workspace cleanup --repo . --inactive-for 30d --check-merged --json` is report-only and never mutates the workspace runtime, routes, ownership, Git, Docker, applications, worktrees, or branches. It reports ownership, workspace runtime registration, runtime state (`running|stopped|busy|not-found|absent|unknown`), checkout, advisory activity, route, and integration evidence. 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. Runtime `lastUsed` remains advisory.",
3041
3041
  '- `--measure-size` adds per-workspace storage consumption and stays read-only, but walks each worktree and runs `docker ps` / `docker inspect --size`, so leave it off for a quick report. Each row reports `worktree` and `containerWritable` bytes, which are reclaimable; `imageShared` bytes, which are not, because those image layers are shared with other containers and overlap across rows; and `reclaimable`, the sum of the reclaimable fields. Never add `imageShared` across rows. Any figure that cannot be trusted reports `{"status": "unknown", "reason": ...}` rather than zero, and a workspace with no container reports a measured `0`.',
3042
3042
  "- Workspace commands require Git. Normal config, app, status, and doctor flows work from a `.devrouter.yml` folder without `.git`. Git has no worktree-removal hook; use `workspace ls`, doctor, or dry-run GC after out-of-band removal.",
3043
- "- devcontainer integration: `devcontainer.json` lists the base compose file then `${localEnv:DEVCONTAINER_COMPOSE_OVERLAY:docker-compose.default.yml}`. The default overlay contains `services: {}`; `.devcontainer/docker-compose.devrouter.yml` passes `WORKSPACE` and `DEVROUTER_WORKSPACE` into the app and bind-mounts `${DEVROUTER_GIT_COMMON_DIR}` to the same absolute app-container path. Ensure proves exact DevPod ownership, overlay/Git mounts, env, aliases, health, Git, HTTP route reachability, and unique running TCP upstream ownership before success.",
3043
+ "- devcontainer integration: `devcontainer.json` lists the base compose file then `${localEnv:DEVCONTAINER_COMPOSE_OVERLAY:docker-compose.default.yml}`. The default overlay contains `services: {}`; `.devcontainer/docker-compose.devrouter.yml` passes `WORKSPACE` and `DEVROUTER_WORKSPACE` into the app and bind-mounts `${DEVROUTER_GIT_COMMON_DIR}` to the same absolute app-container path. Ensure proves exact workspace runtime ownership, overlay/Git mounts, env, aliases, health, Git, HTTP route reachability, and unique running TCP upstream ownership before success.",
3044
3044
  "",
3045
3045
  "Secret Manager Integration (config-based):",
3046
3046
  "- Optional top-level `secretManager.command` in `.devrouter.yml` wraps `devrouter app run` and `devrouter app exec` commands with the SM command and re-applies devrouter-injected dep env vars after the SM boundary via `env KEY=VAL` prefix.",
@@ -3204,11 +3204,11 @@ var init_ai_prompt = __esm({
3204
3204
  },
3205
3205
  {
3206
3206
  command: "devrouter stop [path] [--delete] [--json]",
3207
- purpose: "Stop the exact checkout DevPod and routes; --delete explicitly deletes the ownership-proven DevPod without removing the checkout."
3207
+ purpose: "Stop the exact checkout workspace runtime and routes; --delete explicitly deletes the ownership-proven runtime without removing the checkout."
3208
3208
  },
3209
3209
  {
3210
3210
  command: "devrouter exec [path] -- <command...>",
3211
- purpose: "Run literal argv inside the exact already-running DevPod and proven checkout mount."
3211
+ purpose: "Run literal argv inside the exact already-running workspace runtime and proven checkout mount."
3212
3212
  },
3213
3213
  {
3214
3214
  command: "devrouter workspace up <branch> [--path <dir>] [--no-devpod] [--open]",
@@ -3220,7 +3220,7 @@ var init_ai_prompt = __esm({
3220
3220
  },
3221
3221
  {
3222
3222
  command: "devrouter workspace ls [--json]",
3223
- purpose: "List owner, Git, DevPod, route, path, and branch evidence for managed workspaces."
3223
+ purpose: "List owner, Git, workspace runtime, route, path, and branch evidence for managed workspaces."
3224
3224
  },
3225
3225
  {
3226
3226
  command: "devrouter workspace cleanup [--repo <path>] [--inactive-for 30d] [--check-merged] [--measure-size] [--json]",
@@ -3228,11 +3228,11 @@ var init_ai_prompt = __esm({
3228
3228
  },
3229
3229
  {
3230
3230
  command: "devrouter workspace stop <workspace|branch>",
3231
- purpose: "Stop the exact DevPod and remove exact routes while preserving the worktree, owner record, and data."
3231
+ purpose: "Stop the exact workspace runtime and remove exact routes while preserving the worktree, owner record, and data."
3232
3232
  },
3233
3233
  {
3234
3234
  command: "devrouter workspace down <workspace|branch> [--keep-worktree]",
3235
- purpose: "Delete exact runtime/routes and remove only a clean, unlocked worktree and its owner record; --keep-worktree preserves checkout and record."
3235
+ purpose: "Delete exact workspace runtime/routes and remove only a clean, unlocked worktree and its owner record; --keep-worktree preserves checkout and record."
3236
3236
  },
3237
3237
  {
3238
3238
  command: "devrouter workspace gc [--json] [--yes]",
@@ -5195,7 +5195,7 @@ function hasExactComposeIdentity(container, options) {
5195
5195
  const actualFiles = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").map((file) => file.trim()).filter(Boolean);
5196
5196
  const providerGeneratedFiles = actualFiles.filter((file) => {
5197
5197
  const normalized = file.replaceAll("\\", "/");
5198
- return /\/.devpod\/agent\/contexts\/[^/]+\/workspaces\/[^/]+\/.docker-compose\/docker-compose\.devcontainer\.containerFeatures-[^/]+\.yml$/.test(
5198
+ return /\/(?:\.devpod\/agent\/contexts\/[^/]+\/workspaces\/[^/]+|\.devsy\/contexts\/[^/]+\/workspaces\/[^/]+\/agent)\/\.docker-compose\/docker-compose\.devcontainer\.containerFeatures-[^/]+\.yml$/.test(
5199
5199
  normalized
5200
5200
  );
5201
5201
  });
@@ -6047,6 +6047,385 @@ var init_tls = __esm({
6047
6047
  }
6048
6048
  });
6049
6049
 
6050
+ // src/core/devpod-registry.ts
6051
+ function listDevpodWorkspacesRaw() {
6052
+ const result = (0, import_node_child_process9.spawnSync)("devpod", ["list", "--output", "json", "--skip-pro"], {
6053
+ encoding: "utf-8"
6054
+ });
6055
+ if (result.status !== 0) {
6056
+ const details = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
6057
+ throw new Error(`devpod list failed: ${details || "devpod is not installed or unavailable"}`);
6058
+ }
6059
+ let parsed;
6060
+ try {
6061
+ parsed = JSON.parse(result.stdout);
6062
+ } catch {
6063
+ throw new Error("devpod list returned invalid JSON.");
6064
+ }
6065
+ if (!Array.isArray(parsed)) {
6066
+ throw new Error("devpod list returned an unexpected response.");
6067
+ }
6068
+ return parsed.map((entry) => {
6069
+ const candidate = entry;
6070
+ if (typeof candidate.id !== "string" || !candidate.source || typeof candidate.source.localFolder !== "string") {
6071
+ throw new Error("devpod list returned a workspace without id/source.localFolder.");
6072
+ }
6073
+ const workspace = {
6074
+ id: candidate.id,
6075
+ source: { localFolder: candidate.source.localFolder }
6076
+ };
6077
+ if ("lastUsed" in candidate) {
6078
+ if (typeof candidate.lastUsed === "string") {
6079
+ workspace.lastUsed = candidate.lastUsed;
6080
+ } else {
6081
+ workspace.lastUsedMalformed = true;
6082
+ }
6083
+ }
6084
+ return workspace;
6085
+ });
6086
+ }
6087
+ var import_node_child_process9;
6088
+ var init_devpod_registry = __esm({
6089
+ "src/core/devpod-registry.ts"() {
6090
+ "use strict";
6091
+ import_node_child_process9 = require("child_process");
6092
+ }
6093
+ });
6094
+
6095
+ // src/core/devsy-workspaces.ts
6096
+ function listDevsyWorkspaces() {
6097
+ const result = (0, import_node_child_process10.spawnSync)(
6098
+ "devsy",
6099
+ ["workspace", "list", "--result-format", "json", "--skip-pro"],
6100
+ {
6101
+ encoding: "utf-8"
6102
+ }
6103
+ );
6104
+ if (result.status !== 0) {
6105
+ const details = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
6106
+ throw new Error(
6107
+ `devsy workspace list failed: ${details || "devsy is not installed or unavailable"}`
6108
+ );
6109
+ }
6110
+ let parsed;
6111
+ try {
6112
+ parsed = JSON.parse(result.stdout);
6113
+ } catch {
6114
+ throw new Error("devsy workspace list returned invalid JSON.");
6115
+ }
6116
+ if (!Array.isArray(parsed)) {
6117
+ throw new Error("devsy workspace list returned an unexpected response.");
6118
+ }
6119
+ return parsed.map((entry) => {
6120
+ const candidate = entry;
6121
+ const source = candidate.source;
6122
+ if (typeof candidate.id !== "string" || !source || typeof source.localFolder !== "string") {
6123
+ throw new Error("devsy workspace list returned a workspace without id/source.localFolder.");
6124
+ }
6125
+ const workspace = {
6126
+ id: candidate.id,
6127
+ source: { localFolder: source.localFolder }
6128
+ };
6129
+ if ("lastUsed" in candidate) {
6130
+ if (typeof candidate.lastUsed === "string") {
6131
+ workspace.lastUsed = candidate.lastUsed;
6132
+ } else {
6133
+ workspace.lastUsedMalformed = true;
6134
+ }
6135
+ }
6136
+ return workspace;
6137
+ });
6138
+ }
6139
+ function parseDevsyRuntimeStatus(output2, expectedId) {
6140
+ let parsed;
6141
+ try {
6142
+ parsed = JSON.parse(output2);
6143
+ } catch {
6144
+ return "unknown";
6145
+ }
6146
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || parsed.id !== expectedId) {
6147
+ return "unknown";
6148
+ }
6149
+ switch (parsed.state) {
6150
+ case "Running":
6151
+ return "running";
6152
+ case "Stopped":
6153
+ return "stopped";
6154
+ case "Busy":
6155
+ return "busy";
6156
+ case "NotFound":
6157
+ return "not-found";
6158
+ default:
6159
+ return "unknown";
6160
+ }
6161
+ }
6162
+ function inspectDevsyRuntimeStatus(devsyId) {
6163
+ const result = (0, import_node_child_process10.spawnSync)("devsy", ["workspace", "status", devsyId, "--result-format", "json"], {
6164
+ encoding: "utf-8",
6165
+ timeout: 1e4
6166
+ });
6167
+ if (result.status !== 0 || result.error) return "unknown";
6168
+ return parseDevsyRuntimeStatus(result.stdout, devsyId);
6169
+ }
6170
+ function inspectDevsyWorkspaceOwnership(workspaces, devsyId, worktreePath) {
6171
+ const idOwners = workspaces.filter((workspace) => workspace.id === devsyId);
6172
+ const pathOwners = workspaces.filter(
6173
+ (workspace) => sameWorkspacePath(workspace.source.localFolder, worktreePath)
6174
+ );
6175
+ const exact = idOwners.filter(
6176
+ (workspace) => sameWorkspacePath(workspace.source.localFolder, worktreePath)
6177
+ );
6178
+ if (idOwners.length > 1 || pathOwners.length > 1 || idOwners[0] && exact.length === 0 || pathOwners[0] && pathOwners[0].id !== devsyId) {
6179
+ return {
6180
+ status: "conflict",
6181
+ reason: `Devsy workspace '${devsyId}' and worktree '${worktreePath}' do not have one exact owner.`
6182
+ };
6183
+ }
6184
+ return exact[0] ? { status: "owned", workspace: exact[0] } : { status: "absent" };
6185
+ }
6186
+ function selectDevsyWorkspace(workspaces, repoPath) {
6187
+ const matches = workspaces.filter(
6188
+ (workspace) => sameWorkspacePath(workspace.source.localFolder, repoPath)
6189
+ );
6190
+ if (matches.length > 1) {
6191
+ const ids = matches.map((match) => match.id).join(", ");
6192
+ throw new Error(`Multiple Devsy workspaces reference '${repoPath}': ${ids}`);
6193
+ }
6194
+ return matches[0];
6195
+ }
6196
+ var import_node_child_process10;
6197
+ var init_devsy_workspaces = __esm({
6198
+ "src/core/devsy-workspaces.ts"() {
6199
+ "use strict";
6200
+ import_node_child_process10 = require("child_process");
6201
+ init_workspace();
6202
+ }
6203
+ });
6204
+
6205
+ // src/core/workspace-runtime.ts
6206
+ function parseWorkspaceRuntime(value) {
6207
+ const requested = value.trim().toLowerCase();
6208
+ if (!SUPPORTED_RUNTIMES2.includes(requested)) {
6209
+ throw new UnsupportedWorkspaceRuntimeError(
6210
+ `Unsupported workspace runtime '${value}'. Supported: ${SUPPORTED_RUNTIMES2.join(", ")}.`
6211
+ );
6212
+ }
6213
+ return requested;
6214
+ }
6215
+ function isRuntimeInstalled(runtime) {
6216
+ const probeArgs = runtime === "devsy" ? ["--version"] : ["version"];
6217
+ const probe = (0, import_node_child_process11.spawnSync)(runtime, probeArgs, { encoding: "utf-8" });
6218
+ return probe.status === 0 && !probe.error;
6219
+ }
6220
+ function readWorkspaceRuntimeConfig() {
6221
+ let raw;
6222
+ try {
6223
+ raw = JSON.parse(import_node_fs16.default.readFileSync(RUNTIME_CONFIG_FILE, "utf-8"));
6224
+ } catch {
6225
+ return {};
6226
+ }
6227
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
6228
+ const candidate = raw;
6229
+ const config = {};
6230
+ if (typeof candidate.runtime === "string") {
6231
+ try {
6232
+ config.runtime = parseWorkspaceRuntime(candidate.runtime);
6233
+ } catch {
6234
+ }
6235
+ }
6236
+ if (typeof candidate.devsyInactivityTimeout === "string" && INACTIVITY_TIMEOUT_PATTERN.test(candidate.devsyInactivityTimeout)) {
6237
+ config.devsyInactivityTimeout = candidate.devsyInactivityTimeout;
6238
+ }
6239
+ return config;
6240
+ }
6241
+ function inspectWorkspaceRuntimeConfig() {
6242
+ let rawText;
6243
+ try {
6244
+ rawText = import_node_fs16.default.readFileSync(RUNTIME_CONFIG_FILE, "utf-8");
6245
+ } catch {
6246
+ return { exists: false, config: {}, problems: [] };
6247
+ }
6248
+ let raw;
6249
+ try {
6250
+ raw = JSON.parse(rawText);
6251
+ } catch {
6252
+ return { exists: true, config: {}, problems: ["workspace-runtime.json is not valid JSON."] };
6253
+ }
6254
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
6255
+ return {
6256
+ exists: true,
6257
+ config: {},
6258
+ problems: ["workspace-runtime.json must contain a JSON object."]
6259
+ };
6260
+ }
6261
+ const candidate = raw;
6262
+ const problems = [];
6263
+ if (candidate.runtime !== void 0) {
6264
+ try {
6265
+ parseWorkspaceRuntime(String(candidate.runtime));
6266
+ } catch {
6267
+ problems.push(`runtime='${String(candidate.runtime)}' is not a supported workspace runtime.`);
6268
+ }
6269
+ }
6270
+ if (candidate.devsyInactivityTimeout !== void 0) {
6271
+ const timeout = String(candidate.devsyInactivityTimeout);
6272
+ if (!isValidInactivityTimeout(timeout)) {
6273
+ problems.push(
6274
+ `devsyInactivityTimeout='${timeout}' is not a Go duration such as 30s, 10m, or 1h30m.`
6275
+ );
6276
+ }
6277
+ }
6278
+ return { exists: true, config: readWorkspaceRuntimeConfig(), problems };
6279
+ }
6280
+ function isValidInactivityTimeout(value) {
6281
+ return INACTIVITY_TIMEOUT_PATTERN.test(value.trim());
6282
+ }
6283
+ function writeWorkspaceRuntimeConfig(config) {
6284
+ const next = {};
6285
+ if (config.runtime !== void 0) next.runtime = parseWorkspaceRuntime(config.runtime);
6286
+ if (config.devsyInactivityTimeout !== void 0) {
6287
+ const timeout = config.devsyInactivityTimeout.trim();
6288
+ if (!isValidInactivityTimeout(timeout)) {
6289
+ throw new Error(
6290
+ `Invalid Devsy inactivity timeout '${config.devsyInactivityTimeout}': use a duration such as 30s, 10m, or 1h30m.`
6291
+ );
6292
+ }
6293
+ next.devsyInactivityTimeout = timeout;
6294
+ }
6295
+ import_node_fs16.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
6296
+ writeFileAtomically(RUNTIME_CONFIG_FILE, `${JSON.stringify(next, null, 2)}
6297
+ `);
6298
+ }
6299
+ function getWorkspaceRegistrySnapshots() {
6300
+ if (cachedSnapshots) return cachedSnapshots;
6301
+ const snapshots = { unavailable: [], errors: {} };
6302
+ if (isRuntimeInstalled("devsy")) {
6303
+ try {
6304
+ snapshots.devsy = listDevsyWorkspaces();
6305
+ } catch (error) {
6306
+ snapshots.unavailable.push("devsy");
6307
+ snapshots.errors.devsy = error instanceof Error ? error.message : String(error);
6308
+ }
6309
+ }
6310
+ if (isRuntimeInstalled("devpod")) {
6311
+ try {
6312
+ snapshots.devpod = listDevpodWorkspacesRaw();
6313
+ } catch (error) {
6314
+ snapshots.unavailable.push("devpod");
6315
+ snapshots.errors.devpod = error instanceof Error ? error.message : String(error);
6316
+ }
6317
+ }
6318
+ cachedSnapshots = snapshots;
6319
+ return snapshots;
6320
+ }
6321
+ function pathOwnerRuntime(repoPath, snapshots) {
6322
+ if (snapshots.unavailable.length > 0) {
6323
+ return { status: "unavailable", runtimes: snapshots.unavailable };
6324
+ }
6325
+ const devsyOwners = snapshots.devsy?.filter(
6326
+ (workspace) => sameWorkspacePath(workspace.source.localFolder, repoPath)
6327
+ );
6328
+ const devpodOwners = snapshots.devpod?.filter(
6329
+ (workspace) => sameWorkspacePath(workspace.source.localFolder, repoPath)
6330
+ );
6331
+ if (devsyOwners?.length && devpodOwners?.length) return { status: "conflict" };
6332
+ if (devsyOwners?.length) return { status: "owner", runtime: "devsy" };
6333
+ if (devpodOwners?.length) return { status: "owner", runtime: "devpod" };
6334
+ return { status: "none" };
6335
+ }
6336
+ function autoDetectedRuntime() {
6337
+ for (const runtime of SUPPORTED_RUNTIMES2) {
6338
+ if (isRuntimeInstalled(runtime)) return runtime;
6339
+ }
6340
+ return void 0;
6341
+ }
6342
+ function resolveWorkspaceRuntimeDetailed(repoPath, override) {
6343
+ if (override) {
6344
+ return { runtime: parseWorkspaceRuntime(override), source: "override" };
6345
+ }
6346
+ const envRaw = process.env.DEVROUTER_WORKSPACE_RUNTIME?.trim();
6347
+ if (envRaw) {
6348
+ return { runtime: parseWorkspaceRuntime(envRaw), source: "env" };
6349
+ }
6350
+ if (repoPath) {
6351
+ const snapshots = getWorkspaceRegistrySnapshots();
6352
+ const owner = pathOwnerRuntime(repoPath, snapshots);
6353
+ if (owner.status === "conflict") {
6354
+ throw new WorkspaceRuntimeOwnershipError(
6355
+ "Both DevPod and Devsy claim this checkout. Remove the stale registration before running a workspace lifecycle command."
6356
+ );
6357
+ }
6358
+ if (owner.status === "unavailable") {
6359
+ throw new WorkspaceRuntimeOwnershipError(
6360
+ `Cannot prove checkout ownership because the ${owner.runtimes.join(
6361
+ " and "
6362
+ )} workspace registry is unavailable. Restore registry access before running a workspace lifecycle command.`
6363
+ );
6364
+ }
6365
+ if (owner.status === "owner") {
6366
+ return { runtime: owner.runtime, source: "path-owner" };
6367
+ }
6368
+ }
6369
+ if (cachedRuntime) return cachedRuntime;
6370
+ let resolution;
6371
+ const machine = readWorkspaceRuntimeConfig().runtime;
6372
+ if (machine) {
6373
+ resolution = { runtime: machine, source: "machine-config" };
6374
+ } else {
6375
+ const detected = autoDetectedRuntime();
6376
+ resolution = detected ? { runtime: detected, source: "auto-detect" } : { runtime: "devpod", source: "default" };
6377
+ }
6378
+ cachedRuntime = resolution;
6379
+ return resolution;
6380
+ }
6381
+ function resolveWorkspaceRuntimeOrDefault(repoPath) {
6382
+ return resolveWorkspaceRuntimeDetailed(repoPath).runtime;
6383
+ }
6384
+ function resolveWorkspaceRuntimeForReport(repoPath) {
6385
+ const snapshots = getWorkspaceRegistrySnapshots();
6386
+ const envRaw = process.env.DEVROUTER_WORKSPACE_RUNTIME?.trim();
6387
+ if (envRaw) {
6388
+ const runtime2 = parseWorkspaceRuntime(envRaw);
6389
+ return snapshots[runtime2] ? runtime2 : void 0;
6390
+ }
6391
+ if (repoPath) {
6392
+ const owner = pathOwnerRuntime(repoPath, snapshots);
6393
+ if (owner.status === "conflict" || owner.status === "unavailable") return void 0;
6394
+ if (owner.status === "owner") return owner.runtime;
6395
+ }
6396
+ const runtime = resolveWorkspaceRuntimeDetailed().runtime;
6397
+ return snapshots[runtime] ? runtime : void 0;
6398
+ }
6399
+ function resetWorkspaceRuntimeCaches() {
6400
+ cachedRuntime = void 0;
6401
+ cachedSnapshots = void 0;
6402
+ }
6403
+ var import_node_child_process11, import_node_fs16, import_node_path15, SUPPORTED_RUNTIMES2, RUNTIME_CONFIG_FILE, INACTIVITY_TIMEOUT_PATTERN, cachedRuntime, cachedSnapshots, UnsupportedWorkspaceRuntimeError, WorkspaceRuntimeOwnershipError;
6404
+ var init_workspace_runtime = __esm({
6405
+ "src/core/workspace-runtime.ts"() {
6406
+ "use strict";
6407
+ import_node_child_process11 = require("child_process");
6408
+ import_node_fs16 = __toESM(require("fs"));
6409
+ import_node_path15 = __toESM(require("path"));
6410
+ init_atomic_file();
6411
+ init_devpod_registry();
6412
+ init_devsy_workspaces();
6413
+ init_router();
6414
+ init_workspace();
6415
+ SUPPORTED_RUNTIMES2 = ["devpod", "devsy"];
6416
+ RUNTIME_CONFIG_FILE = import_node_path15.default.join(DEVROUTER_HOME, "workspace-runtime.json");
6417
+ INACTIVITY_TIMEOUT_PATTERN = /^(?:\d+(?:ms|s|m|h))+$/;
6418
+ UnsupportedWorkspaceRuntimeError = class extends Error {
6419
+ };
6420
+ WorkspaceRuntimeOwnershipError = class extends Error {
6421
+ constructor(message) {
6422
+ super(message);
6423
+ this.name = "WorkspaceRuntimeOwnershipError";
6424
+ }
6425
+ };
6426
+ }
6427
+ });
6428
+
6050
6429
  // src/core/tool-diagnostics.ts
6051
6430
  function outputFromResult(result) {
6052
6431
  const stdout = typeof result.stdout === "string" ? result.stdout.trim() : "";
@@ -6055,7 +6434,7 @@ function outputFromResult(result) {
6055
6434
  return output2.length > 0 ? output2 : void 0;
6056
6435
  }
6057
6436
  function runTool(command, args = []) {
6058
- const result = (0, import_node_child_process9.spawnSync)(command, args, {
6437
+ const result = (0, import_node_child_process12.spawnSync)(command, args, {
6059
6438
  encoding: "utf-8"
6060
6439
  });
6061
6440
  if (result.error) {
@@ -6112,12 +6491,12 @@ function parseMinimumNodeMajor(value) {
6112
6491
  return Number(match[1]);
6113
6492
  }
6114
6493
  function readPackageJson(repoPath) {
6115
- const packagePath = import_node_path15.default.join(repoPath, "package.json");
6116
- if (!import_node_fs16.default.existsSync(packagePath)) {
6494
+ const packagePath = import_node_path16.default.join(repoPath, "package.json");
6495
+ if (!import_node_fs17.default.existsSync(packagePath)) {
6117
6496
  return void 0;
6118
6497
  }
6119
6498
  try {
6120
- return JSON.parse(import_node_fs16.default.readFileSync(packagePath, "utf-8"));
6499
+ return JSON.parse(import_node_fs17.default.readFileSync(packagePath, "utf-8"));
6121
6500
  } catch {
6122
6501
  return void 0;
6123
6502
  }
@@ -6201,63 +6580,110 @@ function buildGlobalToolChecks(repoPath) {
6201
6580
  details: mkcert.ok ? firstLine(mkcert.output) : mkcert.error,
6202
6581
  suggestion: mkcert.ok ? void 0 : brew.ok ? "Install mkcert: brew install mkcert" : "Install mkcert for local HTTPS, then run: devrouter setup --yes"
6203
6582
  });
6204
- const devpod = runTool("devpod", ["version"]);
6583
+ let ownershipProblem;
6584
+ let runtimeResolution;
6585
+ try {
6586
+ runtimeResolution = resolveWorkspaceRuntimeDetailed(repoPath);
6587
+ } catch (error) {
6588
+ if (!(error instanceof WorkspaceRuntimeOwnershipError)) throw error;
6589
+ ownershipProblem = error.message;
6590
+ runtimeResolution = resolveWorkspaceRuntimeDetailed();
6591
+ }
6592
+ const workspaceRuntime = runtimeResolution.runtime;
6593
+ const machineConfig = readWorkspaceRuntimeConfig();
6594
+ const runtimeLabel = workspaceRuntime === "devsy" ? "Devsy" : "DevPod";
6595
+ const configInspection = inspectWorkspaceRuntimeConfig();
6596
+ const runtimeArgs = workspaceRuntime === "devsy" ? ["--version"] : ["version"];
6597
+ const runtimeTool = runTool(workspaceRuntime, runtimeArgs);
6598
+ if (ownershipProblem) {
6599
+ checks.push({
6600
+ id: "repo.workspace-runtime-ownership",
6601
+ level: "error",
6602
+ summary: "Workspace runtime ownership cannot be resolved safely.",
6603
+ details: ownershipProblem,
6604
+ suggestion: "Restore both registries and remove any duplicate checkout registration."
6605
+ });
6606
+ }
6205
6607
  checks.push({
6206
6608
  id: "global.devpod",
6207
- level: devpod.ok ? "ok" : "warn",
6208
- summary: devpod.ok ? "DevPod is installed." : "DevPod is not installed.",
6209
- details: firstLine(devpod.output) ?? devpod.error,
6210
- suggestion: devpod.ok ? void 0 : "Install DevPod for devcontainer workspace flows: brew install devpod"
6609
+ level: runtimeTool.ok ? "ok" : "warn",
6610
+ summary: runtimeTool.ok ? ownershipProblem ? `${runtimeLabel} is configured, but checkout ownership is unresolved.` : `${runtimeLabel} is the active workspace runtime (source: ${runtimeResolution.source}).` : `${runtimeLabel} is the active workspace runtime but is not installed.`,
6611
+ details: [
6612
+ firstLine(runtimeTool.output) ?? runtimeTool.error,
6613
+ `source=${runtimeResolution.source}`,
6614
+ ...machineConfig.devsyInactivityTimeout ? [`devsyInactivityTimeout=${machineConfig.devsyInactivityTimeout}`] : []
6615
+ ].filter(Boolean).join(", "),
6616
+ 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"
6617
+ });
6618
+ const configProblems = [...configInspection.problems];
6619
+ const machineResolvedDevpod = workspaceRuntime === "devpod" && (runtimeResolution.source === "auto-detect" || runtimeResolution.source === "default");
6620
+ if (machineConfig.devsyInactivityTimeout && machineResolvedDevpod) {
6621
+ configProblems.push(
6622
+ "devsyInactivityTimeout is configured but the active workspace runtime is DevPod."
6623
+ );
6624
+ }
6625
+ const configDetails = configInspection.exists ? [
6626
+ `runtime=${machineConfig.runtime ?? "unset"}`,
6627
+ ...machineConfig.devsyInactivityTimeout ? [`devsyInactivityTimeout=${machineConfig.devsyInactivityTimeout}`] : []
6628
+ ].join(", ") : "Runtime selection falls back to auto-detection.";
6629
+ checks.push({
6630
+ id: "global.workspace-runtime-config",
6631
+ level: configProblems.length > 0 ? "warn" : "ok",
6632
+ summary: !configInspection.exists ? "No machine workspace-runtime preference is configured." : configProblems.length > 0 ? "Machine workspace-runtime preference has problems." : "Machine workspace-runtime preference is valid.",
6633
+ details: configProblems.length > 0 ? [configDetails, ...configProblems].join(" ") : configDetails,
6634
+ suggestion: configProblems.length > 0 ? "Run: devrouter setup --yes --workspace-runtime <devpod|devsy>" : void 0
6211
6635
  });
6212
6636
  checks.push(nodeToolchainCheck(repoPath));
6213
6637
  return checks;
6214
6638
  }
6215
- var import_node_child_process9, import_node_fs16, import_node_path15;
6639
+ var import_node_child_process12, import_node_fs17, import_node_path16;
6216
6640
  var init_tool_diagnostics = __esm({
6217
6641
  "src/core/tool-diagnostics.ts"() {
6218
6642
  "use strict";
6219
- import_node_child_process9 = require("child_process");
6220
- import_node_fs16 = __toESM(require("fs"));
6221
- import_node_path15 = __toESM(require("path"));
6643
+ import_node_child_process12 = require("child_process");
6644
+ import_node_fs17 = __toESM(require("fs"));
6645
+ import_node_path16 = __toESM(require("path"));
6646
+ init_workspace_runtime();
6222
6647
  }
6223
6648
  });
6224
6649
 
6225
6650
  // src/core/devpod-workspaces.ts
6226
- function listDevpodWorkspaces() {
6227
- const result = (0, import_node_child_process10.spawnSync)("devpod", ["list", "--output", "json", "--skip-pro"], {
6228
- encoding: "utf-8"
6229
- });
6230
- if (result.status !== 0) {
6231
- const details = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
6232
- throw new Error(`devpod list failed: ${details || "devpod is not installed or unavailable"}`);
6233
- }
6234
- let parsed;
6235
- try {
6236
- parsed = JSON.parse(result.stdout);
6237
- } catch {
6238
- throw new Error("devpod list returned invalid JSON.");
6239
- }
6240
- if (!Array.isArray(parsed)) {
6241
- throw new Error("devpod list returned an unexpected response.");
6242
- }
6243
- return parsed.map((entry) => {
6244
- const candidate = entry;
6245
- if (typeof candidate.id !== "string" || !candidate.source || typeof candidate.source.localFolder !== "string") {
6246
- throw new Error("devpod list returned a workspace without id/source.localFolder.");
6247
- }
6248
- const workspace = {
6249
- id: candidate.id,
6250
- source: { localFolder: candidate.source.localFolder }
6251
- };
6252
- if ("lastUsed" in candidate) {
6253
- if (typeof candidate.lastUsed === "string") {
6254
- workspace.lastUsed = candidate.lastUsed;
6255
- } else {
6256
- workspace.lastUsedMalformed = true;
6257
- }
6258
- }
6259
- return workspace;
6260
- });
6651
+ function listDevpodWorkspaces(repoPath) {
6652
+ if (resolveWorkspaceRuntimeOrDefault(repoPath) === "devsy") {
6653
+ return listDevsyWorkspaces().map((workspace) => ({
6654
+ id: workspace.id,
6655
+ source: workspace.source,
6656
+ ...workspace.lastUsed ? { lastUsed: workspace.lastUsed } : {},
6657
+ ...workspace.lastUsedMalformed ? { lastUsedMalformed: true } : {}
6658
+ }));
6659
+ }
6660
+ return listDevpodWorkspacesRaw();
6661
+ }
6662
+ function listDevpodWorkspacesFromSnapshots(repoPath) {
6663
+ const snapshots = getWorkspaceRegistrySnapshots();
6664
+ const runtime = resolveWorkspaceRuntimeForReport(repoPath);
6665
+ if (!runtime) return void 0;
6666
+ if (runtime === "devsy") {
6667
+ return snapshots.devsy?.map((workspace) => ({
6668
+ id: workspace.id,
6669
+ source: workspace.source,
6670
+ ...workspace.lastUsed ? { lastUsed: workspace.lastUsed } : {},
6671
+ ...workspace.lastUsedMalformed ? { lastUsedMalformed: true } : {}
6672
+ }));
6673
+ }
6674
+ return snapshots.devpod;
6675
+ }
6676
+ function listMergedRuntimeWorkspaces() {
6677
+ const snapshots = getWorkspaceRegistrySnapshots();
6678
+ return [
6679
+ ...snapshots.devpod ?? [],
6680
+ ...snapshots.devsy?.map((workspace) => ({
6681
+ id: workspace.id,
6682
+ source: workspace.source,
6683
+ ...workspace.lastUsed ? { lastUsed: workspace.lastUsed } : {},
6684
+ ...workspace.lastUsedMalformed ? { lastUsedMalformed: true } : {}
6685
+ })) ?? []
6686
+ ];
6261
6687
  }
6262
6688
  function parseDevpodRuntimeStatus(output2, expectedId) {
6263
6689
  let parsed;
@@ -6282,8 +6708,11 @@ function parseDevpodRuntimeStatus(output2, expectedId) {
6282
6708
  return "unknown";
6283
6709
  }
6284
6710
  }
6285
- function inspectDevpodRuntimeStatus(devpodId) {
6286
- const result = (0, import_node_child_process10.spawnSync)("devpod", ["status", devpodId, "--output", "json", "--timeout", "5s"], {
6711
+ function inspectDevpodRuntimeStatus(devpodId, repoPath) {
6712
+ if (resolveWorkspaceRuntimeOrDefault(repoPath) === "devsy") {
6713
+ return inspectDevsyRuntimeStatus(devpodId);
6714
+ }
6715
+ const result = (0, import_node_child_process13.spawnSync)("devpod", ["status", devpodId, "--output", "json", "--timeout", "5s"], {
6287
6716
  encoding: "utf-8"
6288
6717
  });
6289
6718
  if (result.status !== 0 || result.error) return "unknown";
@@ -6316,61 +6745,242 @@ function selectDevpodWorkspace(workspaces, repoPath) {
6316
6745
  }
6317
6746
  return matches[0];
6318
6747
  }
6319
- var import_node_child_process10;
6748
+ var import_node_child_process13;
6320
6749
  var init_devpod_workspaces = __esm({
6321
6750
  "src/core/devpod-workspaces.ts"() {
6322
6751
  "use strict";
6323
- import_node_child_process10 = require("child_process");
6752
+ import_node_child_process13 = require("child_process");
6753
+ init_devpod_registry();
6754
+ init_devsy_workspaces();
6324
6755
  init_workspace();
6756
+ init_workspace_runtime();
6325
6757
  }
6326
6758
  });
6327
6759
 
6328
- // src/core/devpod-mutation.ts
6760
+ // src/core/devsy-mutation.ts
6761
+ function failedStartMayHaveAttached(devsyId, repoPath) {
6762
+ try {
6763
+ const attached = listDevsyWorkspaces();
6764
+ const attachedId = devsyId ?? selectDevsyWorkspace(attached, repoPath)?.id;
6765
+ if (!attachedId) return false;
6766
+ return inspectDevsyWorkspaceOwnership(attached, attachedId, repoPath).status !== "absent";
6767
+ } catch {
6768
+ return true;
6769
+ }
6770
+ }
6329
6771
  function withMutationLock(activity, target, operation) {
6330
- import_node_fs17.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
6772
+ import_node_fs18.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
6773
+ return withFileLockSync(
6774
+ DEVSY_MUTATION_LOCK_FILE,
6775
+ { activity, target: `'${target}'`, waitMs: DEVSY_MUTATION_WAIT_MS },
6776
+ operation
6777
+ );
6778
+ }
6779
+ function commandFailure2(result) {
6780
+ return [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
6781
+ }
6782
+ function runDevsyAction(action2, devsyId, force = false) {
6783
+ const args = action2 === "delete" ? ["delete", devsyId, ...force ? ["--force"] : [], "--ignore-not-found"] : ["stop", devsyId];
6784
+ const result = (0, import_node_child_process14.spawnSync)("devsy", ["workspace", ...args], { encoding: "utf-8" });
6785
+ if (result.status !== 0) {
6786
+ throw new Error(
6787
+ `devsy workspace ${action2}${force ? " --force" : ""} failed for '${devsyId}': ${commandFailure2(result) || "unknown error"}`
6788
+ );
6789
+ }
6790
+ }
6791
+ function inspectExactOwnership(devsyId, worktreePath) {
6792
+ const ownership = inspectDevsyWorkspaceOwnership(listDevsyWorkspaces(), devsyId, worktreePath);
6793
+ if (ownership.status === "conflict") throw new Error(ownership.reason);
6794
+ return ownership;
6795
+ }
6796
+ function mutateOwnedDevsyWorkspace(action2, devsyId, worktreePath) {
6797
+ return withMutationLock(`Devsy ${action2}`, worktreePath, () => {
6798
+ const before = inspectExactOwnership(devsyId, worktreePath);
6799
+ if (before.status === "absent") return { status: "absent" };
6800
+ runDevsyAction(action2, devsyId);
6801
+ let after = inspectExactOwnership(devsyId, worktreePath);
6802
+ if (action2 === "stop" && after.status !== "owned") {
6803
+ throw new Error(
6804
+ `Devsy workspace '${devsyId}' no longer owns '${worktreePath}' after provider stop.`
6805
+ );
6806
+ }
6807
+ if (action2 === "delete" && after.status === "owned") {
6808
+ const runtime = inspectDevsyRuntimeStatus(devsyId);
6809
+ if (runtime !== "not-found") {
6810
+ throw new Error(
6811
+ `Devsy workspace '${devsyId}' still owns '${worktreePath}' after provider delete (runtime=${runtime}).`
6812
+ );
6813
+ }
6814
+ after = inspectExactOwnership(devsyId, worktreePath);
6815
+ if (after.status === "owned") {
6816
+ runDevsyAction("delete", devsyId, true);
6817
+ after = inspectExactOwnership(devsyId, worktreePath);
6818
+ }
6819
+ if (after.status !== "absent") {
6820
+ throw new Error(
6821
+ `Devsy workspace '${devsyId}' still owns '${worktreePath}' after forced provider delete.`
6822
+ );
6823
+ }
6824
+ }
6825
+ return { status: "changed" };
6826
+ });
6827
+ }
6828
+ function stopOwnedDevsyWorkspace(devsyId, worktreePath) {
6829
+ return mutateOwnedDevsyWorkspace("stop", devsyId, worktreePath);
6830
+ }
6831
+ function deleteOwnedDevsyWorkspace(devsyId, worktreePath) {
6832
+ return mutateOwnedDevsyWorkspace("delete", devsyId, worktreePath);
6833
+ }
6834
+ function assertDevsyTarget(devsyId, repoPath) {
6835
+ const workspaces = listDevsyWorkspaces();
6836
+ const existing = selectDevsyWorkspace(workspaces, repoPath);
6837
+ const id = devsyId ?? existing?.id;
6838
+ if (id) {
6839
+ const before = inspectDevsyWorkspaceOwnership(workspaces, id, repoPath);
6840
+ if (before.status === "conflict") throw new Error(before.reason);
6841
+ }
6842
+ return id;
6843
+ }
6844
+ function startDevsyWorkspace(options) {
6845
+ const activity = options.recreate ? "Devsy recreate" : "Devsy start";
6846
+ return withMutationLock(activity, options.repoPath, () => {
6847
+ let devsyId = assertDevsyTarget(options.devsyId, options.repoPath);
6848
+ if (devsyId && options.recreate) {
6849
+ const attached = listDevsyWorkspaces();
6850
+ const ownership = inspectDevsyWorkspaceOwnership(attached, devsyId, options.repoPath);
6851
+ if (ownership.status !== "owned") {
6852
+ throw new Error(`Cannot recreate Devsy workspace '${devsyId}' without one exact owner.`);
6853
+ }
6854
+ }
6855
+ if (!devsyId && options.recreate) {
6856
+ throw new Error("Cannot recreate a Devsy workspace before its exact id is known.");
6857
+ }
6858
+ const args = ["workspace", "up", options.repoPath];
6859
+ if (devsyId) args.push("--id", devsyId);
6860
+ if (options.devcontainerPath) args.push("--devcontainer", options.devcontainerPath);
6861
+ args.push("--ide-launch", "skip");
6862
+ if (options.workspace) {
6863
+ args.push(
6864
+ "--workspace-env",
6865
+ `WORKSPACE=${options.workspace.token}`,
6866
+ "--workspace-env",
6867
+ `DEVROUTER_WORKSPACE=${options.workspace.token}`
6868
+ );
6869
+ }
6870
+ if (options.inactivityTimeout) {
6871
+ args.push("--provider-option", `INACTIVITY_TIMEOUT=${options.inactivityTimeout}`);
6872
+ }
6873
+ if (options.recreate) args.push("--recreate");
6874
+ const env = { ...process.env };
6875
+ if (options.workspace) {
6876
+ env.WORKSPACE = options.workspace.token;
6877
+ env.DEVROUTER_WORKSPACE = options.workspace.token;
6878
+ env.DEVROUTER_GIT_COMMON_DIR = options.workspace.gitCommonDir;
6879
+ env.DEVCONTAINER_COMPOSE_OVERLAY = "docker-compose.devrouter.yml";
6880
+ } else {
6881
+ delete env.WORKSPACE;
6882
+ delete env.DEVROUTER_WORKSPACE;
6883
+ delete env.DEVROUTER_GIT_COMMON_DIR;
6884
+ delete env.DEVCONTAINER_COMPOSE_OVERLAY;
6885
+ }
6886
+ const result = (0, import_node_child_process14.spawnSync)("devsy", args, {
6887
+ stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
6888
+ env
6889
+ });
6890
+ if (result.status !== 0) {
6891
+ const message = `devsy workspace up failed for '${devsyId ?? options.repoPath}'.`;
6892
+ if (failedStartMayHaveAttached(devsyId, options.repoPath)) {
6893
+ throw new DevsyStartPostconditionError(message);
6894
+ }
6895
+ throw new Error(message);
6896
+ }
6897
+ try {
6898
+ const attached = listDevsyWorkspaces();
6899
+ devsyId ??= selectDevsyWorkspace(attached, options.repoPath)?.id;
6900
+ if (!devsyId) {
6901
+ throw new Error(`Devsy did not attach '${options.repoPath}' after startup.`);
6902
+ }
6903
+ const ownership = inspectDevsyWorkspaceOwnership(attached, devsyId, options.repoPath);
6904
+ if (ownership.status === "conflict") throw new Error(ownership.reason);
6905
+ if (ownership.status !== "owned") {
6906
+ throw new Error(
6907
+ `Devsy did not attach '${options.repoPath}' as '${devsyId}' after startup.`
6908
+ );
6909
+ }
6910
+ return devsyId;
6911
+ } catch (error) {
6912
+ const message = error instanceof Error ? error.message : String(error);
6913
+ throw new DevsyStartPostconditionError(message);
6914
+ }
6915
+ });
6916
+ }
6917
+ var import_node_child_process14, import_node_fs18, import_node_path17, DEVSY_MUTATION_LOCK_FILE, DEVSY_MUTATION_WAIT_MS, DevsyStartPostconditionError;
6918
+ var init_devsy_mutation = __esm({
6919
+ "src/core/devsy-mutation.ts"() {
6920
+ "use strict";
6921
+ import_node_child_process14 = require("child_process");
6922
+ import_node_fs18 = __toESM(require("fs"));
6923
+ import_node_path17 = __toESM(require("path"));
6924
+ init_devsy_workspaces();
6925
+ init_file_lock();
6926
+ init_router();
6927
+ DEVSY_MUTATION_LOCK_FILE = import_node_path17.default.join(DEVROUTER_HOME, "devsy-mutation.lock");
6928
+ DEVSY_MUTATION_WAIT_MS = 6e4;
6929
+ DevsyStartPostconditionError = class extends Error {
6930
+ };
6931
+ }
6932
+ });
6933
+
6934
+ // src/core/devpod-mutation.ts
6935
+ function withMutationLock2(activity, target, operation) {
6936
+ import_node_fs19.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
6331
6937
  return withFileLockSync(
6332
6938
  DEVPOD_MUTATION_LOCK_FILE,
6333
6939
  { activity, target: `'${target}'`, waitMs: DEVPOD_MUTATION_WAIT_MS },
6334
6940
  operation
6335
6941
  );
6336
6942
  }
6337
- function commandFailure2(result) {
6943
+ function commandFailure3(result) {
6338
6944
  return [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
6339
6945
  }
6340
6946
  function runDevpodAction(action2, devpodId, force = false) {
6341
6947
  const args = action2 === "delete" ? [action2, devpodId, ...force ? ["--force"] : [], "--ignore-not-found"] : [action2, devpodId];
6342
- const result = (0, import_node_child_process11.spawnSync)("devpod", args, { encoding: "utf-8" });
6948
+ const result = (0, import_node_child_process15.spawnSync)("devpod", args, { encoding: "utf-8" });
6343
6949
  if (result.status !== 0) {
6344
6950
  throw new Error(
6345
- `devpod ${action2}${force ? " --force" : ""} failed for '${devpodId}': ${commandFailure2(result) || "unknown error"}`
6951
+ `devpod ${action2}${force ? " --force" : ""} failed for '${devpodId}': ${commandFailure3(result) || "unknown error"}`
6346
6952
  );
6347
6953
  }
6348
6954
  }
6349
- function inspectExactOwnership(devpodId, worktreePath) {
6350
- const ownership = inspectDevpodWorkspaceOwnership(listDevpodWorkspaces(), devpodId, worktreePath);
6955
+ function inspectExactOwnership2(devpodId, worktreePath) {
6956
+ const ownership = inspectDevpodWorkspaceOwnership(
6957
+ listDevpodWorkspaces(worktreePath),
6958
+ devpodId,
6959
+ worktreePath
6960
+ );
6351
6961
  if (ownership.status === "conflict") throw new Error(ownership.reason);
6352
6962
  return ownership;
6353
6963
  }
6354
6964
  function mutateOwnedDevpodWorkspace(action2, devpodId, worktreePath) {
6355
- return withMutationLock(`DevPod ${action2}`, worktreePath, () => {
6356
- const before = inspectExactOwnership(devpodId, worktreePath);
6965
+ return withMutationLock2(`DevPod ${action2}`, worktreePath, () => {
6966
+ const before = inspectExactOwnership2(devpodId, worktreePath);
6357
6967
  if (before.status === "absent") return { status: "absent" };
6358
6968
  runDevpodAction(action2, devpodId);
6359
- let after = inspectExactOwnership(devpodId, worktreePath);
6969
+ let after = inspectExactOwnership2(devpodId, worktreePath);
6360
6970
  if (action2 === "stop" && after.status !== "owned") {
6361
6971
  throw new Error(`DevPod '${devpodId}' no longer owns '${worktreePath}' after provider stop.`);
6362
6972
  }
6363
6973
  if (action2 === "delete" && after.status === "owned") {
6364
- const runtime = inspectDevpodRuntimeStatus(devpodId);
6974
+ const runtime = inspectDevpodRuntimeStatus(devpodId, worktreePath);
6365
6975
  if (runtime !== "not-found") {
6366
6976
  throw new Error(
6367
6977
  `DevPod '${devpodId}' still owns '${worktreePath}' after provider delete (runtime=${runtime}).`
6368
6978
  );
6369
6979
  }
6370
- after = inspectExactOwnership(devpodId, worktreePath);
6980
+ after = inspectExactOwnership2(devpodId, worktreePath);
6371
6981
  if (after.status === "owned") {
6372
6982
  runDevpodAction("delete", devpodId, true);
6373
- after = inspectExactOwnership(devpodId, worktreePath);
6983
+ after = inspectExactOwnership2(devpodId, worktreePath);
6374
6984
  }
6375
6985
  if (after.status !== "absent") {
6376
6986
  throw new Error(
@@ -6382,15 +6992,50 @@ function mutateOwnedDevpodWorkspace(action2, devpodId, worktreePath) {
6382
6992
  });
6383
6993
  }
6384
6994
  function stopOwnedDevpodWorkspace(devpodId, worktreePath) {
6385
- return mutateOwnedDevpodWorkspace("stop", devpodId, worktreePath);
6995
+ if (resolveWorkspaceRuntimeOrDefault(worktreePath) === "devsy") {
6996
+ const result2 = stopOwnedDevsyWorkspace(devpodId, worktreePath);
6997
+ resetWorkspaceRuntimeCaches();
6998
+ return result2;
6999
+ }
7000
+ const result = mutateOwnedDevpodWorkspace("stop", devpodId, worktreePath);
7001
+ resetWorkspaceRuntimeCaches();
7002
+ return result;
6386
7003
  }
6387
7004
  function deleteOwnedDevpodWorkspace(devpodId, worktreePath) {
6388
- return mutateOwnedDevpodWorkspace("delete", devpodId, worktreePath);
7005
+ if (resolveWorkspaceRuntimeOrDefault(worktreePath) === "devsy") {
7006
+ const result2 = deleteOwnedDevsyWorkspace(devpodId, worktreePath);
7007
+ resetWorkspaceRuntimeCaches();
7008
+ return result2;
7009
+ }
7010
+ const result = mutateOwnedDevpodWorkspace("delete", devpodId, worktreePath);
7011
+ resetWorkspaceRuntimeCaches();
7012
+ return result;
6389
7013
  }
6390
7014
  function startDevpodWorkspace(options) {
7015
+ if (resolveWorkspaceRuntimeOrDefault(options.repoPath) === "devsy") {
7016
+ try {
7017
+ const result = startDevsyWorkspace({
7018
+ repoPath: options.repoPath,
7019
+ devsyId: options.devpodId,
7020
+ devcontainerPath: options.devcontainerPath,
7021
+ recreate: options.recreate,
7022
+ quiet: options.quiet,
7023
+ workspace: options.workspace,
7024
+ inactivityTimeout: readWorkspaceRuntimeConfig().devsyInactivityTimeout
7025
+ });
7026
+ resetWorkspaceRuntimeCaches();
7027
+ return result;
7028
+ } catch (error) {
7029
+ if (error instanceof DevsyStartPostconditionError) {
7030
+ resetWorkspaceRuntimeCaches();
7031
+ throw new DevpodStartPostconditionError(error.message);
7032
+ }
7033
+ throw error;
7034
+ }
7035
+ }
6391
7036
  const activity = options.recreate ? "DevPod recreate" : "DevPod start";
6392
- return withMutationLock(activity, options.repoPath, () => {
6393
- const workspaces = listDevpodWorkspaces();
7037
+ return withMutationLock2(activity, options.repoPath, () => {
7038
+ const workspaces = listDevpodWorkspaces(options.repoPath);
6394
7039
  let devpodId = options.devpodId ?? selectDevpodWorkspace(workspaces, options.repoPath)?.id;
6395
7040
  if (devpodId) {
6396
7041
  const before = inspectDevpodWorkspaceOwnership(workspaces, devpodId, options.repoPath);
@@ -6428,7 +7073,7 @@ function startDevpodWorkspace(options) {
6428
7073
  delete env.DEVROUTER_GIT_COMMON_DIR;
6429
7074
  delete env.DEVCONTAINER_COMPOSE_OVERLAY;
6430
7075
  }
6431
- const result = (0, import_node_child_process11.spawnSync)("devpod", args, {
7076
+ const result = (0, import_node_child_process15.spawnSync)("devpod", args, {
6432
7077
  stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
6433
7078
  env
6434
7079
  });
@@ -6436,7 +7081,7 @@ function startDevpodWorkspace(options) {
6436
7081
  throw new Error(`devpod up failed for '${devpodId ?? options.repoPath}'.`);
6437
7082
  }
6438
7083
  try {
6439
- const attached = listDevpodWorkspaces();
7084
+ const attached = listDevpodWorkspaces(options.repoPath);
6440
7085
  devpodId ??= selectDevpodWorkspace(attached, options.repoPath)?.id;
6441
7086
  if (!devpodId) {
6442
7087
  throw new Error(`DevPod did not attach '${options.repoPath}' after startup.`);
@@ -6448,6 +7093,7 @@ function startDevpodWorkspace(options) {
6448
7093
  `DevPod did not attach '${options.repoPath}' as '${devpodId}' after startup.`
6449
7094
  );
6450
7095
  }
7096
+ resetWorkspaceRuntimeCaches();
6451
7097
  return devpodId;
6452
7098
  } catch (error) {
6453
7099
  const message = error instanceof Error ? error.message : String(error);
@@ -6455,17 +7101,19 @@ function startDevpodWorkspace(options) {
6455
7101
  }
6456
7102
  });
6457
7103
  }
6458
- var import_node_child_process11, import_node_fs17, import_node_path16, DEVPOD_MUTATION_LOCK_FILE, DEVPOD_MUTATION_WAIT_MS, DevpodStartPostconditionError;
7104
+ var import_node_child_process15, import_node_fs19, import_node_path18, DEVPOD_MUTATION_LOCK_FILE, DEVPOD_MUTATION_WAIT_MS, DevpodStartPostconditionError;
6459
7105
  var init_devpod_mutation = __esm({
6460
7106
  "src/core/devpod-mutation.ts"() {
6461
7107
  "use strict";
6462
- import_node_child_process11 = require("child_process");
6463
- import_node_fs17 = __toESM(require("fs"));
6464
- import_node_path16 = __toESM(require("path"));
7108
+ import_node_child_process15 = require("child_process");
7109
+ import_node_fs19 = __toESM(require("fs"));
7110
+ import_node_path18 = __toESM(require("path"));
6465
7111
  init_devpod_workspaces();
7112
+ init_devsy_mutation();
6466
7113
  init_file_lock();
6467
7114
  init_router();
6468
- DEVPOD_MUTATION_LOCK_FILE = import_node_path16.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
7115
+ init_workspace_runtime();
7116
+ DEVPOD_MUTATION_LOCK_FILE = import_node_path18.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
6469
7117
  DEVPOD_MUTATION_WAIT_MS = 6e4;
6470
7118
  DevpodStartPostconditionError = class extends Error {
6471
7119
  };
@@ -6479,7 +7127,7 @@ function commandError(command, repoPath, stderr) {
6479
7127
  );
6480
7128
  }
6481
7129
  function resolveGitCommonDir(repoPath) {
6482
- const result = (0, import_node_child_process12.spawnSync)("git", ["-C", repoPath, "rev-parse", "--git-common-dir"], {
7130
+ const result = (0, import_node_child_process16.spawnSync)("git", ["-C", repoPath, "rev-parse", "--git-common-dir"], {
6483
7131
  encoding: "utf-8",
6484
7132
  env: READ_ONLY_GIT_ENV
6485
7133
  });
@@ -6487,10 +7135,10 @@ function resolveGitCommonDir(repoPath) {
6487
7135
  if (result.status !== 0 || !output2) {
6488
7136
  throw commandError("Could not resolve the Git common directory", repoPath, result.stderr);
6489
7137
  }
6490
- return comparableWorkspacePath(import_node_path17.default.isAbsolute(output2) ? output2 : import_node_path17.default.resolve(repoPath, output2));
7138
+ return comparableWorkspacePath(import_node_path19.default.isAbsolute(output2) ? output2 : import_node_path19.default.resolve(repoPath, output2));
6491
7139
  }
6492
7140
  function resolveGitTopLevel(repoPath) {
6493
- const result = (0, import_node_child_process12.spawnSync)("git", ["-C", repoPath, "rev-parse", "--show-toplevel"], {
7141
+ const result = (0, import_node_child_process16.spawnSync)("git", ["-C", repoPath, "rev-parse", "--show-toplevel"], {
6494
7142
  encoding: "utf-8",
6495
7143
  env: READ_ONLY_GIT_ENV
6496
7144
  });
@@ -6501,7 +7149,7 @@ function resolveGitTopLevel(repoPath) {
6501
7149
  return comparableWorkspacePath(output2);
6502
7150
  }
6503
7151
  function listGitWorktrees(repoPath) {
6504
- const result = (0, import_node_child_process12.spawnSync)("git", ["-C", repoPath, "worktree", "list", "--porcelain"], {
7152
+ const result = (0, import_node_child_process16.spawnSync)("git", ["-C", repoPath, "worktree", "list", "--porcelain"], {
6505
7153
  encoding: "utf-8",
6506
7154
  env: READ_ONLY_GIT_ENV
6507
7155
  });
@@ -6538,7 +7186,7 @@ function listGitWorktrees(repoPath) {
6538
7186
  return worktrees;
6539
7187
  }
6540
7188
  function ownershipDirectory(repoPath) {
6541
- return import_node_path17.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
7189
+ return import_node_path19.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
6542
7190
  }
6543
7191
  function validateWorkspace(value, label) {
6544
7192
  if (typeof value !== "string" || wsFromBranch(value) !== value) {
@@ -6566,7 +7214,7 @@ function validateRecord(value, expectedWorkspace) {
6566
7214
  `workspace ownership file '${expectedWorkspace}' contains identity '${workspace}'`
6567
7215
  );
6568
7216
  }
6569
- if (typeof candidate.worktreePath !== "string" || !import_node_path17.default.isAbsolute(candidate.worktreePath)) {
7217
+ if (typeof candidate.worktreePath !== "string" || !import_node_path19.default.isAbsolute(candidate.worktreePath)) {
6570
7218
  throw new Error("invalid workspace ownership worktreePath");
6571
7219
  }
6572
7220
  if (candidate.branch !== null && typeof candidate.branch !== "string") {
@@ -6586,7 +7234,7 @@ function validateRecord(value, expectedWorkspace) {
6586
7234
  function readRecordFile(filePath, expectedWorkspace) {
6587
7235
  let parsed;
6588
7236
  try {
6589
- parsed = JSON.parse(import_node_fs18.default.readFileSync(filePath, "utf-8"));
7237
+ parsed = JSON.parse(import_node_fs20.default.readFileSync(filePath, "utf-8"));
6590
7238
  } catch (error) {
6591
7239
  if (error instanceof SyntaxError) {
6592
7240
  throw new Error(`invalid workspace ownership JSON at '${filePath}'`);
@@ -6602,7 +7250,7 @@ function listWorkspaceOwnership(repoPath) {
6602
7250
  function listWorkspaceOwnershipInDirectory(directory) {
6603
7251
  let entries;
6604
7252
  try {
6605
- entries = import_node_fs18.default.readdirSync(directory, { withFileTypes: true });
7253
+ entries = import_node_fs20.default.readdirSync(directory, { withFileTypes: true });
6606
7254
  } catch (error) {
6607
7255
  if (error.code === "ENOENT") return [];
6608
7256
  throw error;
@@ -6610,14 +7258,14 @@ function listWorkspaceOwnershipInDirectory(directory) {
6610
7258
  return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
6611
7259
  const workspace = entry.name.slice(0, -".json".length);
6612
7260
  validateWorkspace(workspace, "filename");
6613
- return readRecordFile(import_node_path17.default.join(directory, entry.name), workspace);
7261
+ return readRecordFile(import_node_path19.default.join(directory, entry.name), workspace);
6614
7262
  });
6615
7263
  }
6616
7264
  function writeWorkspaceOwnershipInDirectory(directory, input2) {
6617
7265
  const workspace = validateWorkspace(input2.workspace, "workspace");
6618
7266
  const devpodId = validateWorkspace(input2.devpodId, "devpodId");
6619
7267
  const worktreePath = comparableWorkspacePath(input2.worktreePath);
6620
- const filePath = import_node_path17.default.join(directory, `${workspace}.json`);
7268
+ const filePath = import_node_path19.default.join(directory, `${workspace}.json`);
6621
7269
  const now = (/* @__PURE__ */ new Date()).toISOString();
6622
7270
  const records = listWorkspaceOwnershipInDirectory(directory);
6623
7271
  const existing = records.find((record2) => record2.workspace === workspace);
@@ -6653,9 +7301,9 @@ function writeWorkspaceOwnershipInDirectory(directory, input2) {
6653
7301
  return record;
6654
7302
  }
6655
7303
  function removeWorkspaceOwnershipInDirectory(directory, workspace) {
6656
- const filePath = import_node_path17.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
7304
+ const filePath = import_node_path19.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
6657
7305
  try {
6658
- import_node_fs18.default.rmSync(filePath);
7306
+ import_node_fs20.default.rmSync(filePath);
6659
7307
  return true;
6660
7308
  } catch (error) {
6661
7309
  if (error.code === "ENOENT") return false;
@@ -6666,7 +7314,7 @@ function sameOwnershipRecord(left, right) {
6666
7314
  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;
6667
7315
  }
6668
7316
  function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
6669
- const filePath = import_node_path17.default.join(
7317
+ const filePath = import_node_path19.default.join(
6670
7318
  directory,
6671
7319
  `${validateWorkspace(expected.workspace, "workspace")}.json`
6672
7320
  );
@@ -6678,14 +7326,14 @@ function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
6678
7326
  throw error;
6679
7327
  }
6680
7328
  if (!sameOwnershipRecord(current, expected)) return "changed";
6681
- import_node_fs18.default.rmSync(filePath);
7329
+ import_node_fs20.default.rmSync(filePath);
6682
7330
  return "removed";
6683
7331
  }
6684
7332
  function withWorkspaceOwnershipTransaction(repoPath, operation) {
6685
7333
  const directory = ownershipDirectory(repoPath);
6686
- import_node_fs18.default.mkdirSync(directory, { recursive: true });
7334
+ import_node_fs20.default.mkdirSync(directory, { recursive: true });
6687
7335
  return withFileLockSync(
6688
- import_node_path17.default.join(directory, ".lock"),
7336
+ import_node_path19.default.join(directory, ".lock"),
6689
7337
  { activity: "workspace ownership transaction", target: `'${repoPath}'`, waitMs: 5e3 },
6690
7338
  () => operation({
6691
7339
  list: () => listWorkspaceOwnershipInDirectory(directory),
@@ -6716,7 +7364,7 @@ function inspectWorkspaceOwnership(record, worktrees, devpods) {
6716
7364
  if (worktree?.locked) {
6717
7365
  return { ownerStatus: "locked", devpodStatus, worktree };
6718
7366
  }
6719
- if ((!worktree || worktree.prunable) && import_node_fs18.default.existsSync(record.worktreePath)) {
7367
+ if ((!worktree || worktree.prunable) && import_node_fs20.default.existsSync(record.worktreePath)) {
6720
7368
  return { ownerStatus: "conflict", devpodStatus, worktree };
6721
7369
  }
6722
7370
  if (!worktree || worktree.prunable) {
@@ -6740,20 +7388,20 @@ function listMissingWorkspaceOwnership(repoPath) {
6740
7388
  (record) => inspectWorkspaceOwnership(record, worktrees, void 0).ownerStatus === "missing"
6741
7389
  );
6742
7390
  }
6743
- var import_node_child_process12, import_node_fs18, import_node_path17, READ_ONLY_GIT_ENV, OWNERSHIP_VERSION, OWNERSHIP_DIR;
7391
+ var import_node_child_process16, import_node_fs20, import_node_path19, READ_ONLY_GIT_ENV, OWNERSHIP_VERSION, OWNERSHIP_DIR;
6744
7392
  var init_workspace_ownership = __esm({
6745
7393
  "src/core/workspace-ownership.ts"() {
6746
7394
  "use strict";
6747
- import_node_child_process12 = require("child_process");
6748
- import_node_fs18 = __toESM(require("fs"));
6749
- import_node_path17 = __toESM(require("path"));
7395
+ import_node_child_process16 = require("child_process");
7396
+ import_node_fs20 = __toESM(require("fs"));
7397
+ import_node_path19 = __toESM(require("path"));
6750
7398
  init_atomic_file();
6751
7399
  init_devpod_workspaces();
6752
7400
  init_file_lock();
6753
7401
  init_workspace();
6754
7402
  READ_ONLY_GIT_ENV = { ...process.env, GIT_OPTIONAL_LOCKS: "0" };
6755
7403
  OWNERSHIP_VERSION = 1;
6756
- OWNERSHIP_DIR = import_node_path17.default.join("devrouter", "workspaces");
7404
+ OWNERSHIP_DIR = import_node_path19.default.join("devrouter", "workspaces");
6757
7405
  }
6758
7406
  });
6759
7407
 
@@ -6767,10 +7415,10 @@ function inRepositoryWorkspaceScope(repoPath, worktreePath, livePaths) {
6767
7415
  if (livePaths.some((candidate) => sameWorkspacePath(candidate, worktreePath))) return true;
6768
7416
  const comparableRepo = comparableWorkspacePath(repoPath);
6769
7417
  const comparableWorktree = comparableWorkspacePath(worktreePath);
6770
- const localRoot = import_node_path18.default.join(comparableRepo, "trees") + import_node_path18.default.sep;
7418
+ const localRoot = import_node_path20.default.join(comparableRepo, "trees") + import_node_path20.default.sep;
6771
7419
  if (comparableWorktree.startsWith(localRoot)) return true;
6772
- const legacyPrefix = `${import_node_path18.default.basename(comparableRepo)}-`;
6773
- return import_node_path18.default.dirname(comparableWorktree) === import_node_path18.default.dirname(comparableRepo) && import_node_path18.default.basename(comparableWorktree).startsWith(legacyPrefix);
7420
+ const legacyPrefix = `${import_node_path20.default.basename(comparableRepo)}-`;
7421
+ return import_node_path20.default.dirname(comparableWorktree) === import_node_path20.default.dirname(comparableRepo) && import_node_path20.default.basename(comparableWorktree).startsWith(legacyPrefix);
6774
7422
  }
6775
7423
  function previewActions(devpodStatus, routeCount, includeRecord) {
6776
7424
  const actions = [
@@ -6886,7 +7534,7 @@ function revalidateCandidate(candidate, repoPath, transaction) {
6886
7534
  const fresh = ownedCandidate(
6887
7535
  record,
6888
7536
  listGitWorktrees(repoPath),
6889
- listDevpodWorkspaces(),
7537
+ listDevpodWorkspaces(record.worktreePath),
6890
7538
  listHostRouteState()
6891
7539
  );
6892
7540
  if (fresh.eligible) return { status: "ready", candidate: fresh, record };
@@ -6987,11 +7635,20 @@ function inspectWorkspaceGc(repo) {
6987
7635
  const repoPath = comparableWorkspacePath(resolveRepoPath(repo));
6988
7636
  const worktrees = listGitWorktrees(repoPath);
6989
7637
  const records = listWorkspaceOwnership(repoPath);
6990
- const devpods = listDevpodWorkspaces();
6991
7638
  const routes = listHostRouteState();
6992
7639
  const candidates = [
6993
- ...records.map((record) => ownedCandidate(record, worktrees, devpods, routes)),
6994
- ...legacyCandidates(repoPath, records, worktrees, devpods, routes)
7640
+ // Per-record resolution keeps mixed DevPod+Devsy fleets on the registry
7641
+ // that owns each record (snapshot reads: the apply path revalidates
7642
+ // live); legacy detection scans both registries.
7643
+ ...records.map(
7644
+ (record) => ownedCandidate(
7645
+ record,
7646
+ worktrees,
7647
+ listDevpodWorkspacesFromSnapshots(record.worktreePath),
7648
+ routes
7649
+ )
7650
+ ),
7651
+ ...legacyCandidates(repoPath, records, worktrees, listMergedRuntimeWorkspaces(), routes)
6995
7652
  ];
6996
7653
  return {
6997
7654
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -7013,11 +7670,11 @@ function applyWorkspaceGc(plan) {
7013
7670
  candidates
7014
7671
  };
7015
7672
  }
7016
- var import_node_path18;
7673
+ var import_node_path20;
7017
7674
  var init_workspace_gc = __esm({
7018
7675
  "src/core/workspace-gc.ts"() {
7019
7676
  "use strict";
7020
- import_node_path18 = __toESM(require("path"));
7677
+ import_node_path20 = __toESM(require("path"));
7021
7678
  init_devpod_mutation();
7022
7679
  init_devpod_workspaces();
7023
7680
  init_host_routes();
@@ -7087,11 +7744,11 @@ function inspectPostgresCredentials(repoPath, config) {
7087
7744
  parseErrors.push(`${app.name}: ${composeFile} (${message})`);
7088
7745
  continue;
7089
7746
  }
7090
- if (!import_node_fs19.default.existsSync(absolutePath)) {
7747
+ if (!import_node_fs21.default.existsSync(absolutePath)) {
7091
7748
  continue;
7092
7749
  }
7093
7750
  try {
7094
- const raw = import_node_fs19.default.readFileSync(absolutePath, "utf-8");
7751
+ const raw = import_node_fs21.default.readFileSync(absolutePath, "utf-8");
7095
7752
  const parsed = import_yaml5.default.parse(raw);
7096
7753
  const root = asRecord2(parsed);
7097
7754
  const services = asRecord2(root?.services);
@@ -7370,7 +8027,7 @@ async function buildDoctorReport(options = {}) {
7370
8027
  const config = runtimeConfig.config;
7371
8028
  loadedConfig = config;
7372
8029
  loadedWorkspace = runtimeConfig.workspace;
7373
- const cliVersion = true ? "0.0.40" : "0.0.0-dev";
8030
+ const cliVersion = true ? "0.0.42" : "0.0.0-dev";
7374
8031
  const configVersion = config.devrouter?.version;
7375
8032
  if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
7376
8033
  addCheck(checks, {
@@ -7400,9 +8057,9 @@ async function buildDoctorReport(options = {}) {
7400
8057
  (app) => app.docker.composeFiles.map((filePath) => ({
7401
8058
  app: app.name,
7402
8059
  filePath,
7403
- absolutePath: import_node_path19.default.resolve(repo.path, filePath)
8060
+ absolutePath: import_node_path21.default.resolve(repo.path, filePath)
7404
8061
  }))
7405
- ).filter((entry) => !import_node_fs19.default.existsSync(entry.absolutePath));
8062
+ ).filter((entry) => !import_node_fs21.default.existsSync(entry.absolutePath));
7406
8063
  addCheck(checks, {
7407
8064
  id: "repo.compose-files",
7408
8065
  level: missingComposeFiles.length === 0 ? "ok" : "error",
@@ -7445,8 +8102,8 @@ async function buildDoctorReport(options = {}) {
7445
8102
  const missingHostCwds = config.apps.filter((app) => app.runtime === "host").map((app) => ({
7446
8103
  app: app.name,
7447
8104
  cwd: app.hostRun.cwd,
7448
- absolutePath: import_node_path19.default.resolve(repo.path, app.hostRun.cwd)
7449
- })).filter((entry) => !import_node_fs19.default.existsSync(entry.absolutePath));
8105
+ absolutePath: import_node_path21.default.resolve(repo.path, app.hostRun.cwd)
8106
+ })).filter((entry) => !import_node_fs21.default.existsSync(entry.absolutePath));
7450
8107
  addCheck(checks, {
7451
8108
  id: "repo.host-cwd",
7452
8109
  level: missingHostCwds.length === 0 ? "ok" : "error",
@@ -7590,12 +8247,12 @@ async function buildDoctorReport(options = {}) {
7590
8247
  nextSteps
7591
8248
  };
7592
8249
  }
7593
- var import_node_fs19, import_node_path19, import_yaml5, POSTGRES_DEFAULTS;
8250
+ var import_node_fs21, import_node_path21, import_yaml5, POSTGRES_DEFAULTS;
7594
8251
  var init_doctor = __esm({
7595
8252
  "src/core/doctor.ts"() {
7596
8253
  "use strict";
7597
- import_node_fs19 = __toESM(require("fs"));
7598
- import_node_path19 = __toESM(require("path"));
8254
+ import_node_fs21 = __toESM(require("fs"));
8255
+ import_node_path21 = __toESM(require("path"));
7599
8256
  import_yaml5 = __toESM(require("yaml"));
7600
8257
  init_docker();
7601
8258
  init_host_routes();
@@ -7702,6 +8359,32 @@ async function runSetup(options = {}) {
7702
8359
  })
7703
8360
  );
7704
8361
  }
8362
+ if (options.workspaceRuntime !== void 0 || options.devsyInactivityTimeout !== void 0) {
8363
+ try {
8364
+ const existing = readWorkspaceRuntimeConfig();
8365
+ const next = {
8366
+ runtime: options.workspaceRuntime ? parseWorkspaceRuntime(options.workspaceRuntime) : existing.runtime,
8367
+ devsyInactivityTimeout: options.devsyInactivityTimeout ?? existing.devsyInactivityTimeout
8368
+ };
8369
+ writeWorkspaceRuntimeConfig(next);
8370
+ actions.push(
8371
+ action("performed", {
8372
+ id: "global.workspace-runtime-config",
8373
+ summary: `Persisted workspace runtime preferences (runtime=${next.runtime ?? "unset"}, devsyInactivityTimeout=${next.devsyInactivityTimeout ?? "unset"}).`
8374
+ })
8375
+ );
8376
+ } catch (error) {
8377
+ const message = error instanceof Error ? error.message : String(error);
8378
+ actions.push(
8379
+ action("failed", {
8380
+ id: "global.workspace-runtime-config",
8381
+ summary: "Failed to persist workspace runtime preferences.",
8382
+ details: message,
8383
+ suggestion: "Use: devrouter setup --yes --workspace-runtime <devpod|devsy> [--devsy-inactivity-timeout <duration>]"
8384
+ })
8385
+ );
8386
+ }
8387
+ }
7705
8388
  try {
7706
8389
  const existed = await networkExists(DEVNET_NAME);
7707
8390
  await ensureNetwork(DEVNET_NAME);
@@ -7797,6 +8480,7 @@ var init_setup = __esm({
7797
8480
  init_router();
7798
8481
  init_tls();
7799
8482
  init_tool_diagnostics();
8483
+ init_workspace_runtime();
7800
8484
  }
7801
8485
  });
7802
8486
 
@@ -7806,7 +8490,12 @@ __export(setup_exports, {
7806
8490
  runSetupCommand: () => runSetupCommand
7807
8491
  });
7808
8492
  async function runSetupCommand(options) {
7809
- const report = await runSetup({ repo: options.repo, yes: Boolean(options.yes) });
8493
+ const report = await runSetup({
8494
+ repo: options.repo,
8495
+ yes: Boolean(options.yes),
8496
+ workspaceRuntime: options.workspaceRuntime,
8497
+ devsyInactivityTimeout: options.devsyInactivityTimeout
8498
+ });
7810
8499
  if (options.json) {
7811
8500
  printJSON(report);
7812
8501
  } else {
@@ -7853,12 +8542,12 @@ function parseSsPortListeners(stdout, port) {
7853
8542
  return listeners;
7854
8543
  }
7855
8544
  function findPortListeners(port) {
7856
- const result = (0, import_node_child_process13.spawnSync)("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN"], {
8545
+ const result = (0, import_node_child_process17.spawnSync)("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN"], {
7857
8546
  encoding: "utf-8"
7858
8547
  });
7859
8548
  if (result.error && result.error.code === "ENOENT") {
7860
8549
  if (process.platform === "linux") {
7861
- const ssResult = (0, import_node_child_process13.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
8550
+ const ssResult = (0, import_node_child_process17.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
7862
8551
  if (ssResult.status === 0 && ssResult.stdout) {
7863
8552
  return parseSsPortListeners(ssResult.stdout, port);
7864
8553
  }
@@ -7887,11 +8576,11 @@ function findPortListeners(port) {
7887
8576
  };
7888
8577
  });
7889
8578
  }
7890
- var import_node_child_process13;
8579
+ var import_node_child_process17;
7891
8580
  var init_ports = __esm({
7892
8581
  "src/util/ports.ts"() {
7893
8582
  "use strict";
7894
- import_node_child_process13 = require("child_process");
8583
+ import_node_child_process17 = require("child_process");
7895
8584
  }
7896
8585
  });
7897
8586
 
@@ -7968,18 +8657,18 @@ function probeHttpRoute(host, options = {}) {
7968
8657
  args.push("--cacert", CERT_FILE);
7969
8658
  }
7970
8659
  args.push(url);
7971
- const result = (0, import_node_child_process14.spawnSync)("curl", args, { encoding: "utf-8" });
8660
+ const result = (0, import_node_child_process18.spawnSync)("curl", args, { encoding: "utf-8" });
7972
8661
  const stdout = result.stdout?.trim() ?? "";
7973
8662
  const status = /^\d{3}$/.test(stdout) ? Number(stdout) : void 0;
7974
8663
  const ok = result.status === 0 && status !== void 0 && status >= 100 && status < 500;
7975
8664
  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"}`;
7976
8665
  return { ok, status, details };
7977
8666
  }
7978
- var import_node_child_process14;
8667
+ var import_node_child_process18;
7979
8668
  var init_http_route_probe = __esm({
7980
8669
  "src/core/http-route-probe.ts"() {
7981
8670
  "use strict";
7982
- import_node_child_process14 = require("child_process");
8671
+ import_node_child_process18 = require("child_process");
7983
8672
  init_router();
7984
8673
  init_tls();
7985
8674
  }
@@ -8058,11 +8747,11 @@ var init_route_publication = __esm({
8058
8747
  // src/core/workspace-ensure.ts
8059
8748
  function assertOverlay(container, repoPath) {
8060
8749
  const workingDir = container.labels["com.docker.compose.project.working_dir"];
8061
- if (!workingDir || !sameWorkspacePath(workingDir, import_node_path20.default.join(repoPath, ".devcontainer"))) {
8750
+ if (!workingDir || !sameWorkspacePath(workingDir, import_node_path22.default.join(repoPath, ".devcontainer"))) {
8062
8751
  throw new Error(`Container '${container.id}' does not belong to the exact worktree.`);
8063
8752
  }
8064
8753
  const configFiles = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").filter(Boolean);
8065
- const expectedOverlay = import_node_path20.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
8754
+ const expectedOverlay = import_node_path22.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
8066
8755
  if (!configFiles.some((configFile) => sameWorkspacePath(configFile, expectedOverlay))) {
8067
8756
  throw new Error(`Container '${container.id}' was not started with ${DEVCONTAINER_OVERLAY}.`);
8068
8757
  }
@@ -8339,7 +9028,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
8339
9028
  target
8340
9029
  });
8341
9030
  if (target.kind === "linked") {
8342
- const workspaceEnv = (0, import_node_child_process15.spawnSync)(
9031
+ const workspaceEnv = (0, import_node_child_process19.spawnSync)(
8343
9032
  "docker",
8344
9033
  ["exec", appContainer.id, "printenv", "WORKSPACE"],
8345
9034
  { encoding: "utf-8" }
@@ -8349,7 +9038,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
8349
9038
  `Workspace app container must expose WORKSPACE='${target.workspace}' (got '${workspaceEnv.stdout.trim() || "(empty)"}').`
8350
9039
  );
8351
9040
  }
8352
- const devrouterWorkspaceEnv = (0, import_node_child_process15.spawnSync)(
9041
+ const devrouterWorkspaceEnv = (0, import_node_child_process19.spawnSync)(
8353
9042
  "docker",
8354
9043
  ["exec", appContainer.id, "printenv", "DEVROUTER_WORKSPACE"],
8355
9044
  { encoding: "utf-8" }
@@ -8360,7 +9049,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
8360
9049
  );
8361
9050
  }
8362
9051
  }
8363
- const gitCheck = (0, import_node_child_process15.spawnSync)(
9052
+ const gitCheck = (0, import_node_child_process19.spawnSync)(
8364
9053
  "docker",
8365
9054
  [
8366
9055
  "exec",
@@ -8414,7 +9103,7 @@ async function waitForHttpRoutes(repoPath, apps, timeoutMs) {
8414
9103
  );
8415
9104
  }
8416
9105
  function resolveLinkedTarget(repoPath) {
8417
- const devpods = listDevpodWorkspaces();
9106
+ const devpods = listDevpodWorkspaces(repoPath);
8418
9107
  const existingDevpod = selectDevpodWorkspace(devpods, repoPath);
8419
9108
  const persisted = readPersistedWorkspace(repoPath);
8420
9109
  const candidate = existingDevpod?.id ?? persisted ?? resolveWorktreeWorkspace(repoPath);
@@ -8438,7 +9127,7 @@ function resolveLinkedTarget(repoPath) {
8438
9127
  };
8439
9128
  }
8440
9129
  function resolvePrimaryTarget(repoPath) {
8441
- const existingDevpod = selectDevpodWorkspace(listDevpodWorkspaces(), repoPath);
9130
+ const existingDevpod = selectDevpodWorkspace(listDevpodWorkspaces(repoPath), repoPath);
8442
9131
  return {
8443
9132
  kind: "primary",
8444
9133
  devpodId: existingDevpod?.id,
@@ -8447,14 +9136,14 @@ function resolvePrimaryTarget(repoPath) {
8447
9136
  }
8448
9137
  function isPrimaryCheckout(repoPath) {
8449
9138
  try {
8450
- return import_node_fs20.default.statSync(import_node_path20.default.join(repoPath, ".git")).isDirectory();
9139
+ return import_node_fs22.default.statSync(import_node_path22.default.join(repoPath, ".git")).isDirectory();
8451
9140
  } catch {
8452
9141
  return false;
8453
9142
  }
8454
9143
  }
8455
9144
  function openUrls(urls) {
8456
9145
  for (const url of urls) {
8457
- const opened = (0, import_node_child_process15.spawnSync)("open", [url], { encoding: "utf-8" });
9146
+ const opened = (0, import_node_child_process19.spawnSync)("open", [url], { encoding: "utf-8" });
8458
9147
  if (opened.status !== 0) {
8459
9148
  process.stderr.write(`Warning: could not open '${url}'.
8460
9149
  `);
@@ -8497,8 +9186,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
8497
9186
  const target = linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
8498
9187
  let devpodId = target.devpodId;
8499
9188
  if (target.kind === "linked") {
8500
- const overlayPath = import_node_path20.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
8501
- if (!import_node_fs20.default.existsSync(overlayPath)) {
9189
+ const overlayPath = import_node_path22.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
9190
+ if (!import_node_fs22.default.existsSync(overlayPath)) {
8502
9191
  throw new Error(`Missing required DevPod compose overlay: ${overlayPath}`);
8503
9192
  }
8504
9193
  }
@@ -8565,7 +9254,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
8565
9254
  }
8566
9255
  const apps = proxyAppsFromConfig(runtime.config);
8567
9256
  const parsedUpstreams = apps.map((app) => parseUpstream(app.upstream));
8568
- const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path20.default.basename(repoPath)) ?? "app";
9257
+ const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path22.default.basename(repoPath)) ?? "app";
8569
9258
  for (const [index, app] of apps.entries()) {
8570
9259
  if (!parsedUpstreams[index].host.startsWith(`${aliasPrefix}-`)) {
8571
9260
  const owner = target.kind === "linked" ? "workspace" : "checkout";
@@ -9009,13 +9698,13 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
9009
9698
  }
9010
9699
  });
9011
9700
  }
9012
- var import_node_child_process15, import_node_fs20, import_node_path20, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
9701
+ var import_node_child_process19, import_node_fs22, import_node_path22, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
9013
9702
  var init_workspace_ensure = __esm({
9014
9703
  "src/core/workspace-ensure.ts"() {
9015
9704
  "use strict";
9016
- import_node_child_process15 = require("child_process");
9017
- import_node_fs20 = __toESM(require("fs"));
9018
- import_node_path20 = __toESM(require("path"));
9705
+ import_node_child_process19 = require("child_process");
9706
+ import_node_fs22 = __toESM(require("fs"));
9707
+ import_node_path22 = __toESM(require("path"));
9019
9708
  init_devcontainer_profile();
9020
9709
  init_devpod_environment();
9021
9710
  init_devpod_mutation();
@@ -9095,20 +9784,20 @@ function warnMissingWorkspaceOwnership(repoPath) {
9095
9784
  );
9096
9785
  }
9097
9786
  function defaultWorktreePath(mainRepo, ws) {
9098
- return import_node_path21.default.join(mainRepo, "trees", ws);
9787
+ return import_node_path23.default.join(mainRepo, "trees", ws);
9099
9788
  }
9100
9789
  function assertDefaultWorktreeRootIgnored(mainRepo) {
9101
- const ignored = (0, import_node_child_process16.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
9790
+ const ignored = (0, import_node_child_process20.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
9102
9791
  encoding: "utf-8"
9103
9792
  });
9104
9793
  if (ignored.status !== 0) {
9105
9794
  throw new Error(
9106
- `Default worktree root '${import_node_path21.default.join(mainRepo, "trees")}' is not ignored. Add 'trees/' to '${import_node_path21.default.join(mainRepo, ".gitignore")}' or use --path.`
9795
+ `Default worktree root '${import_node_path23.default.join(mainRepo, "trees")}' is not ignored. Add 'trees/' to '${import_node_path23.default.join(mainRepo, ".gitignore")}' or use --path.`
9107
9796
  );
9108
9797
  }
9109
9798
  }
9110
9799
  function legacyDefaultWorktreePath(mainRepo, ws) {
9111
- return import_node_path21.default.join(import_node_path21.default.dirname(mainRepo), `${import_node_path21.default.basename(mainRepo)}-${ws}`);
9800
+ return import_node_path23.default.join(import_node_path23.default.dirname(mainRepo), `${import_node_path23.default.basename(mainRepo)}-${ws}`);
9112
9801
  }
9113
9802
  function teardownFallbackPath(mainRepo, workspace) {
9114
9803
  const candidates = [
@@ -9220,13 +9909,13 @@ function assertFullDownPreflight(mainRepo, target) {
9220
9909
  if (sameWorkspacePath(target.worktreePath, mainRepo)) {
9221
9910
  throw new Error("Refusing to remove the primary Git checkout.");
9222
9911
  }
9223
- if (!target.worktree || target.worktree.prunable || !import_node_fs21.default.existsSync(target.worktreePath)) return;
9912
+ if (!target.worktree || target.worktree.prunable || !import_node_fs23.default.existsSync(target.worktreePath)) return;
9224
9913
  if (target.worktree.locked) {
9225
9914
  throw new Error(
9226
9915
  `Worktree '${target.worktreePath}' is locked; unlock it before workspace down.`
9227
9916
  );
9228
9917
  }
9229
- const status = (0, import_node_child_process16.spawnSync)(
9918
+ const status = (0, import_node_child_process20.spawnSync)(
9230
9919
  "git",
9231
9920
  ["-C", target.worktreePath, "status", "--porcelain", "--untracked-files=normal"],
9232
9921
  { encoding: "utf-8" }
@@ -9247,8 +9936,8 @@ async function workspaceUp(branch, opts = {}) {
9247
9936
  if (!ws) {
9248
9937
  throw new Error(`Branch '${branch}' does not yield a valid workspace token.`);
9249
9938
  }
9250
- const worktreePath = opts.path ? import_node_path21.default.resolve(opts.path) : defaultWorktreePath(mainRepo, ws);
9251
- if (import_node_fs21.default.existsSync(worktreePath)) {
9939
+ const worktreePath = opts.path ? import_node_path23.default.resolve(opts.path) : defaultWorktreePath(mainRepo, ws);
9940
+ if (import_node_fs23.default.existsSync(worktreePath)) {
9252
9941
  const registered = listGitWorktrees(mainRepo).find(
9253
9942
  (worktree) => sameWorkspacePath(worktree.path, worktreePath)
9254
9943
  );
@@ -9266,11 +9955,11 @@ async function workspaceUp(branch, opts = {}) {
9266
9955
  if (!opts.path) {
9267
9956
  assertDefaultWorktreeRootIgnored(mainRepo);
9268
9957
  }
9269
- const add = (0, import_node_child_process16.spawnSync)("git", ["-C", mainRepo, "worktree", "add", worktreePath, branch], {
9958
+ const add = (0, import_node_child_process20.spawnSync)("git", ["-C", mainRepo, "worktree", "add", worktreePath, branch], {
9270
9959
  encoding: "utf-8"
9271
9960
  });
9272
9961
  if (add.status !== 0) {
9273
- const addNew = (0, import_node_child_process16.spawnSync)(
9962
+ const addNew = (0, import_node_child_process20.spawnSync)(
9274
9963
  "git",
9275
9964
  ["-C", mainRepo, "worktree", "add", "-b", branch, worktreePath],
9276
9965
  {
@@ -9304,12 +9993,9 @@ function workspaceLs(repoPath) {
9304
9993
  const mainRepo = resolveRepoPath(repoPath);
9305
9994
  const worktrees = identifyGitWorktrees(mainRepo);
9306
9995
  const records = listWorkspaceOwnership(mainRepo);
9307
- let devpods;
9308
- try {
9309
- devpods = listDevpodWorkspaces();
9310
- } catch {
9311
- devpods = void 0;
9312
- }
9996
+ const devpodsForPath = (worktreePath) => {
9997
+ return listDevpodWorkspacesFromSnapshots(worktreePath);
9998
+ };
9313
9999
  const paths = Array.from(
9314
10000
  /* @__PURE__ */ new Set([
9315
10001
  ...worktrees.map((worktree) => worktree.path),
@@ -9322,6 +10008,7 @@ function workspaceLs(repoPath) {
9322
10008
  return paths.map((worktreePath) => {
9323
10009
  const worktree = worktreesByPath.get(worktreePath);
9324
10010
  const record = recordsByPath.get(worktreePath);
10011
+ const devpods = devpodsForPath(worktreePath);
9325
10012
  const status = record ? inspectWorkspaceOwnership(record, worktrees, devpods) : void 0;
9326
10013
  const matchingDevpods = devpods?.filter(
9327
10014
  (devpod) => sameWorkspacePath(devpod.source.localFolder, worktreePath)
@@ -9340,7 +10027,7 @@ function workspaceLs(repoPath) {
9340
10027
  });
9341
10028
  }
9342
10029
  function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = false) {
9343
- const devpods = listDevpodWorkspaces();
10030
+ const devpods = listDevpodWorkspaces(resolved.worktreePath);
9344
10031
  assertDevpodTargetSafe(resolved, worktrees, devpods);
9345
10032
  const devpodId = resolved.record?.devpodId ?? resolved.workspace;
9346
10033
  const mutation = action2 === "stop" ? stopOwnedDevpodWorkspace(devpodId, resolved.worktreePath) : deleteOwnedDevpodWorkspace(devpodId, resolved.worktreePath);
@@ -9375,8 +10062,8 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
9375
10062
  opts.quiet
9376
10063
  );
9377
10064
  if (removeWorktree) {
9378
- if (resolved.worktree && !resolved.worktree.prunable && import_node_fs21.default.existsSync(resolved.worktreePath)) {
9379
- const rm = (0, import_node_child_process16.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
10065
+ if (resolved.worktree && !resolved.worktree.prunable && import_node_fs23.default.existsSync(resolved.worktreePath)) {
10066
+ const rm = (0, import_node_child_process20.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
9380
10067
  encoding: "utf-8"
9381
10068
  });
9382
10069
  if (rm.status !== 0) {
@@ -9394,7 +10081,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
9394
10081
  }
9395
10082
  return result;
9396
10083
  };
9397
- return resolved.worktree && !resolved.worktree.prunable && import_node_fs21.default.existsSync(resolved.worktreePath) ? withWorkspaceLifecycleLock(resolved.worktreePath, operation) : operation();
10084
+ return resolved.worktree && !resolved.worktree.prunable && import_node_fs23.default.existsSync(resolved.worktreePath) ? withWorkspaceLifecycleLock(resolved.worktreePath, operation) : operation();
9398
10085
  }
9399
10086
  async function mutateWorkspaceOwnedPath(action2, worktreePath, opts = {}) {
9400
10087
  const mainRepo = resolveRepoPath(opts.repoPath);
@@ -9429,13 +10116,13 @@ async function workspaceStop(target, opts = {}) {
9429
10116
  async function workspaceDown(target, opts = {}) {
9430
10117
  return runWorkspaceLifecycle("down", target, opts);
9431
10118
  }
9432
- var import_node_child_process16, import_node_fs21, import_node_path21;
10119
+ var import_node_child_process20, import_node_fs23, import_node_path23;
9433
10120
  var init_workspace_lifecycle = __esm({
9434
10121
  "src/core/workspace-lifecycle.ts"() {
9435
10122
  "use strict";
9436
- import_node_child_process16 = require("child_process");
9437
- import_node_fs21 = __toESM(require("fs"));
9438
- import_node_path21 = __toESM(require("path"));
10123
+ import_node_child_process20 = require("child_process");
10124
+ import_node_fs23 = __toESM(require("fs"));
10125
+ import_node_path23 = __toESM(require("path"));
9439
10126
  init_devpod_mutation();
9440
10127
  init_devpod_workspaces();
9441
10128
  init_repo_config();
@@ -9485,7 +10172,7 @@ async function environmentStop(repoPath, options = {}) {
9485
10172
  }
9486
10173
  }
9487
10174
  return withWorkspaceLifecycleLock(repoPath, async () => {
9488
- const devpod = selectDevpodWorkspace(listDevpodWorkspaces(), repoPath);
10175
+ const devpod = selectDevpodWorkspace(listDevpodWorkspaces(repoPath), repoPath);
9489
10176
  const mutation = devpod ? options.delete ? deleteOwnedDevpodWorkspace(devpod.id, repoPath) : stopOwnedDevpodWorkspace(devpod.id, repoPath) : { status: "absent" };
9490
10177
  const removedRoutes = removeHostRoutesWhere(
9491
10178
  (route) => sameWorkspacePath(route.repoPath, repoPath)
@@ -9546,6 +10233,39 @@ var init_stop = __esm({
9546
10233
  }
9547
10234
  });
9548
10235
 
10236
+ // src/core/devsy-exec.ts
10237
+ function ensureGuidance(repoPath) {
10238
+ return `Run 'devrouter ensure ${repoPath}' first.`;
10239
+ }
10240
+ async function devsyExec(repoPath, command) {
10241
+ if (command.length === 0) {
10242
+ throw new Error("No command provided. Use `devrouter exec [path] -- <command...>`.");
10243
+ }
10244
+ return withWorkspaceLifecycleLock(repoPath, async () => {
10245
+ const workspace = selectDevsyWorkspace(listDevsyWorkspaces(), repoPath);
10246
+ if (!workspace) {
10247
+ throw new Error(
10248
+ `No exact Devsy workspace exists for '${repoPath}'. ${ensureGuidance(repoPath)}`
10249
+ );
10250
+ }
10251
+ const args = ["workspace", "exec", "--result-format", "plain", workspace.id, "--", ...command];
10252
+ return new Promise((resolve, reject) => {
10253
+ const child = (0, import_node_child_process21.spawn)("devsy", args, { stdio: "inherit" });
10254
+ child.once("error", (error) => reject(new Error(`devsy exec failed: ${error.message}`)));
10255
+ child.once("close", (code) => resolve(code ?? 1));
10256
+ });
10257
+ });
10258
+ }
10259
+ var import_node_child_process21;
10260
+ var init_devsy_exec = __esm({
10261
+ "src/core/devsy-exec.ts"() {
10262
+ "use strict";
10263
+ import_node_child_process21 = require("child_process");
10264
+ init_devsy_workspaces();
10265
+ init_workspace();
10266
+ }
10267
+ });
10268
+
9549
10269
  // src/core/devpod-exec.ts
9550
10270
  function stripDevpodCompletionDiagnostic(value) {
9551
10271
  const index = value.indexOf(DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC);
@@ -9558,27 +10278,27 @@ function stripDevpodCompletionDiagnostic(value) {
9558
10278
  function quotePosixArg(value) {
9559
10279
  return `'${value.replaceAll("'", `'"'"'`)}'`;
9560
10280
  }
9561
- function ensureGuidance(repoPath) {
10281
+ function ensureGuidance2(repoPath) {
9562
10282
  return `Run 'devrouter ensure ${repoPath}' first.`;
9563
10283
  }
9564
10284
  function assertRunningDevpod(devpodId, repoPath) {
9565
- const result = (0, import_node_child_process17.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
10285
+ const result = (0, import_node_child_process22.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
9566
10286
  encoding: "utf-8"
9567
10287
  });
9568
10288
  if (result.status !== 0) {
9569
10289
  const details = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
9570
10290
  throw new Error(
9571
- `Could not prove DevPod '${devpodId}' is running${details ? `: ${details}` : "."} ${ensureGuidance(repoPath)}`
10291
+ `Could not prove DevPod '${devpodId}' is running${details ? `: ${details}` : "."} ${ensureGuidance2(repoPath)}`
9572
10292
  );
9573
10293
  }
9574
10294
  let status;
9575
10295
  try {
9576
10296
  status = JSON.parse(result.stdout);
9577
10297
  } catch {
9578
- throw new Error(`DevPod '${devpodId}' returned invalid status. ${ensureGuidance(repoPath)}`);
10298
+ throw new Error(`DevPod '${devpodId}' returned invalid status. ${ensureGuidance2(repoPath)}`);
9579
10299
  }
9580
10300
  if (status.id !== devpodId || status.state !== "Running") {
9581
- throw new Error(`DevPod '${devpodId}' is not running. ${ensureGuidance(repoPath)}`);
10301
+ throw new Error(`DevPod '${devpodId}' is not running. ${ensureGuidance2(repoPath)}`);
9582
10302
  }
9583
10303
  }
9584
10304
  function resolveWorkspaceDirectory(repoPath) {
@@ -9586,17 +10306,20 @@ function resolveWorkspaceDirectory(repoPath) {
9586
10306
  return resolveRunningWorkspaceContainer(repoPath).workspacePath;
9587
10307
  } catch (error) {
9588
10308
  const message = error instanceof Error ? error.message : String(error);
9589
- throw new Error(`${message} ${ensureGuidance(repoPath)}`);
10309
+ throw new Error(`${message} ${ensureGuidance2(repoPath)}`);
9590
10310
  }
9591
10311
  }
9592
10312
  async function devpodExec(repoPath, command) {
10313
+ if (resolveWorkspaceRuntimeOrDefault(repoPath) === "devsy") {
10314
+ return devsyExec(repoPath, command);
10315
+ }
9593
10316
  if (command.length === 0) {
9594
10317
  throw new Error("No command provided. Use `devrouter exec [path] -- <command...>`.");
9595
10318
  }
9596
10319
  return withWorkspaceLifecycleLock(repoPath, async () => {
9597
- const devpod = selectDevpodWorkspace(listDevpodWorkspaces(), repoPath);
10320
+ const devpod = selectDevpodWorkspace(listDevpodWorkspaces(repoPath), repoPath);
9598
10321
  if (!devpod) {
9599
- throw new Error(`No exact DevPod exists for '${repoPath}'. ${ensureGuidance(repoPath)}`);
10322
+ throw new Error(`No exact DevPod exists for '${repoPath}'. ${ensureGuidance2(repoPath)}`);
9600
10323
  }
9601
10324
  assertRunningDevpod(devpod.id, repoPath);
9602
10325
  const workspaceDirectory = resolveWorkspaceDirectory(repoPath);
@@ -9618,7 +10341,7 @@ async function devpodExec(repoPath, command) {
9618
10341
  wrappedCommand
9619
10342
  ];
9620
10343
  return new Promise((resolve, reject) => {
9621
- const child = (0, import_node_child_process17.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
10344
+ const child = (0, import_node_child_process22.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
9622
10345
  let pending = Buffer.alloc(0);
9623
10346
  let statusBytes = Buffer.alloc(0);
9624
10347
  let readingStatus = false;
@@ -9677,15 +10400,17 @@ async function devpodExec(repoPath, command) {
9677
10400
  });
9678
10401
  });
9679
10402
  }
9680
- var import_node_child_process17, import_node_crypto7, DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC;
10403
+ var import_node_child_process22, import_node_crypto7, DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC;
9681
10404
  var init_devpod_exec = __esm({
9682
10405
  "src/core/devpod-exec.ts"() {
9683
10406
  "use strict";
9684
- import_node_child_process17 = require("child_process");
10407
+ import_node_child_process22 = require("child_process");
9685
10408
  import_node_crypto7 = require("crypto");
9686
10409
  init_devpod_environment();
9687
10410
  init_devpod_workspaces();
10411
+ init_devsy_exec();
9688
10412
  init_workspace();
10413
+ init_workspace_runtime();
9689
10414
  DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC = Buffer.from(
9690
10415
  "Error tunneling to container: wait: remote command exited without exit status or exit signal\n"
9691
10416
  );
@@ -9866,7 +10591,7 @@ async function runOpenCommand(name) {
9866
10591
  );
9867
10592
  return;
9868
10593
  }
9869
- const result = (0, import_node_child_process18.spawnSync)("open", [url], { encoding: "utf-8" });
10594
+ const result = (0, import_node_child_process23.spawnSync)("open", [url], { encoding: "utf-8" });
9870
10595
  if (result.status !== 0) {
9871
10596
  const details = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
9872
10597
  throw new Error(`Unable to open '${url}': ${details || "unknown error"}`);
@@ -9874,11 +10599,11 @@ async function runOpenCommand(name) {
9874
10599
  process.stdout.write(`Opened ${url}
9875
10600
  `);
9876
10601
  }
9877
- var import_node_child_process18;
10602
+ var import_node_child_process23;
9878
10603
  var init_open = __esm({
9879
10604
  "src/commands/open.ts"() {
9880
10605
  "use strict";
9881
- import_node_child_process18 = require("child_process");
10606
+ import_node_child_process23 = require("child_process");
9882
10607
  init_docker();
9883
10608
  init_host_routes();
9884
10609
  init_repo_config();
@@ -9901,7 +10626,7 @@ async function runLogsCommand(options) {
9901
10626
  const args = ["logs", "--tail", tail, ROUTER_CONTAINER_NAME];
9902
10627
  if (options.follow) {
9903
10628
  args.splice(1, 0, "-f");
9904
- const child = (0, import_node_child_process19.spawn)("docker", args, { stdio: "inherit" });
10629
+ const child = (0, import_node_child_process24.spawn)("docker", args, { stdio: "inherit" });
9905
10630
  const onSignal = () => {
9906
10631
  child.kill("SIGTERM");
9907
10632
  };
@@ -9915,17 +10640,17 @@ async function runLogsCommand(options) {
9915
10640
  });
9916
10641
  });
9917
10642
  } else {
9918
- const result = (0, import_node_child_process19.spawnSync)("docker", args, { stdio: "inherit" });
10643
+ const result = (0, import_node_child_process24.spawnSync)("docker", args, { stdio: "inherit" });
9919
10644
  if (result.status !== 0) {
9920
10645
  throw new Error("Failed to retrieve router logs.");
9921
10646
  }
9922
10647
  }
9923
10648
  }
9924
- var import_node_child_process19;
10649
+ var import_node_child_process24;
9925
10650
  var init_logs = __esm({
9926
10651
  "src/commands/logs.ts"() {
9927
10652
  "use strict";
9928
- import_node_child_process19 = require("child_process");
10653
+ import_node_child_process24 = require("child_process");
9929
10654
  init_docker();
9930
10655
  init_router();
9931
10656
  }
@@ -9965,17 +10690,17 @@ function asRecord3(value) {
9965
10690
  return value;
9966
10691
  }
9967
10692
  function readJson(filePath) {
9968
- if (!import_node_fs22.default.existsSync(filePath)) {
10693
+ if (!import_node_fs24.default.existsSync(filePath)) {
9969
10694
  return void 0;
9970
10695
  }
9971
10696
  try {
9972
- return JSON.parse(import_node_fs22.default.readFileSync(filePath, "utf-8"));
10697
+ return JSON.parse(import_node_fs24.default.readFileSync(filePath, "utf-8"));
9973
10698
  } catch {
9974
10699
  return void 0;
9975
10700
  }
9976
10701
  }
9977
10702
  function relative(repoPath, filePath) {
9978
- return import_node_path22.default.relative(repoPath, filePath) || ".";
10703
+ return import_node_path24.default.relative(repoPath, filePath) || ".";
9979
10704
  }
9980
10705
  function redactEnvAssignments(value) {
9981
10706
  return value.replace(
@@ -10016,7 +10741,7 @@ function inspectPackageManager(repoPath, pkg) {
10016
10741
  ["bun.lock", "bun"]
10017
10742
  ];
10018
10743
  for (const [fileName, name] of lockfiles) {
10019
- if (import_node_fs22.default.existsSync(import_node_path22.default.join(repoPath, fileName))) {
10744
+ if (import_node_fs24.default.existsSync(import_node_path24.default.join(repoPath, fileName))) {
10020
10745
  return { name, source: fileName };
10021
10746
  }
10022
10747
  }
@@ -10031,9 +10756,9 @@ function inspectNode(repoPath, pkg) {
10031
10756
  if (typeof engines?.node === "string") {
10032
10757
  return { version: engines.node, source: "package.json:engines.node" };
10033
10758
  }
10034
- const nvmrc = import_node_path22.default.join(repoPath, ".nvmrc");
10035
- if (import_node_fs22.default.existsSync(nvmrc)) {
10036
- const version = import_node_fs22.default.readFileSync(nvmrc, "utf-8").trim();
10759
+ const nvmrc = import_node_path24.default.join(repoPath, ".nvmrc");
10760
+ if (import_node_fs24.default.existsSync(nvmrc)) {
10761
+ const version = import_node_fs24.default.readFileSync(nvmrc, "utf-8").trim();
10037
10762
  return { version, source: ".nvmrc" };
10038
10763
  }
10039
10764
  return void 0;
@@ -10094,7 +10819,7 @@ function configuredComposeFiles(repoPath) {
10094
10819
  const files = config.apps.filter(
10095
10820
  (app) => app.runtime === "docker"
10096
10821
  ).flatMap((app) => app.docker.composeFiles).filter(
10097
- (fileName) => !import_node_path22.default.isAbsolute(fileName) && !import_node_path22.default.normalize(fileName).startsWith("..")
10822
+ (fileName) => !import_node_path24.default.isAbsolute(fileName) && !import_node_path24.default.normalize(fileName).startsWith("..")
10098
10823
  );
10099
10824
  return Array.from(new Set(files));
10100
10825
  } catch {
@@ -10112,7 +10837,7 @@ function composeFiles(repoPath) {
10112
10837
  ...configuredComposeFiles(repoPath)
10113
10838
  ];
10114
10839
  return Array.from(new Set(candidates)).filter(
10115
- (fileName) => import_node_fs22.default.existsSync(import_node_path22.default.join(repoPath, fileName))
10840
+ (fileName) => import_node_fs24.default.existsSync(import_node_path24.default.join(repoPath, fileName))
10116
10841
  );
10117
10842
  }
10118
10843
  function stringArray2(value) {
@@ -10150,7 +10875,7 @@ function inspectServices(repoPath) {
10150
10875
  const services = [];
10151
10876
  for (const fileName of composeFiles(repoPath)) {
10152
10877
  try {
10153
- const parsed = import_yaml6.default.parse(import_node_fs22.default.readFileSync(import_node_path22.default.join(repoPath, fileName), "utf-8"));
10878
+ const parsed = import_yaml6.default.parse(import_node_fs24.default.readFileSync(import_node_path24.default.join(repoPath, fileName), "utf-8"));
10154
10879
  const serviceMap = asRecord3(asRecord3(parsed)?.services);
10155
10880
  for (const [name, value] of Object.entries(serviceMap ?? {})) {
10156
10881
  const service = asRecord3(value);
@@ -10185,8 +10910,8 @@ function inspectServices(repoPath) {
10185
10910
  return services;
10186
10911
  }
10187
10912
  function inspectEnvFiles(repoPath) {
10188
- const files = import_node_fs22.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
10189
- const content = import_node_fs22.default.readFileSync(import_node_path22.default.join(repoPath, fileName), "utf-8");
10913
+ const files = import_node_fs24.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
10914
+ const content = import_node_fs24.default.readFileSync(import_node_path24.default.join(repoPath, fileName), "utf-8");
10190
10915
  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();
10191
10916
  return { path: fileName, names };
10192
10917
  });
@@ -10200,18 +10925,18 @@ function inspectEnvFiles(repoPath) {
10200
10925
  };
10201
10926
  }
10202
10927
  function inspectDevcontainer(repoPath) {
10203
- const dir = import_node_path22.default.join(repoPath, ".devcontainer");
10204
- if (!import_node_fs22.default.existsSync(dir)) {
10928
+ const dir = import_node_path24.default.join(repoPath, ".devcontainer");
10929
+ if (!import_node_fs24.default.existsSync(dir)) {
10205
10930
  return { exists: false, files: [] };
10206
10931
  }
10207
10932
  return {
10208
10933
  exists: true,
10209
- files: import_node_fs22.default.readdirSync(dir).filter((fileName) => import_node_fs22.default.statSync(import_node_path22.default.join(dir, fileName)).isFile()).sort().map((fileName) => `.devcontainer/${fileName}`)
10934
+ files: import_node_fs24.default.readdirSync(dir).filter((fileName) => import_node_fs24.default.statSync(import_node_path24.default.join(dir, fileName)).isFile()).sort().map((fileName) => `.devcontainer/${fileName}`)
10210
10935
  };
10211
10936
  }
10212
10937
  function inspectDevrouter(repoPath) {
10213
10938
  const configPath = getRepoConfigPath(repoPath);
10214
- if (!import_node_fs22.default.existsSync(configPath)) {
10939
+ if (!import_node_fs24.default.existsSync(configPath)) {
10215
10940
  return {
10216
10941
  exists: false,
10217
10942
  configPath,
@@ -10255,15 +10980,15 @@ function inspectAgentGuidance(repoPath) {
10255
10980
  ["AGENTS.md", "agents"],
10256
10981
  ["CLAUDE.md", "claude"]
10257
10982
  ]) {
10258
- if (import_node_fs22.default.existsSync(import_node_path22.default.join(repoPath, fileName))) {
10983
+ if (import_node_fs24.default.existsSync(import_node_path24.default.join(repoPath, fileName))) {
10259
10984
  results.push({ path: fileName, kind });
10260
10985
  }
10261
10986
  }
10262
- const skillsDir = import_node_path22.default.join(repoPath, ".agents", "skills");
10263
- if (import_node_fs22.default.existsSync(skillsDir)) {
10264
- for (const name of import_node_fs22.default.readdirSync(skillsDir).sort()) {
10265
- const skillPath = import_node_path22.default.join(skillsDir, name, "SKILL.md");
10266
- if (import_node_fs22.default.existsSync(skillPath)) {
10987
+ const skillsDir = import_node_path24.default.join(repoPath, ".agents", "skills");
10988
+ if (import_node_fs24.default.existsSync(skillsDir)) {
10989
+ for (const name of import_node_fs24.default.readdirSync(skillsDir).sort()) {
10990
+ const skillPath = import_node_path24.default.join(skillsDir, name, "SKILL.md");
10991
+ if (import_node_fs24.default.existsSync(skillPath)) {
10267
10992
  results.push({ path: relative(repoPath, skillPath), kind: "skill" });
10268
10993
  }
10269
10994
  }
@@ -10306,7 +11031,7 @@ function buildIssues(report) {
10306
11031
  }
10307
11032
  function inspectRepo(options = {}) {
10308
11033
  const repoPath = resolveRepoPath(options.repo);
10309
- const pkg = readJson(import_node_path22.default.join(repoPath, "package.json"));
11034
+ const pkg = readJson(import_node_path24.default.join(repoPath, "package.json"));
10310
11035
  const scripts = inspectScripts(pkg);
10311
11036
  const reportWithoutIssues = {
10312
11037
  repoPath,
@@ -10325,12 +11050,12 @@ function inspectRepo(options = {}) {
10325
11050
  issues: buildIssues(reportWithoutIssues)
10326
11051
  };
10327
11052
  }
10328
- var import_node_fs22, import_node_path22, import_yaml6;
11053
+ var import_node_fs24, import_node_path24, import_yaml6;
10329
11054
  var init_repo_inspect = __esm({
10330
11055
  "src/core/repo-inspect.ts"() {
10331
11056
  "use strict";
10332
- import_node_fs22 = __toESM(require("fs"));
10333
- import_node_path22 = __toESM(require("path"));
11057
+ import_node_fs24 = __toESM(require("fs"));
11058
+ import_node_path24 = __toESM(require("path"));
10334
11059
  import_yaml6 = __toESM(require("yaml"));
10335
11060
  init_repo_config();
10336
11061
  }
@@ -10435,7 +11160,7 @@ function requiredFileChecks(repoPath) {
10435
11160
  ".devcontainer/docker-compose.yml",
10436
11161
  ".devrouter.yml"
10437
11162
  ];
10438
- const missing = required.filter((fileName) => !import_node_fs23.default.existsSync(import_node_path23.default.join(repoPath, fileName)));
11163
+ const missing = required.filter((fileName) => !import_node_fs25.default.existsSync(import_node_path25.default.join(repoPath, fileName)));
10439
11164
  return {
10440
11165
  id: "repo.devcontainer.verify-files",
10441
11166
  level: missing.length === 0 ? "ok" : "error",
@@ -10654,12 +11379,12 @@ async function verifyDevcontainer(options = {}) {
10654
11379
  nextSteps: collectNextSteps3(checks)
10655
11380
  };
10656
11381
  }
10657
- var import_node_fs23, import_node_path23;
11382
+ var import_node_fs25, import_node_path25;
10658
11383
  var init_devcontainer_verify = __esm({
10659
11384
  "src/core/devcontainer-verify.ts"() {
10660
11385
  "use strict";
10661
- import_node_fs23 = __toESM(require("fs"));
10662
- import_node_path23 = __toESM(require("path"));
11386
+ import_node_fs25 = __toESM(require("fs"));
11387
+ import_node_path25 = __toESM(require("path"));
10663
11388
  init_capabilities();
10664
11389
  init_doctor();
10665
11390
  init_host_routes();
@@ -10955,7 +11680,7 @@ function packageManagerIssues(repo) {
10955
11680
  }
10956
11681
  function plannedFiles(repoPath, version) {
10957
11682
  const repo = inspectRepo({ repo: repoPath });
10958
- const projectName = sanitizeProjectName(import_node_path24.default.basename(repo.repoPath));
11683
+ const projectName = sanitizeProjectName(import_node_path26.default.basename(repo.repoPath));
10959
11684
  const nodeMajor = majorVersion(repo.node?.version, "24");
10960
11685
  const pnpmVersion = repo.packageManager?.name === "pnpm" && repo.packageManager.version ? repo.packageManager.version : DEFAULT_PNPM_VERSION;
10961
11686
  const port = inferPort2(repo);
@@ -11000,8 +11725,8 @@ function plannedFiles(repoPath, version) {
11000
11725
  };
11001
11726
  }
11002
11727
  function classifyFile(repoPath, file) {
11003
- const absolutePath = import_node_path24.default.join(repoPath, file.relativePath);
11004
- if (!import_node_fs24.default.existsSync(absolutePath)) {
11728
+ const absolutePath = import_node_path26.default.join(repoPath, file.relativePath);
11729
+ if (!import_node_fs26.default.existsSync(absolutePath)) {
11005
11730
  return {
11006
11731
  path: file.relativePath,
11007
11732
  action: "create",
@@ -11009,7 +11734,7 @@ function classifyFile(repoPath, file) {
11009
11734
  bytes: Buffer.byteLength(file.content)
11010
11735
  };
11011
11736
  }
11012
- const current = import_node_fs24.default.readFileSync(absolutePath, "utf-8");
11737
+ const current = import_node_fs26.default.readFileSync(absolutePath, "utf-8");
11013
11738
  if (!current.includes(MANAGED_MARKER2)) {
11014
11739
  return {
11015
11740
  path: file.relativePath,
@@ -11066,11 +11791,11 @@ function buildPlan(repoPath, dryRun, version) {
11066
11791
  };
11067
11792
  }
11068
11793
  function writeFile(repoPath, file) {
11069
- const absolutePath = import_node_path24.default.join(repoPath, file.relativePath);
11070
- import_node_fs24.default.mkdirSync(import_node_path24.default.dirname(absolutePath), { recursive: true });
11071
- import_node_fs24.default.writeFileSync(absolutePath, file.content, "utf-8");
11794
+ const absolutePath = import_node_path26.default.join(repoPath, file.relativePath);
11795
+ import_node_fs26.default.mkdirSync(import_node_path26.default.dirname(absolutePath), { recursive: true });
11796
+ import_node_fs26.default.writeFileSync(absolutePath, file.content, "utf-8");
11072
11797
  if (file.executable) {
11073
- import_node_fs24.default.chmodSync(absolutePath, 493);
11798
+ import_node_fs26.default.chmodSync(absolutePath, 493);
11074
11799
  }
11075
11800
  }
11076
11801
  function writeDevcontainer(options = {}) {
@@ -11108,12 +11833,12 @@ function writeDevcontainer(options = {}) {
11108
11833
  nextSteps: postWriteNextSteps(repoPath)
11109
11834
  };
11110
11835
  }
11111
- var import_node_fs24, import_node_path24, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
11836
+ var import_node_fs26, import_node_path26, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
11112
11837
  var init_devcontainer_write = __esm({
11113
11838
  "src/core/devcontainer-write.ts"() {
11114
11839
  "use strict";
11115
- import_node_fs24 = __toESM(require("fs"));
11116
- import_node_path24 = __toESM(require("path"));
11840
+ import_node_fs26 = __toESM(require("fs"));
11841
+ import_node_path26 = __toESM(require("path"));
11117
11842
  init_repo_config();
11118
11843
  init_repo_inspect();
11119
11844
  MANAGED_MARKER2 = "devrouter:managed devcontainer";
@@ -11504,7 +12229,7 @@ function sanitizeRouterId(value) {
11504
12229
  return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
11505
12230
  }
11506
12231
  function repoHash(repoPath) {
11507
- return (0, import_node_crypto8.createHash)("sha1").update(import_node_path25.default.resolve(repoPath)).digest("hex").slice(0, 12);
12232
+ return (0, import_node_crypto8.createHash)("sha1").update(import_node_path27.default.resolve(repoPath)).digest("hex").slice(0, 12);
11508
12233
  }
11509
12234
  function asDockerApp(app) {
11510
12235
  return app.runtime === "docker";
@@ -11583,11 +12308,11 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
11583
12308
  if (dockerApps.length === 0) {
11584
12309
  throw new Error("No docker apps selected to prepare compose overlay.");
11585
12310
  }
11586
- const cachePath = import_node_path25.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
11587
- import_node_fs25.default.mkdirSync(cachePath, { recursive: true });
11588
- const overlayPath = import_node_path25.default.join(cachePath, "compose.devrouter.yml");
12311
+ const cachePath = import_node_path27.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
12312
+ import_node_fs27.default.mkdirSync(cachePath, { recursive: true });
12313
+ const overlayPath = import_node_path27.default.join(cachePath, "compose.devrouter.yml");
11589
12314
  const overlayDocument = buildOverlayDocument(dockerApps, publishTcpPorts);
11590
- import_node_fs25.default.writeFileSync(overlayPath, import_yaml7.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
12315
+ import_node_fs27.default.writeFileSync(overlayPath, import_yaml7.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
11591
12316
  return {
11592
12317
  overlayPath,
11593
12318
  composeFiles: ensureComposeFiles(dockerApps),
@@ -11601,7 +12326,7 @@ function runDockerComposeUp(repoPath, composeFiles2, overlayPath, services) {
11601
12326
  fileArgs.push("-f", resolved);
11602
12327
  }
11603
12328
  const args = ["compose", ...fileArgs, "-f", overlayPath, "up", "-d", "--wait", ...services];
11604
- const result = (0, import_node_child_process20.spawnSync)("docker", args, {
12329
+ const result = (0, import_node_child_process25.spawnSync)("docker", args, {
11605
12330
  encoding: "utf-8",
11606
12331
  cwd: repoPath
11607
12332
  });
@@ -11629,7 +12354,7 @@ function queryRunningComposeServices(repoPath, composeFiles2, overlayPath, servi
11629
12354
  "--services",
11630
12355
  ...services
11631
12356
  ];
11632
- const result = (0, import_node_child_process20.spawnSync)("docker", args, {
12357
+ const result = (0, import_node_child_process25.spawnSync)("docker", args, {
11633
12358
  encoding: "utf-8",
11634
12359
  cwd: repoPath
11635
12360
  });
@@ -11655,7 +12380,7 @@ function runDockerComposeStop(repoPath, composeFiles2, overlayPath, services) {
11655
12380
  fileArgs.push("-f", resolved);
11656
12381
  }
11657
12382
  const args = ["compose", ...fileArgs, "-f", overlayPath, "stop", ...services];
11658
- const result = (0, import_node_child_process20.spawnSync)("docker", args, {
12383
+ const result = (0, import_node_child_process25.spawnSync)("docker", args, {
11659
12384
  encoding: "utf-8",
11660
12385
  cwd: repoPath
11661
12386
  });
@@ -11681,7 +12406,7 @@ function runDockerComposeLogs(repoPath, composeFiles2, overlayPath, services, ta
11681
12406
  String(tail),
11682
12407
  ...services
11683
12408
  ];
11684
- (0, import_node_child_process20.spawnSync)("docker", args, {
12409
+ (0, import_node_child_process25.spawnSync)("docker", args, {
11685
12410
  stdio: "inherit",
11686
12411
  cwd: repoPath
11687
12412
  });
@@ -11693,7 +12418,7 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
11693
12418
  fileArgs.push("-f", resolved);
11694
12419
  }
11695
12420
  const args = ["compose", ...fileArgs, "-f", overlayPath, "port", service, String(internalPort)];
11696
- const result = (0, import_node_child_process20.spawnSync)("docker", args, {
12421
+ const result = (0, import_node_child_process25.spawnSync)("docker", args, {
11697
12422
  encoding: "utf-8",
11698
12423
  cwd: repoPath
11699
12424
  });
@@ -11707,14 +12432,14 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
11707
12432
  const port = Number(match[1]);
11708
12433
  return Number.isInteger(port) && port > 0 ? port : void 0;
11709
12434
  }
11710
- var import_node_child_process20, import_node_crypto8, import_node_fs25, import_node_path25, import_yaml7;
12435
+ var import_node_child_process25, import_node_crypto8, import_node_fs27, import_node_path27, import_yaml7;
11711
12436
  var init_docker_run = __esm({
11712
12437
  "src/core/docker-run.ts"() {
11713
12438
  "use strict";
11714
- import_node_child_process20 = require("child_process");
12439
+ import_node_child_process25 = require("child_process");
11715
12440
  import_node_crypto8 = require("crypto");
11716
- import_node_fs25 = __toESM(require("fs"));
11717
- import_node_path25 = __toESM(require("path"));
12441
+ import_node_fs27 = __toESM(require("fs"));
12442
+ import_node_path27 = __toESM(require("path"));
11718
12443
  import_yaml7 = __toESM(require("yaml"));
11719
12444
  init_docker_error_guidance();
11720
12445
  init_paths();
@@ -11804,7 +12529,7 @@ function isProcessRunning(pid) {
11804
12529
  }
11805
12530
  }
11806
12531
  function readProcessTree(rootPid) {
11807
- const result = (0, import_node_child_process21.spawnSync)("ps", ["-ax", "-o", "pid=,ppid="], { encoding: "utf-8" });
12532
+ const result = (0, import_node_child_process26.spawnSync)("ps", ["-ax", "-o", "pid=,ppid="], { encoding: "utf-8" });
11808
12533
  if (result.status !== 0) {
11809
12534
  return [rootPid];
11810
12535
  }
@@ -11915,12 +12640,12 @@ function detectListeningPorts(pids) {
11915
12640
  if (pids.length === 0) {
11916
12641
  return [];
11917
12642
  }
11918
- const result = (0, import_node_child_process21.spawnSync)("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", pids.join(",")], {
12643
+ const result = (0, import_node_child_process26.spawnSync)("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", pids.join(",")], {
11919
12644
  encoding: "utf-8"
11920
12645
  });
11921
12646
  if (result.error && result.error.code === "ENOENT") {
11922
12647
  if (process.platform === "linux") {
11923
- const ssResult = (0, import_node_child_process21.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
12648
+ const ssResult = (0, import_node_child_process26.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
11924
12649
  if (ssResult.status === 0 && ssResult.stdout) {
11925
12650
  return parseSsListeningPorts(ssResult.stdout, new Set(pids));
11926
12651
  }
@@ -11979,7 +12704,7 @@ async function runHostApp(repoPath, app, extraEnv = {}, secretManager, env, work
11979
12704
  app.hostRun.command,
11980
12705
  true
11981
12706
  ) : app.hostRun.command;
11982
- const child = (0, import_node_child_process21.spawn)(spawnCommand, {
12707
+ const child = (0, import_node_child_process26.spawn)(spawnCommand, {
11983
12708
  cwd: commandCwd,
11984
12709
  stdio: "inherit",
11985
12710
  shell: true,
@@ -12347,7 +13072,7 @@ async function execWithAppEnv(options) {
12347
13072
  options.command[0],
12348
13073
  true
12349
13074
  );
12350
- child = (0, import_node_child_process21.spawn)(wrapped, {
13075
+ child = (0, import_node_child_process26.spawn)(wrapped, {
12351
13076
  cwd: deps.repoPath,
12352
13077
  stdio: "inherit",
12353
13078
  shell: true,
@@ -12361,7 +13086,7 @@ async function execWithAppEnv(options) {
12361
13086
  false
12362
13087
  );
12363
13088
  const [cmd, ...wrappedArgs] = wrapped;
12364
- child = (0, import_node_child_process21.spawn)(cmd, wrappedArgs, {
13089
+ child = (0, import_node_child_process26.spawn)(cmd, wrappedArgs, {
12365
13090
  cwd: deps.repoPath,
12366
13091
  stdio: "inherit",
12367
13092
  shell: false,
@@ -12369,7 +13094,7 @@ async function execWithAppEnv(options) {
12369
13094
  });
12370
13095
  }
12371
13096
  } else if (options.shell) {
12372
- child = (0, import_node_child_process21.spawn)(options.command[0], {
13097
+ child = (0, import_node_child_process26.spawn)(options.command[0], {
12373
13098
  cwd: deps.repoPath,
12374
13099
  stdio: "inherit",
12375
13100
  shell: true,
@@ -12377,7 +13102,7 @@ async function execWithAppEnv(options) {
12377
13102
  });
12378
13103
  } else {
12379
13104
  const [command, ...args] = options.command;
12380
- child = (0, import_node_child_process21.spawn)(command, args, {
13105
+ child = (0, import_node_child_process26.spawn)(command, args, {
12381
13106
  cwd: deps.repoPath,
12382
13107
  stdio: "inherit",
12383
13108
  shell: false,
@@ -12398,11 +13123,11 @@ async function execWithAppEnv(options) {
12398
13123
  deps.stopDeps();
12399
13124
  }
12400
13125
  }
12401
- var import_node_child_process21, import_node_net, import_node_process, import_promises, POLL_INTERVAL_MS2, DEFAULT_PORT_TIMEOUT_MS, PROCESS_TERMINATION_GRACE_MS;
13126
+ var import_node_child_process26, import_node_net, import_node_process, import_promises, POLL_INTERVAL_MS2, DEFAULT_PORT_TIMEOUT_MS, PROCESS_TERMINATION_GRACE_MS;
12402
13127
  var init_app_run = __esm({
12403
13128
  "src/core/app-run.ts"() {
12404
13129
  "use strict";
12405
- import_node_child_process21 = require("child_process");
13130
+ import_node_child_process26 = require("child_process");
12406
13131
  import_node_net = __toESM(require("net"));
12407
13132
  import_node_process = require("process");
12408
13133
  import_promises = require("readline/promises");
@@ -12536,13 +13261,13 @@ function measureWorktreeConsumption(worktreePath, options) {
12536
13261
  const startedAt = Date.now();
12537
13262
  let rootStat;
12538
13263
  try {
12539
- rootStat = import_node_fs26.default.lstatSync(worktreePath);
13264
+ rootStat = import_node_fs28.default.lstatSync(worktreePath);
12540
13265
  } catch (error) {
12541
13266
  return { status: "unknown", reason: describeError(error, worktreePath) };
12542
13267
  }
12543
13268
  let rootEntries;
12544
13269
  try {
12545
- rootEntries = import_node_fs26.default.readdirSync(worktreePath, { withFileTypes: true });
13270
+ rootEntries = import_node_fs28.default.readdirSync(worktreePath, { withFileTypes: true });
12546
13271
  } catch (error) {
12547
13272
  return { status: "unknown", reason: describeError(error, worktreePath) };
12548
13273
  }
@@ -12568,10 +13293,10 @@ function measureWorktreeConsumption(worktreePath, options) {
12568
13293
  timedOut = true;
12569
13294
  break;
12570
13295
  }
12571
- const entryPath = import_node_path26.default.join(dirPath, entry.name);
13296
+ const entryPath = import_node_path28.default.join(dirPath, entry.name);
12572
13297
  let entryStat;
12573
13298
  try {
12574
- entryStat = import_node_fs26.default.lstatSync(entryPath);
13299
+ entryStat = import_node_fs28.default.lstatSync(entryPath);
12575
13300
  } catch (error) {
12576
13301
  if (error?.code === "ENOENT") continue;
12577
13302
  unreadableReason = describeIncompleteWalk(error);
@@ -12581,7 +13306,7 @@ function measureWorktreeConsumption(worktreePath, options) {
12581
13306
  if (!entryStat.isDirectory()) continue;
12582
13307
  let childEntries;
12583
13308
  try {
12584
- childEntries = import_node_fs26.default.readdirSync(entryPath, { withFileTypes: true });
13309
+ childEntries = import_node_fs28.default.readdirSync(entryPath, { withFileTypes: true });
12585
13310
  } catch (error) {
12586
13311
  unreadableReason = describeIncompleteWalk(error);
12587
13312
  break;
@@ -12645,12 +13370,12 @@ function describeError(error, worktreePath) {
12645
13370
  }
12646
13371
  return `could not stat worktree path '${worktreePath}': ${error?.message ?? String(error)}`;
12647
13372
  }
12648
- var import_node_fs26, import_node_path26, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
13373
+ var import_node_fs28, import_node_path28, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
12649
13374
  var init_workspace_consumption = __esm({
12650
13375
  "src/core/workspace-consumption.ts"() {
12651
13376
  "use strict";
12652
- import_node_fs26 = __toESM(require("fs"));
12653
- import_node_path26 = __toESM(require("path"));
13377
+ import_node_fs28 = __toESM(require("fs"));
13378
+ import_node_path28 = __toESM(require("path"));
12654
13379
  init_devpod_environment();
12655
13380
  DEFAULT_DEADLINE_MS = 1e4;
12656
13381
  BLOCK_SIZE_BYTES = 512;
@@ -12751,7 +13476,7 @@ function evaluateWorkspaceActivity(evidence, cutoff) {
12751
13476
  }
12752
13477
  function readGitSnapshot(worktree, commandRunner) {
12753
13478
  const comparablePath = comparableWorkspacePath(worktree.path);
12754
- if (worktree.prunable || !import_node_fs27.default.existsSync(comparablePath)) {
13479
+ if (worktree.prunable || !import_node_fs29.default.existsSync(comparablePath)) {
12755
13480
  return { worktree, checkout: "missing", head: null, committerDate: null };
12756
13481
  }
12757
13482
  const head = gitOutput(comparablePath, ["rev-parse", "--verify", "HEAD"], commandRunner);
@@ -13260,12 +13985,6 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
13260
13985
  const cutoff = new Date(now.getTime() - duration.seconds * 1e3).toISOString();
13261
13986
  const worktrees = (dependencies.listWorktrees ?? listGitWorktrees)(repoPath);
13262
13987
  const records = (dependencies.listOwnership ?? listWorkspaceOwnership)(repoPath);
13263
- let devpods;
13264
- try {
13265
- devpods = (dependencies.listDevpods ?? listDevpodWorkspaces)();
13266
- } catch {
13267
- devpods = void 0;
13268
- }
13269
13988
  let routes;
13270
13989
  try {
13271
13990
  routes = (dependencies.readRoutes ?? readHostRouteStateReadOnly)();
@@ -13282,7 +14001,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
13282
14001
  containers = measureContainersFn(worktreePaths);
13283
14002
  } catch (error) {
13284
14003
  const reason = `container measurement failed: ${describeCause(error)}`;
13285
- containers = new Map(worktreePaths.map((path26) => [path26, unknownContainers(reason)]));
14004
+ containers = new Map(worktreePaths.map((path28) => [path28, unknownContainers(reason)]));
13286
14005
  }
13287
14006
  }
13288
14007
  const rows = records.map((record) => {
@@ -13297,6 +14016,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
13297
14016
  prunable: true
13298
14017
  }
13299
14018
  );
14019
+ const devpods = dependencies.listDevpods ? dependencies.listDevpods() : listDevpodWorkspacesFromSnapshots(record.worktreePath);
13300
14020
  const row = buildRow(
13301
14021
  repoPath,
13302
14022
  record,
@@ -13312,7 +14032,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
13312
14032
  return { ownerStatus: status.ownerStatus, devpodStatus: status.devpodStatus };
13313
14033
  }),
13314
14034
  dependencies.inspectIntegration,
13315
- dependencies.inspectDevpodRuntime ?? inspectDevpodRuntimeStatus
14035
+ dependencies.inspectDevpodRuntime ?? ((devpodId) => inspectDevpodRuntimeStatus(devpodId, record.worktreePath))
13316
14036
  );
13317
14037
  return measureSize ? {
13318
14038
  ...row,
@@ -13330,12 +14050,12 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
13330
14050
  workspaces: rows
13331
14051
  };
13332
14052
  }
13333
- var import_node_child_process22, import_node_fs27, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
14053
+ var import_node_child_process27, import_node_fs29, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
13334
14054
  var init_workspace_cleanup = __esm({
13335
14055
  "src/core/workspace-cleanup.ts"() {
13336
14056
  "use strict";
13337
- import_node_child_process22 = require("child_process");
13338
- import_node_fs27 = __toESM(require("fs"));
14057
+ import_node_child_process27 = require("child_process");
14058
+ import_node_fs29 = __toESM(require("fs"));
13339
14059
  init_devpod_workspaces();
13340
14060
  init_host_routes();
13341
14061
  init_repo_config();
@@ -13352,7 +14072,7 @@ var init_workspace_cleanup = __esm({
13352
14072
  ];
13353
14073
  defaultCommandRunner = (command, args, input2) => {
13354
14074
  const environment = command === "git" ? READ_ONLY_GIT_ENV2 : { ...process.env, LC_ALL: "C" };
13355
- const result = (0, import_node_child_process22.spawnSync)(command, args, {
14075
+ const result = (0, import_node_child_process27.spawnSync)(command, args, {
13356
14076
  encoding: "utf-8",
13357
14077
  env: environment,
13358
14078
  ...input2 === void 0 ? {} : { input: input2 }
@@ -13512,7 +14232,7 @@ var init_version = __esm({
13512
14232
 
13513
14233
  // src/cli.ts
13514
14234
  var import_commander = require("commander");
13515
- var CLI_VERSION = true ? "0.0.40" : "0.0.0-dev";
14235
+ var CLI_VERSION = true ? "0.0.42" : "0.0.0-dev";
13516
14236
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
13517
14237
  function withErrorHandling(action2) {
13518
14238
  return async (...args) => {
@@ -13550,11 +14270,16 @@ program.command("upgrade").description(
13550
14270
  }
13551
14271
  )
13552
14272
  );
13553
- program.command("setup").description("Run first-time devrouter machine setup and report diagnostics").option("--repo <path>", "Repository path for final diagnostics (defaults to current directory)").option("--yes", "Confirm non-interactive setup actions").option("--json", "Output JSON").action(
13554
- withErrorHandling(async (options) => {
13555
- const { runSetupCommand: runSetupCommand2 } = await Promise.resolve().then(() => (init_setup2(), setup_exports));
13556
- await runSetupCommand2(options);
13557
- })
14273
+ program.command("setup").description("Run first-time devrouter machine setup and report diagnostics").option("--repo <path>", "Repository path for final diagnostics (defaults to current directory)").option("--yes", "Confirm non-interactive setup actions").option("--workspace-runtime <runtime>", "Persist the machine workspace runtime (devpod|devsy)").option(
14274
+ "--devsy-inactivity-timeout <duration>",
14275
+ "Persist the Devsy inactivity shutdown duration (e.g. 30m)"
14276
+ ).option("--json", "Output JSON").action(
14277
+ withErrorHandling(
14278
+ async (options) => {
14279
+ const { runSetupCommand: runSetupCommand2 } = await Promise.resolve().then(() => (init_setup2(), setup_exports));
14280
+ await runSetupCommand2(options);
14281
+ }
14282
+ )
13558
14283
  );
13559
14284
  program.command("up").description("Ensure devnet and start shared Traefik (reserves 80/443/5432)").action(
13560
14285
  withErrorHandling(async () => {
@@ -13574,7 +14299,7 @@ program.command("ensure").description("Start and prove a primary or linked check
13574
14299
  });
13575
14300
  })
13576
14301
  );
13577
- program.command("stop").description("Stop one checkout's exact DevPod and free its routes").argument("[path]", "Git checkout path (defaults to current directory)").option("--json", "Output JSON").option("--delete", "Delete the exact DevPod instead of preserving its data").action(
14302
+ program.command("stop").description("Stop one checkout's exact workspace runtime (DevPod/Devsy) and free its routes").argument("[path]", "Git checkout path (defaults to current directory)").option("--json", "Output JSON").option("--delete", "Delete the exact workspace runtime instead of preserving its data").action(
13578
14303
  withErrorHandling(async (repoPath, _options, command) => {
13579
14304
  const options = command.opts();
13580
14305
  const { runStopCommand: runStopCommand2 } = await Promise.resolve().then(() => (init_stop(), stop_exports));
@@ -13585,7 +14310,7 @@ program.command("stop").description("Stop one checkout's exact DevPod and free i
13585
14310
  });
13586
14311
  })
13587
14312
  );
13588
- program.command("exec").description("Run one literal command inside the exact checkout's running DevPod").argument("[args...]", "[path] -- <command...>").allowUnknownOption(true).action(
14313
+ program.command("exec").description("Run one literal command inside the exact checkout's running workspace runtime").argument("[args...]", "[path] -- <command...>").allowUnknownOption(true).action(
13589
14314
  withErrorHandling(async () => {
13590
14315
  const commandIndex = process.argv.indexOf("exec", 2);
13591
14316
  const { parseExecInvocation: parseExecInvocation2, runExecCommand: runExecCommand2 } = await Promise.resolve().then(() => (init_exec(), exec_exports));
@@ -13754,7 +14479,7 @@ tlsCommand.command("install").description("Install mkcert certs and enable HTTPS
13754
14479
  );
13755
14480
  var workspaceCommand2 = program.command("workspace").description("Spin up / list / tear down isolated worktree+devcontainer workspaces");
13756
14481
  workspaceCommand2.command("up").description(
13757
- "Create a worktree for <branch>, bring up its devpod, and register namespaced routes"
14482
+ "Create a worktree for <branch>, bring up its workspace runtime, and register namespaced routes"
13758
14483
  ).argument("<branch>", "Git branch to base the workspace on").option("--path <dir>", "Worktree directory (default: <repo>/trees/<workspace>)").option("--no-devpod", "Only create the worktree; do not start the environment or change routes").option("--open", "Open the namespaced routes after registering").option("--repo <path>", "Main repository path (defaults to current directory)").action(
13759
14484
  withErrorHandling(async (branch, _options, command) => {
13760
14485
  const options = command.opts();
@@ -13767,7 +14492,9 @@ workspaceCommand2.command("up").description(
13767
14492
  });
13768
14493
  })
13769
14494
  );
13770
- workspaceCommand2.command("ensure").description("Start and prove a primary or linked checkout's DevPod, upstreams, and routes").argument("[path]", "Git checkout path (defaults to current directory)").option("--profile <name>", "Start only this profile's apps/dependencies from .devrouter.yml").option("--open", "Open HTTP routes after readiness succeeds").option("--json", "Output JSON").action(
14495
+ workspaceCommand2.command("ensure").description(
14496
+ "Start and prove a primary or linked checkout's workspace runtime, upstreams, and routes"
14497
+ ).argument("[path]", "Git checkout path (defaults to current directory)").option("--profile <name>", "Start only this profile's apps/dependencies from .devrouter.yml").option("--open", "Open HTTP routes after readiness succeeds").option("--json", "Output JSON").action(
13771
14498
  withErrorHandling(
13772
14499
  async (worktreePath, _options, command) => {
13773
14500
  const options = command.opts();
@@ -13801,21 +14528,21 @@ workspaceCommand2.command("cleanup").description("Report managed workspace activ
13801
14528
  });
13802
14529
  })
13803
14530
  );
13804
- workspaceCommand2.command("gc").description("Report or clean missing ledger-owned workspace resources").option("--repo <path>", "Main repository path (defaults to current directory)").option("--json", "Output JSON").option("--yes", "Delete eligible DevPod, route, and ownership resources").action(
14531
+ workspaceCommand2.command("gc").description("Report or clean missing ledger-owned workspace resources").option("--repo <path>", "Main repository path (defaults to current directory)").option("--json", "Output JSON").option("--yes", "Delete eligible workspace runtime, route, and ownership resources").action(
13805
14532
  withErrorHandling(async (_options, command) => {
13806
14533
  const options = command.opts();
13807
14534
  const { runWorkspaceGcCommand: runWorkspaceGcCommand2 } = await Promise.resolve().then(() => (init_workspace2(), workspace_exports));
13808
14535
  runWorkspaceGcCommand2(options);
13809
14536
  })
13810
14537
  );
13811
- workspaceCommand2.command("stop").description("Stop a workspace's DevPod and free routes while preserving its worktree and data").argument("<workspace>", "Workspace token or live branch name").option("--repo <path>", "Main repository path (defaults to current directory)").action(
14538
+ workspaceCommand2.command("stop").description("Stop a workspace's runtime and free routes while preserving its worktree and data").argument("<workspace>", "Workspace token or live branch name").option("--repo <path>", "Main repository path (defaults to current directory)").action(
13812
14539
  withErrorHandling(async (target, _options, command) => {
13813
14540
  const options = command.opts();
13814
14541
  const { runWorkspaceStopCommand: runWorkspaceStopCommand2 } = await Promise.resolve().then(() => (init_workspace2(), workspace_exports));
13815
14542
  await runWorkspaceStopCommand2(target, options);
13816
14543
  })
13817
14544
  );
13818
- workspaceCommand2.command("down").description("Delete a workspace's DevPod and routes, then remove its clean Git worktree").argument("<workspace>", "Workspace token or live branch name").option("--keep-worktree", "Delete runtime resources but preserve the Git worktree and record").option("--repo <path>", "Main repository path (defaults to current directory)").action(
14545
+ workspaceCommand2.command("down").description("Delete a workspace's runtime and routes, then remove its clean Git worktree").argument("<workspace>", "Workspace token or live branch name").option("--keep-worktree", "Delete runtime resources but preserve the Git worktree and record").option("--repo <path>", "Main repository path (defaults to current directory)").action(
13819
14546
  withErrorHandling(async (target, _options, command) => {
13820
14547
  const options = command.opts();
13821
14548
  const { runWorkspaceDownCommand: runWorkspaceDownCommand2 } = await Promise.resolve().then(() => (init_workspace2(), workspace_exports));