@devrouter/cli 0.0.39 → 0.0.41

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.39" : "0.0.0-dev";
2507
+ const cliVersion = true ? "0.0.41" : "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]",
@@ -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,228 @@ 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
6329
6761
  function withMutationLock(activity, target, operation) {
6330
- import_node_fs17.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
6762
+ import_node_fs18.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
6763
+ return withFileLockSync(
6764
+ DEVSY_MUTATION_LOCK_FILE,
6765
+ { activity, target: `'${target}'`, waitMs: DEVSY_MUTATION_WAIT_MS },
6766
+ operation
6767
+ );
6768
+ }
6769
+ function commandFailure2(result) {
6770
+ return [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
6771
+ }
6772
+ function runDevsyAction(action2, devsyId, force = false) {
6773
+ const args = action2 === "delete" ? ["delete", devsyId, ...force ? ["--force"] : [], "--ignore-not-found"] : ["stop", devsyId];
6774
+ const result = (0, import_node_child_process14.spawnSync)("devsy", ["workspace", ...args], { encoding: "utf-8" });
6775
+ if (result.status !== 0) {
6776
+ throw new Error(
6777
+ `devsy workspace ${action2}${force ? " --force" : ""} failed for '${devsyId}': ${commandFailure2(result) || "unknown error"}`
6778
+ );
6779
+ }
6780
+ }
6781
+ function inspectExactOwnership(devsyId, worktreePath) {
6782
+ const ownership = inspectDevsyWorkspaceOwnership(listDevsyWorkspaces(), devsyId, worktreePath);
6783
+ if (ownership.status === "conflict") throw new Error(ownership.reason);
6784
+ return ownership;
6785
+ }
6786
+ function mutateOwnedDevsyWorkspace(action2, devsyId, worktreePath) {
6787
+ return withMutationLock(`Devsy ${action2}`, worktreePath, () => {
6788
+ const before = inspectExactOwnership(devsyId, worktreePath);
6789
+ if (before.status === "absent") return { status: "absent" };
6790
+ runDevsyAction(action2, devsyId);
6791
+ let after = inspectExactOwnership(devsyId, worktreePath);
6792
+ if (action2 === "stop" && after.status !== "owned") {
6793
+ throw new Error(
6794
+ `Devsy workspace '${devsyId}' no longer owns '${worktreePath}' after provider stop.`
6795
+ );
6796
+ }
6797
+ if (action2 === "delete" && after.status === "owned") {
6798
+ const runtime = inspectDevsyRuntimeStatus(devsyId);
6799
+ if (runtime !== "not-found") {
6800
+ throw new Error(
6801
+ `Devsy workspace '${devsyId}' still owns '${worktreePath}' after provider delete (runtime=${runtime}).`
6802
+ );
6803
+ }
6804
+ after = inspectExactOwnership(devsyId, worktreePath);
6805
+ if (after.status === "owned") {
6806
+ runDevsyAction("delete", devsyId, true);
6807
+ after = inspectExactOwnership(devsyId, worktreePath);
6808
+ }
6809
+ if (after.status !== "absent") {
6810
+ throw new Error(
6811
+ `Devsy workspace '${devsyId}' still owns '${worktreePath}' after forced provider delete.`
6812
+ );
6813
+ }
6814
+ }
6815
+ return { status: "changed" };
6816
+ });
6817
+ }
6818
+ function stopOwnedDevsyWorkspace(devsyId, worktreePath) {
6819
+ return mutateOwnedDevsyWorkspace("stop", devsyId, worktreePath);
6820
+ }
6821
+ function deleteOwnedDevsyWorkspace(devsyId, worktreePath) {
6822
+ return mutateOwnedDevsyWorkspace("delete", devsyId, worktreePath);
6823
+ }
6824
+ function assertDevsyTarget(devsyId, repoPath) {
6825
+ const workspaces = listDevsyWorkspaces();
6826
+ const existing = selectDevsyWorkspace(workspaces, repoPath);
6827
+ const id = devsyId ?? existing?.id;
6828
+ if (id) {
6829
+ const before = inspectDevsyWorkspaceOwnership(workspaces, id, repoPath);
6830
+ if (before.status === "conflict") throw new Error(before.reason);
6831
+ }
6832
+ return id;
6833
+ }
6834
+ function startDevsyWorkspace(options) {
6835
+ const activity = options.recreate ? "Devsy recreate" : "Devsy start";
6836
+ return withMutationLock(activity, options.repoPath, () => {
6837
+ let devsyId = assertDevsyTarget(options.devsyId, options.repoPath);
6838
+ if (devsyId && options.recreate) {
6839
+ const attached = listDevsyWorkspaces();
6840
+ const ownership = inspectDevsyWorkspaceOwnership(attached, devsyId, options.repoPath);
6841
+ if (ownership.status !== "owned") {
6842
+ throw new Error(`Cannot recreate Devsy workspace '${devsyId}' without one exact owner.`);
6843
+ }
6844
+ }
6845
+ if (!devsyId && options.recreate) {
6846
+ throw new Error("Cannot recreate a Devsy workspace before its exact id is known.");
6847
+ }
6848
+ const args = ["workspace", "up", options.repoPath];
6849
+ if (devsyId) args.push("--id", devsyId);
6850
+ if (options.devcontainerPath) args.push("--devcontainer", options.devcontainerPath);
6851
+ args.push("--ide-launch", "skip");
6852
+ if (options.workspace) {
6853
+ args.push(
6854
+ "--workspace-env",
6855
+ `WORKSPACE=${options.workspace.token}`,
6856
+ "--workspace-env",
6857
+ `DEVROUTER_WORKSPACE=${options.workspace.token}`
6858
+ );
6859
+ }
6860
+ if (options.inactivityTimeout) {
6861
+ args.push("--provider-option", `INACTIVITY_TIMEOUT=${options.inactivityTimeout}`);
6862
+ }
6863
+ if (options.recreate) args.push("--recreate");
6864
+ const env = { ...process.env };
6865
+ if (options.workspace) {
6866
+ env.WORKSPACE = options.workspace.token;
6867
+ env.DEVROUTER_WORKSPACE = options.workspace.token;
6868
+ env.DEVROUTER_GIT_COMMON_DIR = options.workspace.gitCommonDir;
6869
+ env.DEVCONTAINER_COMPOSE_OVERLAY = "docker-compose.devrouter.yml";
6870
+ } else {
6871
+ delete env.WORKSPACE;
6872
+ delete env.DEVROUTER_WORKSPACE;
6873
+ delete env.DEVROUTER_GIT_COMMON_DIR;
6874
+ delete env.DEVCONTAINER_COMPOSE_OVERLAY;
6875
+ }
6876
+ const result = (0, import_node_child_process14.spawnSync)("devsy", args, {
6877
+ stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
6878
+ env
6879
+ });
6880
+ if (result.status !== 0) {
6881
+ throw new Error(`devsy workspace up failed for '${devsyId ?? options.repoPath}'.`);
6882
+ }
6883
+ try {
6884
+ const attached = listDevsyWorkspaces();
6885
+ devsyId ??= selectDevsyWorkspace(attached, options.repoPath)?.id;
6886
+ if (!devsyId) {
6887
+ throw new Error(`Devsy did not attach '${options.repoPath}' after startup.`);
6888
+ }
6889
+ const ownership = inspectDevsyWorkspaceOwnership(attached, devsyId, options.repoPath);
6890
+ if (ownership.status === "conflict") throw new Error(ownership.reason);
6891
+ if (ownership.status !== "owned") {
6892
+ throw new Error(
6893
+ `Devsy did not attach '${options.repoPath}' as '${devsyId}' after startup.`
6894
+ );
6895
+ }
6896
+ return devsyId;
6897
+ } catch (error) {
6898
+ const message = error instanceof Error ? error.message : String(error);
6899
+ throw new DevsyStartPostconditionError(message);
6900
+ }
6901
+ });
6902
+ }
6903
+ var import_node_child_process14, import_node_fs18, import_node_path17, DEVSY_MUTATION_LOCK_FILE, DEVSY_MUTATION_WAIT_MS, DevsyStartPostconditionError;
6904
+ var init_devsy_mutation = __esm({
6905
+ "src/core/devsy-mutation.ts"() {
6906
+ "use strict";
6907
+ import_node_child_process14 = require("child_process");
6908
+ import_node_fs18 = __toESM(require("fs"));
6909
+ import_node_path17 = __toESM(require("path"));
6910
+ init_devsy_workspaces();
6911
+ init_file_lock();
6912
+ init_router();
6913
+ DEVSY_MUTATION_LOCK_FILE = import_node_path17.default.join(DEVROUTER_HOME, "devsy-mutation.lock");
6914
+ DEVSY_MUTATION_WAIT_MS = 6e4;
6915
+ DevsyStartPostconditionError = class extends Error {
6916
+ };
6917
+ }
6918
+ });
6919
+
6920
+ // src/core/devpod-mutation.ts
6921
+ function withMutationLock2(activity, target, operation) {
6922
+ import_node_fs19.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
6331
6923
  return withFileLockSync(
6332
6924
  DEVPOD_MUTATION_LOCK_FILE,
6333
6925
  { activity, target: `'${target}'`, waitMs: DEVPOD_MUTATION_WAIT_MS },
6334
6926
  operation
6335
6927
  );
6336
6928
  }
6337
- function commandFailure2(result) {
6929
+ function commandFailure3(result) {
6338
6930
  return [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
6339
6931
  }
6340
6932
  function runDevpodAction(action2, devpodId, force = false) {
6341
6933
  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" });
6934
+ const result = (0, import_node_child_process15.spawnSync)("devpod", args, { encoding: "utf-8" });
6343
6935
  if (result.status !== 0) {
6344
6936
  throw new Error(
6345
- `devpod ${action2}${force ? " --force" : ""} failed for '${devpodId}': ${commandFailure2(result) || "unknown error"}`
6937
+ `devpod ${action2}${force ? " --force" : ""} failed for '${devpodId}': ${commandFailure3(result) || "unknown error"}`
6346
6938
  );
6347
6939
  }
6348
6940
  }
6349
- function inspectExactOwnership(devpodId, worktreePath) {
6350
- const ownership = inspectDevpodWorkspaceOwnership(listDevpodWorkspaces(), devpodId, worktreePath);
6941
+ function inspectExactOwnership2(devpodId, worktreePath) {
6942
+ const ownership = inspectDevpodWorkspaceOwnership(
6943
+ listDevpodWorkspaces(worktreePath),
6944
+ devpodId,
6945
+ worktreePath
6946
+ );
6351
6947
  if (ownership.status === "conflict") throw new Error(ownership.reason);
6352
6948
  return ownership;
6353
6949
  }
6354
6950
  function mutateOwnedDevpodWorkspace(action2, devpodId, worktreePath) {
6355
- return withMutationLock(`DevPod ${action2}`, worktreePath, () => {
6356
- const before = inspectExactOwnership(devpodId, worktreePath);
6951
+ return withMutationLock2(`DevPod ${action2}`, worktreePath, () => {
6952
+ const before = inspectExactOwnership2(devpodId, worktreePath);
6357
6953
  if (before.status === "absent") return { status: "absent" };
6358
6954
  runDevpodAction(action2, devpodId);
6359
- let after = inspectExactOwnership(devpodId, worktreePath);
6955
+ let after = inspectExactOwnership2(devpodId, worktreePath);
6360
6956
  if (action2 === "stop" && after.status !== "owned") {
6361
6957
  throw new Error(`DevPod '${devpodId}' no longer owns '${worktreePath}' after provider stop.`);
6362
6958
  }
6363
6959
  if (action2 === "delete" && after.status === "owned") {
6364
- const runtime = inspectDevpodRuntimeStatus(devpodId);
6960
+ const runtime = inspectDevpodRuntimeStatus(devpodId, worktreePath);
6365
6961
  if (runtime !== "not-found") {
6366
6962
  throw new Error(
6367
6963
  `DevPod '${devpodId}' still owns '${worktreePath}' after provider delete (runtime=${runtime}).`
6368
6964
  );
6369
6965
  }
6370
- after = inspectExactOwnership(devpodId, worktreePath);
6966
+ after = inspectExactOwnership2(devpodId, worktreePath);
6371
6967
  if (after.status === "owned") {
6372
6968
  runDevpodAction("delete", devpodId, true);
6373
- after = inspectExactOwnership(devpodId, worktreePath);
6969
+ after = inspectExactOwnership2(devpodId, worktreePath);
6374
6970
  }
6375
6971
  if (after.status !== "absent") {
6376
6972
  throw new Error(
@@ -6382,15 +6978,42 @@ function mutateOwnedDevpodWorkspace(action2, devpodId, worktreePath) {
6382
6978
  });
6383
6979
  }
6384
6980
  function stopOwnedDevpodWorkspace(devpodId, worktreePath) {
6385
- return mutateOwnedDevpodWorkspace("stop", devpodId, worktreePath);
6981
+ if (resolveWorkspaceRuntimeOrDefault(worktreePath) === "devsy") {
6982
+ const result2 = stopOwnedDevsyWorkspace(devpodId, worktreePath);
6983
+ resetWorkspaceRuntimeCaches();
6984
+ return result2;
6985
+ }
6986
+ const result = mutateOwnedDevpodWorkspace("stop", devpodId, worktreePath);
6987
+ resetWorkspaceRuntimeCaches();
6988
+ return result;
6386
6989
  }
6387
6990
  function deleteOwnedDevpodWorkspace(devpodId, worktreePath) {
6388
- return mutateOwnedDevpodWorkspace("delete", devpodId, worktreePath);
6991
+ if (resolveWorkspaceRuntimeOrDefault(worktreePath) === "devsy") {
6992
+ const result2 = deleteOwnedDevsyWorkspace(devpodId, worktreePath);
6993
+ resetWorkspaceRuntimeCaches();
6994
+ return result2;
6995
+ }
6996
+ const result = mutateOwnedDevpodWorkspace("delete", devpodId, worktreePath);
6997
+ resetWorkspaceRuntimeCaches();
6998
+ return result;
6389
6999
  }
6390
7000
  function startDevpodWorkspace(options) {
7001
+ if (resolveWorkspaceRuntimeOrDefault(options.repoPath) === "devsy") {
7002
+ const result = startDevsyWorkspace({
7003
+ repoPath: options.repoPath,
7004
+ devsyId: options.devpodId,
7005
+ devcontainerPath: options.devcontainerPath,
7006
+ recreate: options.recreate,
7007
+ quiet: options.quiet,
7008
+ workspace: options.workspace,
7009
+ inactivityTimeout: readWorkspaceRuntimeConfig().devsyInactivityTimeout
7010
+ });
7011
+ resetWorkspaceRuntimeCaches();
7012
+ return result;
7013
+ }
6391
7014
  const activity = options.recreate ? "DevPod recreate" : "DevPod start";
6392
- return withMutationLock(activity, options.repoPath, () => {
6393
- const workspaces = listDevpodWorkspaces();
7015
+ return withMutationLock2(activity, options.repoPath, () => {
7016
+ const workspaces = listDevpodWorkspaces(options.repoPath);
6394
7017
  let devpodId = options.devpodId ?? selectDevpodWorkspace(workspaces, options.repoPath)?.id;
6395
7018
  if (devpodId) {
6396
7019
  const before = inspectDevpodWorkspaceOwnership(workspaces, devpodId, options.repoPath);
@@ -6428,7 +7051,7 @@ function startDevpodWorkspace(options) {
6428
7051
  delete env.DEVROUTER_GIT_COMMON_DIR;
6429
7052
  delete env.DEVCONTAINER_COMPOSE_OVERLAY;
6430
7053
  }
6431
- const result = (0, import_node_child_process11.spawnSync)("devpod", args, {
7054
+ const result = (0, import_node_child_process15.spawnSync)("devpod", args, {
6432
7055
  stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
6433
7056
  env
6434
7057
  });
@@ -6436,7 +7059,7 @@ function startDevpodWorkspace(options) {
6436
7059
  throw new Error(`devpod up failed for '${devpodId ?? options.repoPath}'.`);
6437
7060
  }
6438
7061
  try {
6439
- const attached = listDevpodWorkspaces();
7062
+ const attached = listDevpodWorkspaces(options.repoPath);
6440
7063
  devpodId ??= selectDevpodWorkspace(attached, options.repoPath)?.id;
6441
7064
  if (!devpodId) {
6442
7065
  throw new Error(`DevPod did not attach '${options.repoPath}' after startup.`);
@@ -6448,6 +7071,7 @@ function startDevpodWorkspace(options) {
6448
7071
  `DevPod did not attach '${options.repoPath}' as '${devpodId}' after startup.`
6449
7072
  );
6450
7073
  }
7074
+ resetWorkspaceRuntimeCaches();
6451
7075
  return devpodId;
6452
7076
  } catch (error) {
6453
7077
  const message = error instanceof Error ? error.message : String(error);
@@ -6455,17 +7079,19 @@ function startDevpodWorkspace(options) {
6455
7079
  }
6456
7080
  });
6457
7081
  }
6458
- var import_node_child_process11, import_node_fs17, import_node_path16, DEVPOD_MUTATION_LOCK_FILE, DEVPOD_MUTATION_WAIT_MS, DevpodStartPostconditionError;
7082
+ var import_node_child_process15, import_node_fs19, import_node_path18, DEVPOD_MUTATION_LOCK_FILE, DEVPOD_MUTATION_WAIT_MS, DevpodStartPostconditionError;
6459
7083
  var init_devpod_mutation = __esm({
6460
7084
  "src/core/devpod-mutation.ts"() {
6461
7085
  "use strict";
6462
- import_node_child_process11 = require("child_process");
6463
- import_node_fs17 = __toESM(require("fs"));
6464
- import_node_path16 = __toESM(require("path"));
7086
+ import_node_child_process15 = require("child_process");
7087
+ import_node_fs19 = __toESM(require("fs"));
7088
+ import_node_path18 = __toESM(require("path"));
6465
7089
  init_devpod_workspaces();
7090
+ init_devsy_mutation();
6466
7091
  init_file_lock();
6467
7092
  init_router();
6468
- DEVPOD_MUTATION_LOCK_FILE = import_node_path16.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
7093
+ init_workspace_runtime();
7094
+ DEVPOD_MUTATION_LOCK_FILE = import_node_path18.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
6469
7095
  DEVPOD_MUTATION_WAIT_MS = 6e4;
6470
7096
  DevpodStartPostconditionError = class extends Error {
6471
7097
  };
@@ -6479,7 +7105,7 @@ function commandError(command, repoPath, stderr) {
6479
7105
  );
6480
7106
  }
6481
7107
  function resolveGitCommonDir(repoPath) {
6482
- const result = (0, import_node_child_process12.spawnSync)("git", ["-C", repoPath, "rev-parse", "--git-common-dir"], {
7108
+ const result = (0, import_node_child_process16.spawnSync)("git", ["-C", repoPath, "rev-parse", "--git-common-dir"], {
6483
7109
  encoding: "utf-8",
6484
7110
  env: READ_ONLY_GIT_ENV
6485
7111
  });
@@ -6487,10 +7113,10 @@ function resolveGitCommonDir(repoPath) {
6487
7113
  if (result.status !== 0 || !output2) {
6488
7114
  throw commandError("Could not resolve the Git common directory", repoPath, result.stderr);
6489
7115
  }
6490
- return comparableWorkspacePath(import_node_path17.default.isAbsolute(output2) ? output2 : import_node_path17.default.resolve(repoPath, output2));
7116
+ return comparableWorkspacePath(import_node_path19.default.isAbsolute(output2) ? output2 : import_node_path19.default.resolve(repoPath, output2));
6491
7117
  }
6492
7118
  function resolveGitTopLevel(repoPath) {
6493
- const result = (0, import_node_child_process12.spawnSync)("git", ["-C", repoPath, "rev-parse", "--show-toplevel"], {
7119
+ const result = (0, import_node_child_process16.spawnSync)("git", ["-C", repoPath, "rev-parse", "--show-toplevel"], {
6494
7120
  encoding: "utf-8",
6495
7121
  env: READ_ONLY_GIT_ENV
6496
7122
  });
@@ -6501,7 +7127,7 @@ function resolveGitTopLevel(repoPath) {
6501
7127
  return comparableWorkspacePath(output2);
6502
7128
  }
6503
7129
  function listGitWorktrees(repoPath) {
6504
- const result = (0, import_node_child_process12.spawnSync)("git", ["-C", repoPath, "worktree", "list", "--porcelain"], {
7130
+ const result = (0, import_node_child_process16.spawnSync)("git", ["-C", repoPath, "worktree", "list", "--porcelain"], {
6505
7131
  encoding: "utf-8",
6506
7132
  env: READ_ONLY_GIT_ENV
6507
7133
  });
@@ -6538,7 +7164,7 @@ function listGitWorktrees(repoPath) {
6538
7164
  return worktrees;
6539
7165
  }
6540
7166
  function ownershipDirectory(repoPath) {
6541
- return import_node_path17.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
7167
+ return import_node_path19.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
6542
7168
  }
6543
7169
  function validateWorkspace(value, label) {
6544
7170
  if (typeof value !== "string" || wsFromBranch(value) !== value) {
@@ -6566,7 +7192,7 @@ function validateRecord(value, expectedWorkspace) {
6566
7192
  `workspace ownership file '${expectedWorkspace}' contains identity '${workspace}'`
6567
7193
  );
6568
7194
  }
6569
- if (typeof candidate.worktreePath !== "string" || !import_node_path17.default.isAbsolute(candidate.worktreePath)) {
7195
+ if (typeof candidate.worktreePath !== "string" || !import_node_path19.default.isAbsolute(candidate.worktreePath)) {
6570
7196
  throw new Error("invalid workspace ownership worktreePath");
6571
7197
  }
6572
7198
  if (candidate.branch !== null && typeof candidate.branch !== "string") {
@@ -6586,7 +7212,7 @@ function validateRecord(value, expectedWorkspace) {
6586
7212
  function readRecordFile(filePath, expectedWorkspace) {
6587
7213
  let parsed;
6588
7214
  try {
6589
- parsed = JSON.parse(import_node_fs18.default.readFileSync(filePath, "utf-8"));
7215
+ parsed = JSON.parse(import_node_fs20.default.readFileSync(filePath, "utf-8"));
6590
7216
  } catch (error) {
6591
7217
  if (error instanceof SyntaxError) {
6592
7218
  throw new Error(`invalid workspace ownership JSON at '${filePath}'`);
@@ -6602,7 +7228,7 @@ function listWorkspaceOwnership(repoPath) {
6602
7228
  function listWorkspaceOwnershipInDirectory(directory) {
6603
7229
  let entries;
6604
7230
  try {
6605
- entries = import_node_fs18.default.readdirSync(directory, { withFileTypes: true });
7231
+ entries = import_node_fs20.default.readdirSync(directory, { withFileTypes: true });
6606
7232
  } catch (error) {
6607
7233
  if (error.code === "ENOENT") return [];
6608
7234
  throw error;
@@ -6610,14 +7236,14 @@ function listWorkspaceOwnershipInDirectory(directory) {
6610
7236
  return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
6611
7237
  const workspace = entry.name.slice(0, -".json".length);
6612
7238
  validateWorkspace(workspace, "filename");
6613
- return readRecordFile(import_node_path17.default.join(directory, entry.name), workspace);
7239
+ return readRecordFile(import_node_path19.default.join(directory, entry.name), workspace);
6614
7240
  });
6615
7241
  }
6616
7242
  function writeWorkspaceOwnershipInDirectory(directory, input2) {
6617
7243
  const workspace = validateWorkspace(input2.workspace, "workspace");
6618
7244
  const devpodId = validateWorkspace(input2.devpodId, "devpodId");
6619
7245
  const worktreePath = comparableWorkspacePath(input2.worktreePath);
6620
- const filePath = import_node_path17.default.join(directory, `${workspace}.json`);
7246
+ const filePath = import_node_path19.default.join(directory, `${workspace}.json`);
6621
7247
  const now = (/* @__PURE__ */ new Date()).toISOString();
6622
7248
  const records = listWorkspaceOwnershipInDirectory(directory);
6623
7249
  const existing = records.find((record2) => record2.workspace === workspace);
@@ -6653,9 +7279,9 @@ function writeWorkspaceOwnershipInDirectory(directory, input2) {
6653
7279
  return record;
6654
7280
  }
6655
7281
  function removeWorkspaceOwnershipInDirectory(directory, workspace) {
6656
- const filePath = import_node_path17.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
7282
+ const filePath = import_node_path19.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
6657
7283
  try {
6658
- import_node_fs18.default.rmSync(filePath);
7284
+ import_node_fs20.default.rmSync(filePath);
6659
7285
  return true;
6660
7286
  } catch (error) {
6661
7287
  if (error.code === "ENOENT") return false;
@@ -6666,7 +7292,7 @@ function sameOwnershipRecord(left, right) {
6666
7292
  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
7293
  }
6668
7294
  function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
6669
- const filePath = import_node_path17.default.join(
7295
+ const filePath = import_node_path19.default.join(
6670
7296
  directory,
6671
7297
  `${validateWorkspace(expected.workspace, "workspace")}.json`
6672
7298
  );
@@ -6678,14 +7304,14 @@ function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
6678
7304
  throw error;
6679
7305
  }
6680
7306
  if (!sameOwnershipRecord(current, expected)) return "changed";
6681
- import_node_fs18.default.rmSync(filePath);
7307
+ import_node_fs20.default.rmSync(filePath);
6682
7308
  return "removed";
6683
7309
  }
6684
7310
  function withWorkspaceOwnershipTransaction(repoPath, operation) {
6685
7311
  const directory = ownershipDirectory(repoPath);
6686
- import_node_fs18.default.mkdirSync(directory, { recursive: true });
7312
+ import_node_fs20.default.mkdirSync(directory, { recursive: true });
6687
7313
  return withFileLockSync(
6688
- import_node_path17.default.join(directory, ".lock"),
7314
+ import_node_path19.default.join(directory, ".lock"),
6689
7315
  { activity: "workspace ownership transaction", target: `'${repoPath}'`, waitMs: 5e3 },
6690
7316
  () => operation({
6691
7317
  list: () => listWorkspaceOwnershipInDirectory(directory),
@@ -6716,7 +7342,7 @@ function inspectWorkspaceOwnership(record, worktrees, devpods) {
6716
7342
  if (worktree?.locked) {
6717
7343
  return { ownerStatus: "locked", devpodStatus, worktree };
6718
7344
  }
6719
- if ((!worktree || worktree.prunable) && import_node_fs18.default.existsSync(record.worktreePath)) {
7345
+ if ((!worktree || worktree.prunable) && import_node_fs20.default.existsSync(record.worktreePath)) {
6720
7346
  return { ownerStatus: "conflict", devpodStatus, worktree };
6721
7347
  }
6722
7348
  if (!worktree || worktree.prunable) {
@@ -6740,20 +7366,20 @@ function listMissingWorkspaceOwnership(repoPath) {
6740
7366
  (record) => inspectWorkspaceOwnership(record, worktrees, void 0).ownerStatus === "missing"
6741
7367
  );
6742
7368
  }
6743
- var import_node_child_process12, import_node_fs18, import_node_path17, READ_ONLY_GIT_ENV, OWNERSHIP_VERSION, OWNERSHIP_DIR;
7369
+ var import_node_child_process16, import_node_fs20, import_node_path19, READ_ONLY_GIT_ENV, OWNERSHIP_VERSION, OWNERSHIP_DIR;
6744
7370
  var init_workspace_ownership = __esm({
6745
7371
  "src/core/workspace-ownership.ts"() {
6746
7372
  "use strict";
6747
- import_node_child_process12 = require("child_process");
6748
- import_node_fs18 = __toESM(require("fs"));
6749
- import_node_path17 = __toESM(require("path"));
7373
+ import_node_child_process16 = require("child_process");
7374
+ import_node_fs20 = __toESM(require("fs"));
7375
+ import_node_path19 = __toESM(require("path"));
6750
7376
  init_atomic_file();
6751
7377
  init_devpod_workspaces();
6752
7378
  init_file_lock();
6753
7379
  init_workspace();
6754
7380
  READ_ONLY_GIT_ENV = { ...process.env, GIT_OPTIONAL_LOCKS: "0" };
6755
7381
  OWNERSHIP_VERSION = 1;
6756
- OWNERSHIP_DIR = import_node_path17.default.join("devrouter", "workspaces");
7382
+ OWNERSHIP_DIR = import_node_path19.default.join("devrouter", "workspaces");
6757
7383
  }
6758
7384
  });
6759
7385
 
@@ -6767,10 +7393,10 @@ function inRepositoryWorkspaceScope(repoPath, worktreePath, livePaths) {
6767
7393
  if (livePaths.some((candidate) => sameWorkspacePath(candidate, worktreePath))) return true;
6768
7394
  const comparableRepo = comparableWorkspacePath(repoPath);
6769
7395
  const comparableWorktree = comparableWorkspacePath(worktreePath);
6770
- const localRoot = import_node_path18.default.join(comparableRepo, "trees") + import_node_path18.default.sep;
7396
+ const localRoot = import_node_path20.default.join(comparableRepo, "trees") + import_node_path20.default.sep;
6771
7397
  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);
7398
+ const legacyPrefix = `${import_node_path20.default.basename(comparableRepo)}-`;
7399
+ return import_node_path20.default.dirname(comparableWorktree) === import_node_path20.default.dirname(comparableRepo) && import_node_path20.default.basename(comparableWorktree).startsWith(legacyPrefix);
6774
7400
  }
6775
7401
  function previewActions(devpodStatus, routeCount, includeRecord) {
6776
7402
  const actions = [
@@ -6886,7 +7512,7 @@ function revalidateCandidate(candidate, repoPath, transaction) {
6886
7512
  const fresh = ownedCandidate(
6887
7513
  record,
6888
7514
  listGitWorktrees(repoPath),
6889
- listDevpodWorkspaces(),
7515
+ listDevpodWorkspaces(record.worktreePath),
6890
7516
  listHostRouteState()
6891
7517
  );
6892
7518
  if (fresh.eligible) return { status: "ready", candidate: fresh, record };
@@ -6987,11 +7613,20 @@ function inspectWorkspaceGc(repo) {
6987
7613
  const repoPath = comparableWorkspacePath(resolveRepoPath(repo));
6988
7614
  const worktrees = listGitWorktrees(repoPath);
6989
7615
  const records = listWorkspaceOwnership(repoPath);
6990
- const devpods = listDevpodWorkspaces();
6991
7616
  const routes = listHostRouteState();
6992
7617
  const candidates = [
6993
- ...records.map((record) => ownedCandidate(record, worktrees, devpods, routes)),
6994
- ...legacyCandidates(repoPath, records, worktrees, devpods, routes)
7618
+ // Per-record resolution keeps mixed DevPod+Devsy fleets on the registry
7619
+ // that owns each record (snapshot reads: the apply path revalidates
7620
+ // live); legacy detection scans both registries.
7621
+ ...records.map(
7622
+ (record) => ownedCandidate(
7623
+ record,
7624
+ worktrees,
7625
+ listDevpodWorkspacesFromSnapshots(record.worktreePath),
7626
+ routes
7627
+ )
7628
+ ),
7629
+ ...legacyCandidates(repoPath, records, worktrees, listMergedRuntimeWorkspaces(), routes)
6995
7630
  ];
6996
7631
  return {
6997
7632
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -7013,11 +7648,11 @@ function applyWorkspaceGc(plan) {
7013
7648
  candidates
7014
7649
  };
7015
7650
  }
7016
- var import_node_path18;
7651
+ var import_node_path20;
7017
7652
  var init_workspace_gc = __esm({
7018
7653
  "src/core/workspace-gc.ts"() {
7019
7654
  "use strict";
7020
- import_node_path18 = __toESM(require("path"));
7655
+ import_node_path20 = __toESM(require("path"));
7021
7656
  init_devpod_mutation();
7022
7657
  init_devpod_workspaces();
7023
7658
  init_host_routes();
@@ -7087,11 +7722,11 @@ function inspectPostgresCredentials(repoPath, config) {
7087
7722
  parseErrors.push(`${app.name}: ${composeFile} (${message})`);
7088
7723
  continue;
7089
7724
  }
7090
- if (!import_node_fs19.default.existsSync(absolutePath)) {
7725
+ if (!import_node_fs21.default.existsSync(absolutePath)) {
7091
7726
  continue;
7092
7727
  }
7093
7728
  try {
7094
- const raw = import_node_fs19.default.readFileSync(absolutePath, "utf-8");
7729
+ const raw = import_node_fs21.default.readFileSync(absolutePath, "utf-8");
7095
7730
  const parsed = import_yaml5.default.parse(raw);
7096
7731
  const root = asRecord2(parsed);
7097
7732
  const services = asRecord2(root?.services);
@@ -7370,7 +8005,7 @@ async function buildDoctorReport(options = {}) {
7370
8005
  const config = runtimeConfig.config;
7371
8006
  loadedConfig = config;
7372
8007
  loadedWorkspace = runtimeConfig.workspace;
7373
- const cliVersion = true ? "0.0.39" : "0.0.0-dev";
8008
+ const cliVersion = true ? "0.0.41" : "0.0.0-dev";
7374
8009
  const configVersion = config.devrouter?.version;
7375
8010
  if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
7376
8011
  addCheck(checks, {
@@ -7400,9 +8035,9 @@ async function buildDoctorReport(options = {}) {
7400
8035
  (app) => app.docker.composeFiles.map((filePath) => ({
7401
8036
  app: app.name,
7402
8037
  filePath,
7403
- absolutePath: import_node_path19.default.resolve(repo.path, filePath)
8038
+ absolutePath: import_node_path21.default.resolve(repo.path, filePath)
7404
8039
  }))
7405
- ).filter((entry) => !import_node_fs19.default.existsSync(entry.absolutePath));
8040
+ ).filter((entry) => !import_node_fs21.default.existsSync(entry.absolutePath));
7406
8041
  addCheck(checks, {
7407
8042
  id: "repo.compose-files",
7408
8043
  level: missingComposeFiles.length === 0 ? "ok" : "error",
@@ -7445,8 +8080,8 @@ async function buildDoctorReport(options = {}) {
7445
8080
  const missingHostCwds = config.apps.filter((app) => app.runtime === "host").map((app) => ({
7446
8081
  app: app.name,
7447
8082
  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));
8083
+ absolutePath: import_node_path21.default.resolve(repo.path, app.hostRun.cwd)
8084
+ })).filter((entry) => !import_node_fs21.default.existsSync(entry.absolutePath));
7450
8085
  addCheck(checks, {
7451
8086
  id: "repo.host-cwd",
7452
8087
  level: missingHostCwds.length === 0 ? "ok" : "error",
@@ -7590,12 +8225,12 @@ async function buildDoctorReport(options = {}) {
7590
8225
  nextSteps
7591
8226
  };
7592
8227
  }
7593
- var import_node_fs19, import_node_path19, import_yaml5, POSTGRES_DEFAULTS;
8228
+ var import_node_fs21, import_node_path21, import_yaml5, POSTGRES_DEFAULTS;
7594
8229
  var init_doctor = __esm({
7595
8230
  "src/core/doctor.ts"() {
7596
8231
  "use strict";
7597
- import_node_fs19 = __toESM(require("fs"));
7598
- import_node_path19 = __toESM(require("path"));
8232
+ import_node_fs21 = __toESM(require("fs"));
8233
+ import_node_path21 = __toESM(require("path"));
7599
8234
  import_yaml5 = __toESM(require("yaml"));
7600
8235
  init_docker();
7601
8236
  init_host_routes();
@@ -7702,6 +8337,32 @@ async function runSetup(options = {}) {
7702
8337
  })
7703
8338
  );
7704
8339
  }
8340
+ if (options.workspaceRuntime !== void 0 || options.devsyInactivityTimeout !== void 0) {
8341
+ try {
8342
+ const existing = readWorkspaceRuntimeConfig();
8343
+ const next = {
8344
+ runtime: options.workspaceRuntime ? parseWorkspaceRuntime(options.workspaceRuntime) : existing.runtime,
8345
+ devsyInactivityTimeout: options.devsyInactivityTimeout ?? existing.devsyInactivityTimeout
8346
+ };
8347
+ writeWorkspaceRuntimeConfig(next);
8348
+ actions.push(
8349
+ action("performed", {
8350
+ id: "global.workspace-runtime-config",
8351
+ summary: `Persisted workspace runtime preferences (runtime=${next.runtime ?? "unset"}, devsyInactivityTimeout=${next.devsyInactivityTimeout ?? "unset"}).`
8352
+ })
8353
+ );
8354
+ } catch (error) {
8355
+ const message = error instanceof Error ? error.message : String(error);
8356
+ actions.push(
8357
+ action("failed", {
8358
+ id: "global.workspace-runtime-config",
8359
+ summary: "Failed to persist workspace runtime preferences.",
8360
+ details: message,
8361
+ suggestion: "Use: devrouter setup --yes --workspace-runtime <devpod|devsy> [--devsy-inactivity-timeout <duration>]"
8362
+ })
8363
+ );
8364
+ }
8365
+ }
7705
8366
  try {
7706
8367
  const existed = await networkExists(DEVNET_NAME);
7707
8368
  await ensureNetwork(DEVNET_NAME);
@@ -7797,6 +8458,7 @@ var init_setup = __esm({
7797
8458
  init_router();
7798
8459
  init_tls();
7799
8460
  init_tool_diagnostics();
8461
+ init_workspace_runtime();
7800
8462
  }
7801
8463
  });
7802
8464
 
@@ -7806,7 +8468,12 @@ __export(setup_exports, {
7806
8468
  runSetupCommand: () => runSetupCommand
7807
8469
  });
7808
8470
  async function runSetupCommand(options) {
7809
- const report = await runSetup({ repo: options.repo, yes: Boolean(options.yes) });
8471
+ const report = await runSetup({
8472
+ repo: options.repo,
8473
+ yes: Boolean(options.yes),
8474
+ workspaceRuntime: options.workspaceRuntime,
8475
+ devsyInactivityTimeout: options.devsyInactivityTimeout
8476
+ });
7810
8477
  if (options.json) {
7811
8478
  printJSON(report);
7812
8479
  } else {
@@ -7853,12 +8520,12 @@ function parseSsPortListeners(stdout, port) {
7853
8520
  return listeners;
7854
8521
  }
7855
8522
  function findPortListeners(port) {
7856
- const result = (0, import_node_child_process13.spawnSync)("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN"], {
8523
+ const result = (0, import_node_child_process17.spawnSync)("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN"], {
7857
8524
  encoding: "utf-8"
7858
8525
  });
7859
8526
  if (result.error && result.error.code === "ENOENT") {
7860
8527
  if (process.platform === "linux") {
7861
- const ssResult = (0, import_node_child_process13.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
8528
+ const ssResult = (0, import_node_child_process17.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
7862
8529
  if (ssResult.status === 0 && ssResult.stdout) {
7863
8530
  return parseSsPortListeners(ssResult.stdout, port);
7864
8531
  }
@@ -7887,11 +8554,11 @@ function findPortListeners(port) {
7887
8554
  };
7888
8555
  });
7889
8556
  }
7890
- var import_node_child_process13;
8557
+ var import_node_child_process17;
7891
8558
  var init_ports = __esm({
7892
8559
  "src/util/ports.ts"() {
7893
8560
  "use strict";
7894
- import_node_child_process13 = require("child_process");
8561
+ import_node_child_process17 = require("child_process");
7895
8562
  }
7896
8563
  });
7897
8564
 
@@ -7968,18 +8635,18 @@ function probeHttpRoute(host, options = {}) {
7968
8635
  args.push("--cacert", CERT_FILE);
7969
8636
  }
7970
8637
  args.push(url);
7971
- const result = (0, import_node_child_process14.spawnSync)("curl", args, { encoding: "utf-8" });
8638
+ const result = (0, import_node_child_process18.spawnSync)("curl", args, { encoding: "utf-8" });
7972
8639
  const stdout = result.stdout?.trim() ?? "";
7973
8640
  const status = /^\d{3}$/.test(stdout) ? Number(stdout) : void 0;
7974
8641
  const ok = result.status === 0 && status !== void 0 && status >= 100 && status < 500;
7975
8642
  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
8643
  return { ok, status, details };
7977
8644
  }
7978
- var import_node_child_process14;
8645
+ var import_node_child_process18;
7979
8646
  var init_http_route_probe = __esm({
7980
8647
  "src/core/http-route-probe.ts"() {
7981
8648
  "use strict";
7982
- import_node_child_process14 = require("child_process");
8649
+ import_node_child_process18 = require("child_process");
7983
8650
  init_router();
7984
8651
  init_tls();
7985
8652
  }
@@ -8058,11 +8725,11 @@ var init_route_publication = __esm({
8058
8725
  // src/core/workspace-ensure.ts
8059
8726
  function assertOverlay(container, repoPath) {
8060
8727
  const workingDir = container.labels["com.docker.compose.project.working_dir"];
8061
- if (!workingDir || !sameWorkspacePath(workingDir, import_node_path20.default.join(repoPath, ".devcontainer"))) {
8728
+ if (!workingDir || !sameWorkspacePath(workingDir, import_node_path22.default.join(repoPath, ".devcontainer"))) {
8062
8729
  throw new Error(`Container '${container.id}' does not belong to the exact worktree.`);
8063
8730
  }
8064
8731
  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);
8732
+ const expectedOverlay = import_node_path22.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
8066
8733
  if (!configFiles.some((configFile) => sameWorkspacePath(configFile, expectedOverlay))) {
8067
8734
  throw new Error(`Container '${container.id}' was not started with ${DEVCONTAINER_OVERLAY}.`);
8068
8735
  }
@@ -8217,6 +8884,18 @@ function restorePreviousManagedConfig(options) {
8217
8884
  }
8218
8885
  writeManagedDevcontainerConfig(restoredPlan);
8219
8886
  }
8887
+ function restoreFirstTransitionManagedConfig(options) {
8888
+ const restoredPlan = inspectManagedDevcontainerConfig({
8889
+ repoPath: options.repoPath,
8890
+ config: options.config,
8891
+ profile: {
8892
+ apps: [],
8893
+ devcontainerServices: [...options.services]
8894
+ },
8895
+ linked: options.linked
8896
+ });
8897
+ writeManagedDevcontainerConfig(restoredPlan);
8898
+ }
8220
8899
  function isWarmWorkspaceActive(repoPath) {
8221
8900
  try {
8222
8901
  return workspaceAppContainers(inspectWorkspaceContainers(), repoPath).some(
@@ -8327,7 +9006,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
8327
9006
  target
8328
9007
  });
8329
9008
  if (target.kind === "linked") {
8330
- const workspaceEnv = (0, import_node_child_process15.spawnSync)(
9009
+ const workspaceEnv = (0, import_node_child_process19.spawnSync)(
8331
9010
  "docker",
8332
9011
  ["exec", appContainer.id, "printenv", "WORKSPACE"],
8333
9012
  { encoding: "utf-8" }
@@ -8337,7 +9016,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
8337
9016
  `Workspace app container must expose WORKSPACE='${target.workspace}' (got '${workspaceEnv.stdout.trim() || "(empty)"}').`
8338
9017
  );
8339
9018
  }
8340
- const devrouterWorkspaceEnv = (0, import_node_child_process15.spawnSync)(
9019
+ const devrouterWorkspaceEnv = (0, import_node_child_process19.spawnSync)(
8341
9020
  "docker",
8342
9021
  ["exec", appContainer.id, "printenv", "DEVROUTER_WORKSPACE"],
8343
9022
  { encoding: "utf-8" }
@@ -8348,7 +9027,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
8348
9027
  );
8349
9028
  }
8350
9029
  }
8351
- const gitCheck = (0, import_node_child_process15.spawnSync)(
9030
+ const gitCheck = (0, import_node_child_process19.spawnSync)(
8352
9031
  "docker",
8353
9032
  [
8354
9033
  "exec",
@@ -8402,7 +9081,7 @@ async function waitForHttpRoutes(repoPath, apps, timeoutMs) {
8402
9081
  );
8403
9082
  }
8404
9083
  function resolveLinkedTarget(repoPath) {
8405
- const devpods = listDevpodWorkspaces();
9084
+ const devpods = listDevpodWorkspaces(repoPath);
8406
9085
  const existingDevpod = selectDevpodWorkspace(devpods, repoPath);
8407
9086
  const persisted = readPersistedWorkspace(repoPath);
8408
9087
  const candidate = existingDevpod?.id ?? persisted ?? resolveWorktreeWorkspace(repoPath);
@@ -8426,7 +9105,7 @@ function resolveLinkedTarget(repoPath) {
8426
9105
  };
8427
9106
  }
8428
9107
  function resolvePrimaryTarget(repoPath) {
8429
- const existingDevpod = selectDevpodWorkspace(listDevpodWorkspaces(), repoPath);
9108
+ const existingDevpod = selectDevpodWorkspace(listDevpodWorkspaces(repoPath), repoPath);
8430
9109
  return {
8431
9110
  kind: "primary",
8432
9111
  devpodId: existingDevpod?.id,
@@ -8435,14 +9114,14 @@ function resolvePrimaryTarget(repoPath) {
8435
9114
  }
8436
9115
  function isPrimaryCheckout(repoPath) {
8437
9116
  try {
8438
- return import_node_fs20.default.statSync(import_node_path20.default.join(repoPath, ".git")).isDirectory();
9117
+ return import_node_fs22.default.statSync(import_node_path22.default.join(repoPath, ".git")).isDirectory();
8439
9118
  } catch {
8440
9119
  return false;
8441
9120
  }
8442
9121
  }
8443
9122
  function openUrls(urls) {
8444
9123
  for (const url of urls) {
8445
- const opened = (0, import_node_child_process15.spawnSync)("open", [url], { encoding: "utf-8" });
9124
+ const opened = (0, import_node_child_process19.spawnSync)("open", [url], { encoding: "utf-8" });
8446
9125
  if (opened.status !== 0) {
8447
9126
  process.stderr.write(`Warning: could not open '${url}'.
8448
9127
  `);
@@ -8485,8 +9164,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
8485
9164
  const target = linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
8486
9165
  let devpodId = target.devpodId;
8487
9166
  if (target.kind === "linked") {
8488
- const overlayPath = import_node_path20.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
8489
- if (!import_node_fs20.default.existsSync(overlayPath)) {
9167
+ const overlayPath = import_node_path22.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
9168
+ if (!import_node_fs22.default.existsSync(overlayPath)) {
8490
9169
  throw new Error(`Missing required DevPod compose overlay: ${overlayPath}`);
8491
9170
  }
8492
9171
  }
@@ -8553,7 +9232,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
8553
9232
  }
8554
9233
  const apps = proxyAppsFromConfig(runtime.config);
8555
9234
  const parsedUpstreams = apps.map((app) => parseUpstream(app.upstream));
8556
- const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path20.default.basename(repoPath)) ?? "app";
9235
+ const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path22.default.basename(repoPath)) ?? "app";
8557
9236
  for (const [index, app] of apps.entries()) {
8558
9237
  if (!parsedUpstreams[index].host.startsWith(`${aliasPrefix}-`)) {
8559
9238
  const owner = target.kind === "linked" ? "workspace" : "checkout";
@@ -8824,8 +9503,13 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
8824
9503
  linked,
8825
9504
  previousState: previousManagedState
8826
9505
  });
8827
- } else {
8828
- removeManagedDevcontainerConfig(managedPlan);
9506
+ } else if (managedRuntimeConfig) {
9507
+ restoreFirstTransitionManagedConfig({
9508
+ repoPath,
9509
+ config: managedRuntimeConfig,
9510
+ linked,
9511
+ services: previousServices
9512
+ });
8829
9513
  }
8830
9514
  }
8831
9515
  } catch (rollbackError) {
@@ -8952,7 +9636,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
8952
9636
  linked,
8953
9637
  previousState: previousManagedState
8954
9638
  });
8955
- } else {
9639
+ } else if (!environmentStarted) {
8956
9640
  removeManagedDevcontainerConfig(managedPlan);
8957
9641
  }
8958
9642
  } catch (rollbackError) {
@@ -8992,13 +9676,13 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
8992
9676
  }
8993
9677
  });
8994
9678
  }
8995
- var import_node_child_process15, import_node_fs20, import_node_path20, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
9679
+ var import_node_child_process19, import_node_fs22, import_node_path22, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
8996
9680
  var init_workspace_ensure = __esm({
8997
9681
  "src/core/workspace-ensure.ts"() {
8998
9682
  "use strict";
8999
- import_node_child_process15 = require("child_process");
9000
- import_node_fs20 = __toESM(require("fs"));
9001
- import_node_path20 = __toESM(require("path"));
9683
+ import_node_child_process19 = require("child_process");
9684
+ import_node_fs22 = __toESM(require("fs"));
9685
+ import_node_path22 = __toESM(require("path"));
9002
9686
  init_devcontainer_profile();
9003
9687
  init_devpod_environment();
9004
9688
  init_devpod_mutation();
@@ -9078,20 +9762,20 @@ function warnMissingWorkspaceOwnership(repoPath) {
9078
9762
  );
9079
9763
  }
9080
9764
  function defaultWorktreePath(mainRepo, ws) {
9081
- return import_node_path21.default.join(mainRepo, "trees", ws);
9765
+ return import_node_path23.default.join(mainRepo, "trees", ws);
9082
9766
  }
9083
9767
  function assertDefaultWorktreeRootIgnored(mainRepo) {
9084
- const ignored = (0, import_node_child_process16.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
9768
+ const ignored = (0, import_node_child_process20.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
9085
9769
  encoding: "utf-8"
9086
9770
  });
9087
9771
  if (ignored.status !== 0) {
9088
9772
  throw new Error(
9089
- `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.`
9773
+ `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.`
9090
9774
  );
9091
9775
  }
9092
9776
  }
9093
9777
  function legacyDefaultWorktreePath(mainRepo, ws) {
9094
- return import_node_path21.default.join(import_node_path21.default.dirname(mainRepo), `${import_node_path21.default.basename(mainRepo)}-${ws}`);
9778
+ return import_node_path23.default.join(import_node_path23.default.dirname(mainRepo), `${import_node_path23.default.basename(mainRepo)}-${ws}`);
9095
9779
  }
9096
9780
  function teardownFallbackPath(mainRepo, workspace) {
9097
9781
  const candidates = [
@@ -9203,13 +9887,13 @@ function assertFullDownPreflight(mainRepo, target) {
9203
9887
  if (sameWorkspacePath(target.worktreePath, mainRepo)) {
9204
9888
  throw new Error("Refusing to remove the primary Git checkout.");
9205
9889
  }
9206
- if (!target.worktree || target.worktree.prunable || !import_node_fs21.default.existsSync(target.worktreePath)) return;
9890
+ if (!target.worktree || target.worktree.prunable || !import_node_fs23.default.existsSync(target.worktreePath)) return;
9207
9891
  if (target.worktree.locked) {
9208
9892
  throw new Error(
9209
9893
  `Worktree '${target.worktreePath}' is locked; unlock it before workspace down.`
9210
9894
  );
9211
9895
  }
9212
- const status = (0, import_node_child_process16.spawnSync)(
9896
+ const status = (0, import_node_child_process20.spawnSync)(
9213
9897
  "git",
9214
9898
  ["-C", target.worktreePath, "status", "--porcelain", "--untracked-files=normal"],
9215
9899
  { encoding: "utf-8" }
@@ -9230,8 +9914,8 @@ async function workspaceUp(branch, opts = {}) {
9230
9914
  if (!ws) {
9231
9915
  throw new Error(`Branch '${branch}' does not yield a valid workspace token.`);
9232
9916
  }
9233
- const worktreePath = opts.path ? import_node_path21.default.resolve(opts.path) : defaultWorktreePath(mainRepo, ws);
9234
- if (import_node_fs21.default.existsSync(worktreePath)) {
9917
+ const worktreePath = opts.path ? import_node_path23.default.resolve(opts.path) : defaultWorktreePath(mainRepo, ws);
9918
+ if (import_node_fs23.default.existsSync(worktreePath)) {
9235
9919
  const registered = listGitWorktrees(mainRepo).find(
9236
9920
  (worktree) => sameWorkspacePath(worktree.path, worktreePath)
9237
9921
  );
@@ -9249,11 +9933,11 @@ async function workspaceUp(branch, opts = {}) {
9249
9933
  if (!opts.path) {
9250
9934
  assertDefaultWorktreeRootIgnored(mainRepo);
9251
9935
  }
9252
- const add = (0, import_node_child_process16.spawnSync)("git", ["-C", mainRepo, "worktree", "add", worktreePath, branch], {
9936
+ const add = (0, import_node_child_process20.spawnSync)("git", ["-C", mainRepo, "worktree", "add", worktreePath, branch], {
9253
9937
  encoding: "utf-8"
9254
9938
  });
9255
9939
  if (add.status !== 0) {
9256
- const addNew = (0, import_node_child_process16.spawnSync)(
9940
+ const addNew = (0, import_node_child_process20.spawnSync)(
9257
9941
  "git",
9258
9942
  ["-C", mainRepo, "worktree", "add", "-b", branch, worktreePath],
9259
9943
  {
@@ -9287,12 +9971,9 @@ function workspaceLs(repoPath) {
9287
9971
  const mainRepo = resolveRepoPath(repoPath);
9288
9972
  const worktrees = identifyGitWorktrees(mainRepo);
9289
9973
  const records = listWorkspaceOwnership(mainRepo);
9290
- let devpods;
9291
- try {
9292
- devpods = listDevpodWorkspaces();
9293
- } catch {
9294
- devpods = void 0;
9295
- }
9974
+ const devpodsForPath = (worktreePath) => {
9975
+ return listDevpodWorkspacesFromSnapshots(worktreePath);
9976
+ };
9296
9977
  const paths = Array.from(
9297
9978
  /* @__PURE__ */ new Set([
9298
9979
  ...worktrees.map((worktree) => worktree.path),
@@ -9305,6 +9986,7 @@ function workspaceLs(repoPath) {
9305
9986
  return paths.map((worktreePath) => {
9306
9987
  const worktree = worktreesByPath.get(worktreePath);
9307
9988
  const record = recordsByPath.get(worktreePath);
9989
+ const devpods = devpodsForPath(worktreePath);
9308
9990
  const status = record ? inspectWorkspaceOwnership(record, worktrees, devpods) : void 0;
9309
9991
  const matchingDevpods = devpods?.filter(
9310
9992
  (devpod) => sameWorkspacePath(devpod.source.localFolder, worktreePath)
@@ -9323,7 +10005,7 @@ function workspaceLs(repoPath) {
9323
10005
  });
9324
10006
  }
9325
10007
  function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = false) {
9326
- const devpods = listDevpodWorkspaces();
10008
+ const devpods = listDevpodWorkspaces(resolved.worktreePath);
9327
10009
  assertDevpodTargetSafe(resolved, worktrees, devpods);
9328
10010
  const devpodId = resolved.record?.devpodId ?? resolved.workspace;
9329
10011
  const mutation = action2 === "stop" ? stopOwnedDevpodWorkspace(devpodId, resolved.worktreePath) : deleteOwnedDevpodWorkspace(devpodId, resolved.worktreePath);
@@ -9358,8 +10040,8 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
9358
10040
  opts.quiet
9359
10041
  );
9360
10042
  if (removeWorktree) {
9361
- if (resolved.worktree && !resolved.worktree.prunable && import_node_fs21.default.existsSync(resolved.worktreePath)) {
9362
- const rm = (0, import_node_child_process16.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
10043
+ if (resolved.worktree && !resolved.worktree.prunable && import_node_fs23.default.existsSync(resolved.worktreePath)) {
10044
+ const rm = (0, import_node_child_process20.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
9363
10045
  encoding: "utf-8"
9364
10046
  });
9365
10047
  if (rm.status !== 0) {
@@ -9377,7 +10059,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
9377
10059
  }
9378
10060
  return result;
9379
10061
  };
9380
- return resolved.worktree && !resolved.worktree.prunable && import_node_fs21.default.existsSync(resolved.worktreePath) ? withWorkspaceLifecycleLock(resolved.worktreePath, operation) : operation();
10062
+ return resolved.worktree && !resolved.worktree.prunable && import_node_fs23.default.existsSync(resolved.worktreePath) ? withWorkspaceLifecycleLock(resolved.worktreePath, operation) : operation();
9381
10063
  }
9382
10064
  async function mutateWorkspaceOwnedPath(action2, worktreePath, opts = {}) {
9383
10065
  const mainRepo = resolveRepoPath(opts.repoPath);
@@ -9412,13 +10094,13 @@ async function workspaceStop(target, opts = {}) {
9412
10094
  async function workspaceDown(target, opts = {}) {
9413
10095
  return runWorkspaceLifecycle("down", target, opts);
9414
10096
  }
9415
- var import_node_child_process16, import_node_fs21, import_node_path21;
10097
+ var import_node_child_process20, import_node_fs23, import_node_path23;
9416
10098
  var init_workspace_lifecycle = __esm({
9417
10099
  "src/core/workspace-lifecycle.ts"() {
9418
10100
  "use strict";
9419
- import_node_child_process16 = require("child_process");
9420
- import_node_fs21 = __toESM(require("fs"));
9421
- import_node_path21 = __toESM(require("path"));
10101
+ import_node_child_process20 = require("child_process");
10102
+ import_node_fs23 = __toESM(require("fs"));
10103
+ import_node_path23 = __toESM(require("path"));
9422
10104
  init_devpod_mutation();
9423
10105
  init_devpod_workspaces();
9424
10106
  init_repo_config();
@@ -9468,7 +10150,7 @@ async function environmentStop(repoPath, options = {}) {
9468
10150
  }
9469
10151
  }
9470
10152
  return withWorkspaceLifecycleLock(repoPath, async () => {
9471
- const devpod = selectDevpodWorkspace(listDevpodWorkspaces(), repoPath);
10153
+ const devpod = selectDevpodWorkspace(listDevpodWorkspaces(repoPath), repoPath);
9472
10154
  const mutation = devpod ? options.delete ? deleteOwnedDevpodWorkspace(devpod.id, repoPath) : stopOwnedDevpodWorkspace(devpod.id, repoPath) : { status: "absent" };
9473
10155
  const removedRoutes = removeHostRoutesWhere(
9474
10156
  (route) => sameWorkspacePath(route.repoPath, repoPath)
@@ -9529,6 +10211,39 @@ var init_stop = __esm({
9529
10211
  }
9530
10212
  });
9531
10213
 
10214
+ // src/core/devsy-exec.ts
10215
+ function ensureGuidance(repoPath) {
10216
+ return `Run 'devrouter ensure ${repoPath}' first.`;
10217
+ }
10218
+ async function devsyExec(repoPath, command) {
10219
+ if (command.length === 0) {
10220
+ throw new Error("No command provided. Use `devrouter exec [path] -- <command...>`.");
10221
+ }
10222
+ return withWorkspaceLifecycleLock(repoPath, async () => {
10223
+ const workspace = selectDevsyWorkspace(listDevsyWorkspaces(), repoPath);
10224
+ if (!workspace) {
10225
+ throw new Error(
10226
+ `No exact Devsy workspace exists for '${repoPath}'. ${ensureGuidance(repoPath)}`
10227
+ );
10228
+ }
10229
+ const args = ["workspace", "exec", "--result-format", "plain", workspace.id, "--", ...command];
10230
+ return new Promise((resolve, reject) => {
10231
+ const child = (0, import_node_child_process21.spawn)("devsy", args, { stdio: "inherit" });
10232
+ child.once("error", (error) => reject(new Error(`devsy exec failed: ${error.message}`)));
10233
+ child.once("close", (code) => resolve(code ?? 1));
10234
+ });
10235
+ });
10236
+ }
10237
+ var import_node_child_process21;
10238
+ var init_devsy_exec = __esm({
10239
+ "src/core/devsy-exec.ts"() {
10240
+ "use strict";
10241
+ import_node_child_process21 = require("child_process");
10242
+ init_devsy_workspaces();
10243
+ init_workspace();
10244
+ }
10245
+ });
10246
+
9532
10247
  // src/core/devpod-exec.ts
9533
10248
  function stripDevpodCompletionDiagnostic(value) {
9534
10249
  const index = value.indexOf(DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC);
@@ -9541,27 +10256,27 @@ function stripDevpodCompletionDiagnostic(value) {
9541
10256
  function quotePosixArg(value) {
9542
10257
  return `'${value.replaceAll("'", `'"'"'`)}'`;
9543
10258
  }
9544
- function ensureGuidance(repoPath) {
10259
+ function ensureGuidance2(repoPath) {
9545
10260
  return `Run 'devrouter ensure ${repoPath}' first.`;
9546
10261
  }
9547
10262
  function assertRunningDevpod(devpodId, repoPath) {
9548
- const result = (0, import_node_child_process17.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
10263
+ const result = (0, import_node_child_process22.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
9549
10264
  encoding: "utf-8"
9550
10265
  });
9551
10266
  if (result.status !== 0) {
9552
10267
  const details = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
9553
10268
  throw new Error(
9554
- `Could not prove DevPod '${devpodId}' is running${details ? `: ${details}` : "."} ${ensureGuidance(repoPath)}`
10269
+ `Could not prove DevPod '${devpodId}' is running${details ? `: ${details}` : "."} ${ensureGuidance2(repoPath)}`
9555
10270
  );
9556
10271
  }
9557
10272
  let status;
9558
10273
  try {
9559
10274
  status = JSON.parse(result.stdout);
9560
10275
  } catch {
9561
- throw new Error(`DevPod '${devpodId}' returned invalid status. ${ensureGuidance(repoPath)}`);
10276
+ throw new Error(`DevPod '${devpodId}' returned invalid status. ${ensureGuidance2(repoPath)}`);
9562
10277
  }
9563
10278
  if (status.id !== devpodId || status.state !== "Running") {
9564
- throw new Error(`DevPod '${devpodId}' is not running. ${ensureGuidance(repoPath)}`);
10279
+ throw new Error(`DevPod '${devpodId}' is not running. ${ensureGuidance2(repoPath)}`);
9565
10280
  }
9566
10281
  }
9567
10282
  function resolveWorkspaceDirectory(repoPath) {
@@ -9569,17 +10284,20 @@ function resolveWorkspaceDirectory(repoPath) {
9569
10284
  return resolveRunningWorkspaceContainer(repoPath).workspacePath;
9570
10285
  } catch (error) {
9571
10286
  const message = error instanceof Error ? error.message : String(error);
9572
- throw new Error(`${message} ${ensureGuidance(repoPath)}`);
10287
+ throw new Error(`${message} ${ensureGuidance2(repoPath)}`);
9573
10288
  }
9574
10289
  }
9575
10290
  async function devpodExec(repoPath, command) {
10291
+ if (resolveWorkspaceRuntimeOrDefault(repoPath) === "devsy") {
10292
+ return devsyExec(repoPath, command);
10293
+ }
9576
10294
  if (command.length === 0) {
9577
10295
  throw new Error("No command provided. Use `devrouter exec [path] -- <command...>`.");
9578
10296
  }
9579
10297
  return withWorkspaceLifecycleLock(repoPath, async () => {
9580
- const devpod = selectDevpodWorkspace(listDevpodWorkspaces(), repoPath);
10298
+ const devpod = selectDevpodWorkspace(listDevpodWorkspaces(repoPath), repoPath);
9581
10299
  if (!devpod) {
9582
- throw new Error(`No exact DevPod exists for '${repoPath}'. ${ensureGuidance(repoPath)}`);
10300
+ throw new Error(`No exact DevPod exists for '${repoPath}'. ${ensureGuidance2(repoPath)}`);
9583
10301
  }
9584
10302
  assertRunningDevpod(devpod.id, repoPath);
9585
10303
  const workspaceDirectory = resolveWorkspaceDirectory(repoPath);
@@ -9601,7 +10319,7 @@ async function devpodExec(repoPath, command) {
9601
10319
  wrappedCommand
9602
10320
  ];
9603
10321
  return new Promise((resolve, reject) => {
9604
- const child = (0, import_node_child_process17.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
10322
+ const child = (0, import_node_child_process22.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
9605
10323
  let pending = Buffer.alloc(0);
9606
10324
  let statusBytes = Buffer.alloc(0);
9607
10325
  let readingStatus = false;
@@ -9660,15 +10378,17 @@ async function devpodExec(repoPath, command) {
9660
10378
  });
9661
10379
  });
9662
10380
  }
9663
- var import_node_child_process17, import_node_crypto7, DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC;
10381
+ var import_node_child_process22, import_node_crypto7, DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC;
9664
10382
  var init_devpod_exec = __esm({
9665
10383
  "src/core/devpod-exec.ts"() {
9666
10384
  "use strict";
9667
- import_node_child_process17 = require("child_process");
10385
+ import_node_child_process22 = require("child_process");
9668
10386
  import_node_crypto7 = require("crypto");
9669
10387
  init_devpod_environment();
9670
10388
  init_devpod_workspaces();
10389
+ init_devsy_exec();
9671
10390
  init_workspace();
10391
+ init_workspace_runtime();
9672
10392
  DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC = Buffer.from(
9673
10393
  "Error tunneling to container: wait: remote command exited without exit status or exit signal\n"
9674
10394
  );
@@ -9849,7 +10569,7 @@ async function runOpenCommand(name) {
9849
10569
  );
9850
10570
  return;
9851
10571
  }
9852
- const result = (0, import_node_child_process18.spawnSync)("open", [url], { encoding: "utf-8" });
10572
+ const result = (0, import_node_child_process23.spawnSync)("open", [url], { encoding: "utf-8" });
9853
10573
  if (result.status !== 0) {
9854
10574
  const details = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
9855
10575
  throw new Error(`Unable to open '${url}': ${details || "unknown error"}`);
@@ -9857,11 +10577,11 @@ async function runOpenCommand(name) {
9857
10577
  process.stdout.write(`Opened ${url}
9858
10578
  `);
9859
10579
  }
9860
- var import_node_child_process18;
10580
+ var import_node_child_process23;
9861
10581
  var init_open = __esm({
9862
10582
  "src/commands/open.ts"() {
9863
10583
  "use strict";
9864
- import_node_child_process18 = require("child_process");
10584
+ import_node_child_process23 = require("child_process");
9865
10585
  init_docker();
9866
10586
  init_host_routes();
9867
10587
  init_repo_config();
@@ -9884,7 +10604,7 @@ async function runLogsCommand(options) {
9884
10604
  const args = ["logs", "--tail", tail, ROUTER_CONTAINER_NAME];
9885
10605
  if (options.follow) {
9886
10606
  args.splice(1, 0, "-f");
9887
- const child = (0, import_node_child_process19.spawn)("docker", args, { stdio: "inherit" });
10607
+ const child = (0, import_node_child_process24.spawn)("docker", args, { stdio: "inherit" });
9888
10608
  const onSignal = () => {
9889
10609
  child.kill("SIGTERM");
9890
10610
  };
@@ -9898,17 +10618,17 @@ async function runLogsCommand(options) {
9898
10618
  });
9899
10619
  });
9900
10620
  } else {
9901
- const result = (0, import_node_child_process19.spawnSync)("docker", args, { stdio: "inherit" });
10621
+ const result = (0, import_node_child_process24.spawnSync)("docker", args, { stdio: "inherit" });
9902
10622
  if (result.status !== 0) {
9903
10623
  throw new Error("Failed to retrieve router logs.");
9904
10624
  }
9905
10625
  }
9906
10626
  }
9907
- var import_node_child_process19;
10627
+ var import_node_child_process24;
9908
10628
  var init_logs = __esm({
9909
10629
  "src/commands/logs.ts"() {
9910
10630
  "use strict";
9911
- import_node_child_process19 = require("child_process");
10631
+ import_node_child_process24 = require("child_process");
9912
10632
  init_docker();
9913
10633
  init_router();
9914
10634
  }
@@ -9948,17 +10668,17 @@ function asRecord3(value) {
9948
10668
  return value;
9949
10669
  }
9950
10670
  function readJson(filePath) {
9951
- if (!import_node_fs22.default.existsSync(filePath)) {
10671
+ if (!import_node_fs24.default.existsSync(filePath)) {
9952
10672
  return void 0;
9953
10673
  }
9954
10674
  try {
9955
- return JSON.parse(import_node_fs22.default.readFileSync(filePath, "utf-8"));
10675
+ return JSON.parse(import_node_fs24.default.readFileSync(filePath, "utf-8"));
9956
10676
  } catch {
9957
10677
  return void 0;
9958
10678
  }
9959
10679
  }
9960
10680
  function relative(repoPath, filePath) {
9961
- return import_node_path22.default.relative(repoPath, filePath) || ".";
10681
+ return import_node_path24.default.relative(repoPath, filePath) || ".";
9962
10682
  }
9963
10683
  function redactEnvAssignments(value) {
9964
10684
  return value.replace(
@@ -9999,7 +10719,7 @@ function inspectPackageManager(repoPath, pkg) {
9999
10719
  ["bun.lock", "bun"]
10000
10720
  ];
10001
10721
  for (const [fileName, name] of lockfiles) {
10002
- if (import_node_fs22.default.existsSync(import_node_path22.default.join(repoPath, fileName))) {
10722
+ if (import_node_fs24.default.existsSync(import_node_path24.default.join(repoPath, fileName))) {
10003
10723
  return { name, source: fileName };
10004
10724
  }
10005
10725
  }
@@ -10014,9 +10734,9 @@ function inspectNode(repoPath, pkg) {
10014
10734
  if (typeof engines?.node === "string") {
10015
10735
  return { version: engines.node, source: "package.json:engines.node" };
10016
10736
  }
10017
- const nvmrc = import_node_path22.default.join(repoPath, ".nvmrc");
10018
- if (import_node_fs22.default.existsSync(nvmrc)) {
10019
- const version = import_node_fs22.default.readFileSync(nvmrc, "utf-8").trim();
10737
+ const nvmrc = import_node_path24.default.join(repoPath, ".nvmrc");
10738
+ if (import_node_fs24.default.existsSync(nvmrc)) {
10739
+ const version = import_node_fs24.default.readFileSync(nvmrc, "utf-8").trim();
10020
10740
  return { version, source: ".nvmrc" };
10021
10741
  }
10022
10742
  return void 0;
@@ -10077,7 +10797,7 @@ function configuredComposeFiles(repoPath) {
10077
10797
  const files = config.apps.filter(
10078
10798
  (app) => app.runtime === "docker"
10079
10799
  ).flatMap((app) => app.docker.composeFiles).filter(
10080
- (fileName) => !import_node_path22.default.isAbsolute(fileName) && !import_node_path22.default.normalize(fileName).startsWith("..")
10800
+ (fileName) => !import_node_path24.default.isAbsolute(fileName) && !import_node_path24.default.normalize(fileName).startsWith("..")
10081
10801
  );
10082
10802
  return Array.from(new Set(files));
10083
10803
  } catch {
@@ -10095,7 +10815,7 @@ function composeFiles(repoPath) {
10095
10815
  ...configuredComposeFiles(repoPath)
10096
10816
  ];
10097
10817
  return Array.from(new Set(candidates)).filter(
10098
- (fileName) => import_node_fs22.default.existsSync(import_node_path22.default.join(repoPath, fileName))
10818
+ (fileName) => import_node_fs24.default.existsSync(import_node_path24.default.join(repoPath, fileName))
10099
10819
  );
10100
10820
  }
10101
10821
  function stringArray2(value) {
@@ -10133,7 +10853,7 @@ function inspectServices(repoPath) {
10133
10853
  const services = [];
10134
10854
  for (const fileName of composeFiles(repoPath)) {
10135
10855
  try {
10136
- const parsed = import_yaml6.default.parse(import_node_fs22.default.readFileSync(import_node_path22.default.join(repoPath, fileName), "utf-8"));
10856
+ const parsed = import_yaml6.default.parse(import_node_fs24.default.readFileSync(import_node_path24.default.join(repoPath, fileName), "utf-8"));
10137
10857
  const serviceMap = asRecord3(asRecord3(parsed)?.services);
10138
10858
  for (const [name, value] of Object.entries(serviceMap ?? {})) {
10139
10859
  const service = asRecord3(value);
@@ -10168,8 +10888,8 @@ function inspectServices(repoPath) {
10168
10888
  return services;
10169
10889
  }
10170
10890
  function inspectEnvFiles(repoPath) {
10171
- const files = import_node_fs22.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
10172
- const content = import_node_fs22.default.readFileSync(import_node_path22.default.join(repoPath, fileName), "utf-8");
10891
+ const files = import_node_fs24.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
10892
+ const content = import_node_fs24.default.readFileSync(import_node_path24.default.join(repoPath, fileName), "utf-8");
10173
10893
  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();
10174
10894
  return { path: fileName, names };
10175
10895
  });
@@ -10183,18 +10903,18 @@ function inspectEnvFiles(repoPath) {
10183
10903
  };
10184
10904
  }
10185
10905
  function inspectDevcontainer(repoPath) {
10186
- const dir = import_node_path22.default.join(repoPath, ".devcontainer");
10187
- if (!import_node_fs22.default.existsSync(dir)) {
10906
+ const dir = import_node_path24.default.join(repoPath, ".devcontainer");
10907
+ if (!import_node_fs24.default.existsSync(dir)) {
10188
10908
  return { exists: false, files: [] };
10189
10909
  }
10190
10910
  return {
10191
10911
  exists: true,
10192
- 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}`)
10912
+ 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}`)
10193
10913
  };
10194
10914
  }
10195
10915
  function inspectDevrouter(repoPath) {
10196
10916
  const configPath = getRepoConfigPath(repoPath);
10197
- if (!import_node_fs22.default.existsSync(configPath)) {
10917
+ if (!import_node_fs24.default.existsSync(configPath)) {
10198
10918
  return {
10199
10919
  exists: false,
10200
10920
  configPath,
@@ -10238,15 +10958,15 @@ function inspectAgentGuidance(repoPath) {
10238
10958
  ["AGENTS.md", "agents"],
10239
10959
  ["CLAUDE.md", "claude"]
10240
10960
  ]) {
10241
- if (import_node_fs22.default.existsSync(import_node_path22.default.join(repoPath, fileName))) {
10961
+ if (import_node_fs24.default.existsSync(import_node_path24.default.join(repoPath, fileName))) {
10242
10962
  results.push({ path: fileName, kind });
10243
10963
  }
10244
10964
  }
10245
- const skillsDir = import_node_path22.default.join(repoPath, ".agents", "skills");
10246
- if (import_node_fs22.default.existsSync(skillsDir)) {
10247
- for (const name of import_node_fs22.default.readdirSync(skillsDir).sort()) {
10248
- const skillPath = import_node_path22.default.join(skillsDir, name, "SKILL.md");
10249
- if (import_node_fs22.default.existsSync(skillPath)) {
10965
+ const skillsDir = import_node_path24.default.join(repoPath, ".agents", "skills");
10966
+ if (import_node_fs24.default.existsSync(skillsDir)) {
10967
+ for (const name of import_node_fs24.default.readdirSync(skillsDir).sort()) {
10968
+ const skillPath = import_node_path24.default.join(skillsDir, name, "SKILL.md");
10969
+ if (import_node_fs24.default.existsSync(skillPath)) {
10250
10970
  results.push({ path: relative(repoPath, skillPath), kind: "skill" });
10251
10971
  }
10252
10972
  }
@@ -10289,7 +11009,7 @@ function buildIssues(report) {
10289
11009
  }
10290
11010
  function inspectRepo(options = {}) {
10291
11011
  const repoPath = resolveRepoPath(options.repo);
10292
- const pkg = readJson(import_node_path22.default.join(repoPath, "package.json"));
11012
+ const pkg = readJson(import_node_path24.default.join(repoPath, "package.json"));
10293
11013
  const scripts = inspectScripts(pkg);
10294
11014
  const reportWithoutIssues = {
10295
11015
  repoPath,
@@ -10308,12 +11028,12 @@ function inspectRepo(options = {}) {
10308
11028
  issues: buildIssues(reportWithoutIssues)
10309
11029
  };
10310
11030
  }
10311
- var import_node_fs22, import_node_path22, import_yaml6;
11031
+ var import_node_fs24, import_node_path24, import_yaml6;
10312
11032
  var init_repo_inspect = __esm({
10313
11033
  "src/core/repo-inspect.ts"() {
10314
11034
  "use strict";
10315
- import_node_fs22 = __toESM(require("fs"));
10316
- import_node_path22 = __toESM(require("path"));
11035
+ import_node_fs24 = __toESM(require("fs"));
11036
+ import_node_path24 = __toESM(require("path"));
10317
11037
  import_yaml6 = __toESM(require("yaml"));
10318
11038
  init_repo_config();
10319
11039
  }
@@ -10418,7 +11138,7 @@ function requiredFileChecks(repoPath) {
10418
11138
  ".devcontainer/docker-compose.yml",
10419
11139
  ".devrouter.yml"
10420
11140
  ];
10421
- const missing = required.filter((fileName) => !import_node_fs23.default.existsSync(import_node_path23.default.join(repoPath, fileName)));
11141
+ const missing = required.filter((fileName) => !import_node_fs25.default.existsSync(import_node_path25.default.join(repoPath, fileName)));
10422
11142
  return {
10423
11143
  id: "repo.devcontainer.verify-files",
10424
11144
  level: missing.length === 0 ? "ok" : "error",
@@ -10637,12 +11357,12 @@ async function verifyDevcontainer(options = {}) {
10637
11357
  nextSteps: collectNextSteps3(checks)
10638
11358
  };
10639
11359
  }
10640
- var import_node_fs23, import_node_path23;
11360
+ var import_node_fs25, import_node_path25;
10641
11361
  var init_devcontainer_verify = __esm({
10642
11362
  "src/core/devcontainer-verify.ts"() {
10643
11363
  "use strict";
10644
- import_node_fs23 = __toESM(require("fs"));
10645
- import_node_path23 = __toESM(require("path"));
11364
+ import_node_fs25 = __toESM(require("fs"));
11365
+ import_node_path25 = __toESM(require("path"));
10646
11366
  init_capabilities();
10647
11367
  init_doctor();
10648
11368
  init_host_routes();
@@ -10938,7 +11658,7 @@ function packageManagerIssues(repo) {
10938
11658
  }
10939
11659
  function plannedFiles(repoPath, version) {
10940
11660
  const repo = inspectRepo({ repo: repoPath });
10941
- const projectName = sanitizeProjectName(import_node_path24.default.basename(repo.repoPath));
11661
+ const projectName = sanitizeProjectName(import_node_path26.default.basename(repo.repoPath));
10942
11662
  const nodeMajor = majorVersion(repo.node?.version, "24");
10943
11663
  const pnpmVersion = repo.packageManager?.name === "pnpm" && repo.packageManager.version ? repo.packageManager.version : DEFAULT_PNPM_VERSION;
10944
11664
  const port = inferPort2(repo);
@@ -10983,8 +11703,8 @@ function plannedFiles(repoPath, version) {
10983
11703
  };
10984
11704
  }
10985
11705
  function classifyFile(repoPath, file) {
10986
- const absolutePath = import_node_path24.default.join(repoPath, file.relativePath);
10987
- if (!import_node_fs24.default.existsSync(absolutePath)) {
11706
+ const absolutePath = import_node_path26.default.join(repoPath, file.relativePath);
11707
+ if (!import_node_fs26.default.existsSync(absolutePath)) {
10988
11708
  return {
10989
11709
  path: file.relativePath,
10990
11710
  action: "create",
@@ -10992,7 +11712,7 @@ function classifyFile(repoPath, file) {
10992
11712
  bytes: Buffer.byteLength(file.content)
10993
11713
  };
10994
11714
  }
10995
- const current = import_node_fs24.default.readFileSync(absolutePath, "utf-8");
11715
+ const current = import_node_fs26.default.readFileSync(absolutePath, "utf-8");
10996
11716
  if (!current.includes(MANAGED_MARKER2)) {
10997
11717
  return {
10998
11718
  path: file.relativePath,
@@ -11049,11 +11769,11 @@ function buildPlan(repoPath, dryRun, version) {
11049
11769
  };
11050
11770
  }
11051
11771
  function writeFile(repoPath, file) {
11052
- const absolutePath = import_node_path24.default.join(repoPath, file.relativePath);
11053
- import_node_fs24.default.mkdirSync(import_node_path24.default.dirname(absolutePath), { recursive: true });
11054
- import_node_fs24.default.writeFileSync(absolutePath, file.content, "utf-8");
11772
+ const absolutePath = import_node_path26.default.join(repoPath, file.relativePath);
11773
+ import_node_fs26.default.mkdirSync(import_node_path26.default.dirname(absolutePath), { recursive: true });
11774
+ import_node_fs26.default.writeFileSync(absolutePath, file.content, "utf-8");
11055
11775
  if (file.executable) {
11056
- import_node_fs24.default.chmodSync(absolutePath, 493);
11776
+ import_node_fs26.default.chmodSync(absolutePath, 493);
11057
11777
  }
11058
11778
  }
11059
11779
  function writeDevcontainer(options = {}) {
@@ -11091,12 +11811,12 @@ function writeDevcontainer(options = {}) {
11091
11811
  nextSteps: postWriteNextSteps(repoPath)
11092
11812
  };
11093
11813
  }
11094
- var import_node_fs24, import_node_path24, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
11814
+ var import_node_fs26, import_node_path26, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
11095
11815
  var init_devcontainer_write = __esm({
11096
11816
  "src/core/devcontainer-write.ts"() {
11097
11817
  "use strict";
11098
- import_node_fs24 = __toESM(require("fs"));
11099
- import_node_path24 = __toESM(require("path"));
11818
+ import_node_fs26 = __toESM(require("fs"));
11819
+ import_node_path26 = __toESM(require("path"));
11100
11820
  init_repo_config();
11101
11821
  init_repo_inspect();
11102
11822
  MANAGED_MARKER2 = "devrouter:managed devcontainer";
@@ -11487,7 +12207,7 @@ function sanitizeRouterId(value) {
11487
12207
  return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
11488
12208
  }
11489
12209
  function repoHash(repoPath) {
11490
- return (0, import_node_crypto8.createHash)("sha1").update(import_node_path25.default.resolve(repoPath)).digest("hex").slice(0, 12);
12210
+ return (0, import_node_crypto8.createHash)("sha1").update(import_node_path27.default.resolve(repoPath)).digest("hex").slice(0, 12);
11491
12211
  }
11492
12212
  function asDockerApp(app) {
11493
12213
  return app.runtime === "docker";
@@ -11566,11 +12286,11 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
11566
12286
  if (dockerApps.length === 0) {
11567
12287
  throw new Error("No docker apps selected to prepare compose overlay.");
11568
12288
  }
11569
- const cachePath = import_node_path25.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
11570
- import_node_fs25.default.mkdirSync(cachePath, { recursive: true });
11571
- const overlayPath = import_node_path25.default.join(cachePath, "compose.devrouter.yml");
12289
+ const cachePath = import_node_path27.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
12290
+ import_node_fs27.default.mkdirSync(cachePath, { recursive: true });
12291
+ const overlayPath = import_node_path27.default.join(cachePath, "compose.devrouter.yml");
11572
12292
  const overlayDocument = buildOverlayDocument(dockerApps, publishTcpPorts);
11573
- import_node_fs25.default.writeFileSync(overlayPath, import_yaml7.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
12293
+ import_node_fs27.default.writeFileSync(overlayPath, import_yaml7.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
11574
12294
  return {
11575
12295
  overlayPath,
11576
12296
  composeFiles: ensureComposeFiles(dockerApps),
@@ -11584,7 +12304,7 @@ function runDockerComposeUp(repoPath, composeFiles2, overlayPath, services) {
11584
12304
  fileArgs.push("-f", resolved);
11585
12305
  }
11586
12306
  const args = ["compose", ...fileArgs, "-f", overlayPath, "up", "-d", "--wait", ...services];
11587
- const result = (0, import_node_child_process20.spawnSync)("docker", args, {
12307
+ const result = (0, import_node_child_process25.spawnSync)("docker", args, {
11588
12308
  encoding: "utf-8",
11589
12309
  cwd: repoPath
11590
12310
  });
@@ -11612,7 +12332,7 @@ function queryRunningComposeServices(repoPath, composeFiles2, overlayPath, servi
11612
12332
  "--services",
11613
12333
  ...services
11614
12334
  ];
11615
- const result = (0, import_node_child_process20.spawnSync)("docker", args, {
12335
+ const result = (0, import_node_child_process25.spawnSync)("docker", args, {
11616
12336
  encoding: "utf-8",
11617
12337
  cwd: repoPath
11618
12338
  });
@@ -11638,7 +12358,7 @@ function runDockerComposeStop(repoPath, composeFiles2, overlayPath, services) {
11638
12358
  fileArgs.push("-f", resolved);
11639
12359
  }
11640
12360
  const args = ["compose", ...fileArgs, "-f", overlayPath, "stop", ...services];
11641
- const result = (0, import_node_child_process20.spawnSync)("docker", args, {
12361
+ const result = (0, import_node_child_process25.spawnSync)("docker", args, {
11642
12362
  encoding: "utf-8",
11643
12363
  cwd: repoPath
11644
12364
  });
@@ -11664,7 +12384,7 @@ function runDockerComposeLogs(repoPath, composeFiles2, overlayPath, services, ta
11664
12384
  String(tail),
11665
12385
  ...services
11666
12386
  ];
11667
- (0, import_node_child_process20.spawnSync)("docker", args, {
12387
+ (0, import_node_child_process25.spawnSync)("docker", args, {
11668
12388
  stdio: "inherit",
11669
12389
  cwd: repoPath
11670
12390
  });
@@ -11676,7 +12396,7 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
11676
12396
  fileArgs.push("-f", resolved);
11677
12397
  }
11678
12398
  const args = ["compose", ...fileArgs, "-f", overlayPath, "port", service, String(internalPort)];
11679
- const result = (0, import_node_child_process20.spawnSync)("docker", args, {
12399
+ const result = (0, import_node_child_process25.spawnSync)("docker", args, {
11680
12400
  encoding: "utf-8",
11681
12401
  cwd: repoPath
11682
12402
  });
@@ -11690,14 +12410,14 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
11690
12410
  const port = Number(match[1]);
11691
12411
  return Number.isInteger(port) && port > 0 ? port : void 0;
11692
12412
  }
11693
- var import_node_child_process20, import_node_crypto8, import_node_fs25, import_node_path25, import_yaml7;
12413
+ var import_node_child_process25, import_node_crypto8, import_node_fs27, import_node_path27, import_yaml7;
11694
12414
  var init_docker_run = __esm({
11695
12415
  "src/core/docker-run.ts"() {
11696
12416
  "use strict";
11697
- import_node_child_process20 = require("child_process");
12417
+ import_node_child_process25 = require("child_process");
11698
12418
  import_node_crypto8 = require("crypto");
11699
- import_node_fs25 = __toESM(require("fs"));
11700
- import_node_path25 = __toESM(require("path"));
12419
+ import_node_fs27 = __toESM(require("fs"));
12420
+ import_node_path27 = __toESM(require("path"));
11701
12421
  import_yaml7 = __toESM(require("yaml"));
11702
12422
  init_docker_error_guidance();
11703
12423
  init_paths();
@@ -11787,7 +12507,7 @@ function isProcessRunning(pid) {
11787
12507
  }
11788
12508
  }
11789
12509
  function readProcessTree(rootPid) {
11790
- const result = (0, import_node_child_process21.spawnSync)("ps", ["-ax", "-o", "pid=,ppid="], { encoding: "utf-8" });
12510
+ const result = (0, import_node_child_process26.spawnSync)("ps", ["-ax", "-o", "pid=,ppid="], { encoding: "utf-8" });
11791
12511
  if (result.status !== 0) {
11792
12512
  return [rootPid];
11793
12513
  }
@@ -11898,12 +12618,12 @@ function detectListeningPorts(pids) {
11898
12618
  if (pids.length === 0) {
11899
12619
  return [];
11900
12620
  }
11901
- const result = (0, import_node_child_process21.spawnSync)("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", pids.join(",")], {
12621
+ const result = (0, import_node_child_process26.spawnSync)("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", pids.join(",")], {
11902
12622
  encoding: "utf-8"
11903
12623
  });
11904
12624
  if (result.error && result.error.code === "ENOENT") {
11905
12625
  if (process.platform === "linux") {
11906
- const ssResult = (0, import_node_child_process21.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
12626
+ const ssResult = (0, import_node_child_process26.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
11907
12627
  if (ssResult.status === 0 && ssResult.stdout) {
11908
12628
  return parseSsListeningPorts(ssResult.stdout, new Set(pids));
11909
12629
  }
@@ -11962,7 +12682,7 @@ async function runHostApp(repoPath, app, extraEnv = {}, secretManager, env, work
11962
12682
  app.hostRun.command,
11963
12683
  true
11964
12684
  ) : app.hostRun.command;
11965
- const child = (0, import_node_child_process21.spawn)(spawnCommand, {
12685
+ const child = (0, import_node_child_process26.spawn)(spawnCommand, {
11966
12686
  cwd: commandCwd,
11967
12687
  stdio: "inherit",
11968
12688
  shell: true,
@@ -12330,7 +13050,7 @@ async function execWithAppEnv(options) {
12330
13050
  options.command[0],
12331
13051
  true
12332
13052
  );
12333
- child = (0, import_node_child_process21.spawn)(wrapped, {
13053
+ child = (0, import_node_child_process26.spawn)(wrapped, {
12334
13054
  cwd: deps.repoPath,
12335
13055
  stdio: "inherit",
12336
13056
  shell: true,
@@ -12344,7 +13064,7 @@ async function execWithAppEnv(options) {
12344
13064
  false
12345
13065
  );
12346
13066
  const [cmd, ...wrappedArgs] = wrapped;
12347
- child = (0, import_node_child_process21.spawn)(cmd, wrappedArgs, {
13067
+ child = (0, import_node_child_process26.spawn)(cmd, wrappedArgs, {
12348
13068
  cwd: deps.repoPath,
12349
13069
  stdio: "inherit",
12350
13070
  shell: false,
@@ -12352,7 +13072,7 @@ async function execWithAppEnv(options) {
12352
13072
  });
12353
13073
  }
12354
13074
  } else if (options.shell) {
12355
- child = (0, import_node_child_process21.spawn)(options.command[0], {
13075
+ child = (0, import_node_child_process26.spawn)(options.command[0], {
12356
13076
  cwd: deps.repoPath,
12357
13077
  stdio: "inherit",
12358
13078
  shell: true,
@@ -12360,7 +13080,7 @@ async function execWithAppEnv(options) {
12360
13080
  });
12361
13081
  } else {
12362
13082
  const [command, ...args] = options.command;
12363
- child = (0, import_node_child_process21.spawn)(command, args, {
13083
+ child = (0, import_node_child_process26.spawn)(command, args, {
12364
13084
  cwd: deps.repoPath,
12365
13085
  stdio: "inherit",
12366
13086
  shell: false,
@@ -12381,11 +13101,11 @@ async function execWithAppEnv(options) {
12381
13101
  deps.stopDeps();
12382
13102
  }
12383
13103
  }
12384
- var import_node_child_process21, import_node_net, import_node_process, import_promises, POLL_INTERVAL_MS2, DEFAULT_PORT_TIMEOUT_MS, PROCESS_TERMINATION_GRACE_MS;
13104
+ var import_node_child_process26, import_node_net, import_node_process, import_promises, POLL_INTERVAL_MS2, DEFAULT_PORT_TIMEOUT_MS, PROCESS_TERMINATION_GRACE_MS;
12385
13105
  var init_app_run = __esm({
12386
13106
  "src/core/app-run.ts"() {
12387
13107
  "use strict";
12388
- import_node_child_process21 = require("child_process");
13108
+ import_node_child_process26 = require("child_process");
12389
13109
  import_node_net = __toESM(require("net"));
12390
13110
  import_node_process = require("process");
12391
13111
  import_promises = require("readline/promises");
@@ -12519,13 +13239,13 @@ function measureWorktreeConsumption(worktreePath, options) {
12519
13239
  const startedAt = Date.now();
12520
13240
  let rootStat;
12521
13241
  try {
12522
- rootStat = import_node_fs26.default.lstatSync(worktreePath);
13242
+ rootStat = import_node_fs28.default.lstatSync(worktreePath);
12523
13243
  } catch (error) {
12524
13244
  return { status: "unknown", reason: describeError(error, worktreePath) };
12525
13245
  }
12526
13246
  let rootEntries;
12527
13247
  try {
12528
- rootEntries = import_node_fs26.default.readdirSync(worktreePath, { withFileTypes: true });
13248
+ rootEntries = import_node_fs28.default.readdirSync(worktreePath, { withFileTypes: true });
12529
13249
  } catch (error) {
12530
13250
  return { status: "unknown", reason: describeError(error, worktreePath) };
12531
13251
  }
@@ -12551,10 +13271,10 @@ function measureWorktreeConsumption(worktreePath, options) {
12551
13271
  timedOut = true;
12552
13272
  break;
12553
13273
  }
12554
- const entryPath = import_node_path26.default.join(dirPath, entry.name);
13274
+ const entryPath = import_node_path28.default.join(dirPath, entry.name);
12555
13275
  let entryStat;
12556
13276
  try {
12557
- entryStat = import_node_fs26.default.lstatSync(entryPath);
13277
+ entryStat = import_node_fs28.default.lstatSync(entryPath);
12558
13278
  } catch (error) {
12559
13279
  if (error?.code === "ENOENT") continue;
12560
13280
  unreadableReason = describeIncompleteWalk(error);
@@ -12564,7 +13284,7 @@ function measureWorktreeConsumption(worktreePath, options) {
12564
13284
  if (!entryStat.isDirectory()) continue;
12565
13285
  let childEntries;
12566
13286
  try {
12567
- childEntries = import_node_fs26.default.readdirSync(entryPath, { withFileTypes: true });
13287
+ childEntries = import_node_fs28.default.readdirSync(entryPath, { withFileTypes: true });
12568
13288
  } catch (error) {
12569
13289
  unreadableReason = describeIncompleteWalk(error);
12570
13290
  break;
@@ -12628,12 +13348,12 @@ function describeError(error, worktreePath) {
12628
13348
  }
12629
13349
  return `could not stat worktree path '${worktreePath}': ${error?.message ?? String(error)}`;
12630
13350
  }
12631
- var import_node_fs26, import_node_path26, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
13351
+ var import_node_fs28, import_node_path28, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
12632
13352
  var init_workspace_consumption = __esm({
12633
13353
  "src/core/workspace-consumption.ts"() {
12634
13354
  "use strict";
12635
- import_node_fs26 = __toESM(require("fs"));
12636
- import_node_path26 = __toESM(require("path"));
13355
+ import_node_fs28 = __toESM(require("fs"));
13356
+ import_node_path28 = __toESM(require("path"));
12637
13357
  init_devpod_environment();
12638
13358
  DEFAULT_DEADLINE_MS = 1e4;
12639
13359
  BLOCK_SIZE_BYTES = 512;
@@ -12734,7 +13454,7 @@ function evaluateWorkspaceActivity(evidence, cutoff) {
12734
13454
  }
12735
13455
  function readGitSnapshot(worktree, commandRunner) {
12736
13456
  const comparablePath = comparableWorkspacePath(worktree.path);
12737
- if (worktree.prunable || !import_node_fs27.default.existsSync(comparablePath)) {
13457
+ if (worktree.prunable || !import_node_fs29.default.existsSync(comparablePath)) {
12738
13458
  return { worktree, checkout: "missing", head: null, committerDate: null };
12739
13459
  }
12740
13460
  const head = gitOutput(comparablePath, ["rev-parse", "--verify", "HEAD"], commandRunner);
@@ -13243,12 +13963,6 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
13243
13963
  const cutoff = new Date(now.getTime() - duration.seconds * 1e3).toISOString();
13244
13964
  const worktrees = (dependencies.listWorktrees ?? listGitWorktrees)(repoPath);
13245
13965
  const records = (dependencies.listOwnership ?? listWorkspaceOwnership)(repoPath);
13246
- let devpods;
13247
- try {
13248
- devpods = (dependencies.listDevpods ?? listDevpodWorkspaces)();
13249
- } catch {
13250
- devpods = void 0;
13251
- }
13252
13966
  let routes;
13253
13967
  try {
13254
13968
  routes = (dependencies.readRoutes ?? readHostRouteStateReadOnly)();
@@ -13265,7 +13979,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
13265
13979
  containers = measureContainersFn(worktreePaths);
13266
13980
  } catch (error) {
13267
13981
  const reason = `container measurement failed: ${describeCause(error)}`;
13268
- containers = new Map(worktreePaths.map((path26) => [path26, unknownContainers(reason)]));
13982
+ containers = new Map(worktreePaths.map((path28) => [path28, unknownContainers(reason)]));
13269
13983
  }
13270
13984
  }
13271
13985
  const rows = records.map((record) => {
@@ -13280,6 +13994,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
13280
13994
  prunable: true
13281
13995
  }
13282
13996
  );
13997
+ const devpods = dependencies.listDevpods ? dependencies.listDevpods() : listDevpodWorkspacesFromSnapshots(record.worktreePath);
13283
13998
  const row = buildRow(
13284
13999
  repoPath,
13285
14000
  record,
@@ -13295,7 +14010,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
13295
14010
  return { ownerStatus: status.ownerStatus, devpodStatus: status.devpodStatus };
13296
14011
  }),
13297
14012
  dependencies.inspectIntegration,
13298
- dependencies.inspectDevpodRuntime ?? inspectDevpodRuntimeStatus
14013
+ dependencies.inspectDevpodRuntime ?? ((devpodId) => inspectDevpodRuntimeStatus(devpodId, record.worktreePath))
13299
14014
  );
13300
14015
  return measureSize ? {
13301
14016
  ...row,
@@ -13313,12 +14028,12 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
13313
14028
  workspaces: rows
13314
14029
  };
13315
14030
  }
13316
- var import_node_child_process22, import_node_fs27, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
14031
+ var import_node_child_process27, import_node_fs29, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
13317
14032
  var init_workspace_cleanup = __esm({
13318
14033
  "src/core/workspace-cleanup.ts"() {
13319
14034
  "use strict";
13320
- import_node_child_process22 = require("child_process");
13321
- import_node_fs27 = __toESM(require("fs"));
14035
+ import_node_child_process27 = require("child_process");
14036
+ import_node_fs29 = __toESM(require("fs"));
13322
14037
  init_devpod_workspaces();
13323
14038
  init_host_routes();
13324
14039
  init_repo_config();
@@ -13335,7 +14050,7 @@ var init_workspace_cleanup = __esm({
13335
14050
  ];
13336
14051
  defaultCommandRunner = (command, args, input2) => {
13337
14052
  const environment = command === "git" ? READ_ONLY_GIT_ENV2 : { ...process.env, LC_ALL: "C" };
13338
- const result = (0, import_node_child_process22.spawnSync)(command, args, {
14053
+ const result = (0, import_node_child_process27.spawnSync)(command, args, {
13339
14054
  encoding: "utf-8",
13340
14055
  env: environment,
13341
14056
  ...input2 === void 0 ? {} : { input: input2 }
@@ -13495,7 +14210,7 @@ var init_version = __esm({
13495
14210
 
13496
14211
  // src/cli.ts
13497
14212
  var import_commander = require("commander");
13498
- var CLI_VERSION = true ? "0.0.39" : "0.0.0-dev";
14213
+ var CLI_VERSION = true ? "0.0.41" : "0.0.0-dev";
13499
14214
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
13500
14215
  function withErrorHandling(action2) {
13501
14216
  return async (...args) => {
@@ -13533,11 +14248,16 @@ program.command("upgrade").description(
13533
14248
  }
13534
14249
  )
13535
14250
  );
13536
- 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(
13537
- withErrorHandling(async (options) => {
13538
- const { runSetupCommand: runSetupCommand2 } = await Promise.resolve().then(() => (init_setup2(), setup_exports));
13539
- await runSetupCommand2(options);
13540
- })
14251
+ 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(
14252
+ "--devsy-inactivity-timeout <duration>",
14253
+ "Persist the Devsy inactivity shutdown duration (e.g. 30m)"
14254
+ ).option("--json", "Output JSON").action(
14255
+ withErrorHandling(
14256
+ async (options) => {
14257
+ const { runSetupCommand: runSetupCommand2 } = await Promise.resolve().then(() => (init_setup2(), setup_exports));
14258
+ await runSetupCommand2(options);
14259
+ }
14260
+ )
13541
14261
  );
13542
14262
  program.command("up").description("Ensure devnet and start shared Traefik (reserves 80/443/5432)").action(
13543
14263
  withErrorHandling(async () => {
@@ -13557,7 +14277,7 @@ program.command("ensure").description("Start and prove a primary or linked check
13557
14277
  });
13558
14278
  })
13559
14279
  );
13560
- 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(
14280
+ 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(
13561
14281
  withErrorHandling(async (repoPath, _options, command) => {
13562
14282
  const options = command.opts();
13563
14283
  const { runStopCommand: runStopCommand2 } = await Promise.resolve().then(() => (init_stop(), stop_exports));
@@ -13568,7 +14288,7 @@ program.command("stop").description("Stop one checkout's exact DevPod and free i
13568
14288
  });
13569
14289
  })
13570
14290
  );
13571
- program.command("exec").description("Run one literal command inside the exact checkout's running DevPod").argument("[args...]", "[path] -- <command...>").allowUnknownOption(true).action(
14291
+ program.command("exec").description("Run one literal command inside the exact checkout's running workspace runtime").argument("[args...]", "[path] -- <command...>").allowUnknownOption(true).action(
13572
14292
  withErrorHandling(async () => {
13573
14293
  const commandIndex = process.argv.indexOf("exec", 2);
13574
14294
  const { parseExecInvocation: parseExecInvocation2, runExecCommand: runExecCommand2 } = await Promise.resolve().then(() => (init_exec(), exec_exports));
@@ -13737,7 +14457,7 @@ tlsCommand.command("install").description("Install mkcert certs and enable HTTPS
13737
14457
  );
13738
14458
  var workspaceCommand2 = program.command("workspace").description("Spin up / list / tear down isolated worktree+devcontainer workspaces");
13739
14459
  workspaceCommand2.command("up").description(
13740
- "Create a worktree for <branch>, bring up its devpod, and register namespaced routes"
14460
+ "Create a worktree for <branch>, bring up its workspace runtime, and register namespaced routes"
13741
14461
  ).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(
13742
14462
  withErrorHandling(async (branch, _options, command) => {
13743
14463
  const options = command.opts();
@@ -13750,7 +14470,9 @@ workspaceCommand2.command("up").description(
13750
14470
  });
13751
14471
  })
13752
14472
  );
13753
- 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(
14473
+ workspaceCommand2.command("ensure").description(
14474
+ "Start and prove a primary or linked checkout's workspace runtime, upstreams, and routes"
14475
+ ).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(
13754
14476
  withErrorHandling(
13755
14477
  async (worktreePath, _options, command) => {
13756
14478
  const options = command.opts();
@@ -13784,21 +14506,21 @@ workspaceCommand2.command("cleanup").description("Report managed workspace activ
13784
14506
  });
13785
14507
  })
13786
14508
  );
13787
- 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(
14509
+ 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(
13788
14510
  withErrorHandling(async (_options, command) => {
13789
14511
  const options = command.opts();
13790
14512
  const { runWorkspaceGcCommand: runWorkspaceGcCommand2 } = await Promise.resolve().then(() => (init_workspace2(), workspace_exports));
13791
14513
  runWorkspaceGcCommand2(options);
13792
14514
  })
13793
14515
  );
13794
- 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(
14516
+ 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(
13795
14517
  withErrorHandling(async (target, _options, command) => {
13796
14518
  const options = command.opts();
13797
14519
  const { runWorkspaceStopCommand: runWorkspaceStopCommand2 } = await Promise.resolve().then(() => (init_workspace2(), workspace_exports));
13798
14520
  await runWorkspaceStopCommand2(target, options);
13799
14521
  })
13800
14522
  );
13801
- 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(
14523
+ 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(
13802
14524
  withErrorHandling(async (target, _options, command) => {
13803
14525
  const options = command.opts();
13804
14526
  const { runWorkspaceDownCommand: runWorkspaceDownCommand2 } = await Promise.resolve().then(() => (init_workspace2(), workspace_exports));