@devrouter/cli 0.0.30 → 0.0.31

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
@@ -235,13 +235,15 @@ Config-level \`envMap\` on dependency references aliases per-dep vars to app-exp
235
235
 
236
236
  ## Workspace isolation (parallel git worktrees / agents)
237
237
 
238
- Run several worktrees of one repo in parallel without host/route collisions. A **workspace token** is a persisted identity spanning the DevPod id, devrouter routes, \`\${WORKSPACE}\` proxy upstreams, and devcontainer aliases.
238
+ Run several worktrees of one repo in parallel without host/route collisions. A **workspace token** spans the DevPod id, devrouter routes, \`\${WORKSPACE}\` proxy upstreams, and devcontainer aliases.
239
239
 
240
- - **Identity**: each linked worktree stores one authoritative identity in Git metadata. First use reuses an exact-path DevPod or derives a sanitized branch/path slug. Later flags or \`DEVROUTER_WORKSPACE\` may repeat the identity but cannot rename it. Ambiguous identities fail closed. The primary checkout remains non-namespaced.
240
+ - **Identity**: each managed linked worktree stores a local token in Git metadata plus a durable owner record in the repository's Git common directory. The 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 slug. Later flags or \`DEVROUTER_WORKSPACE\` may repeat the identity but cannot rename it. Ambiguous identities fail closed. The primary checkout remains non-namespaced.
241
241
  - **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. The runtime config is computed in memory only \u2014 the committed \`.devrouter.yml\` is never rewritten.
242
242
  - **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.
243
243
  - **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>\`.
244
- - **Lifecycle**: \`devrouter workspace up <branch>\` creates and starts a new worktree under the repository's ignored \`trees/<workspace>\` directory; \`devrouter workspace ensure .\` is the canonical startup/reconciliation command in an existing linked worktree; \`workspace ls\` lists identity/route state; \`workspace down\` serializes teardown with ensure. Ensure proves exact DevPod ownership, overlay/Git mounts, env, aliases, health, Git, HTTP route reachability, and unique running TCP upstream ownership before success, with one bounded recreate for stale state.
244
+ - **Lifecycle**: \`workspace up\` creates and starts a worktree; \`workspace ensure .\` starts/reconciles an existing linked worktree; \`workspace ls\` reports owner/Git/DevPod/route state; \`workspace stop\` stops DevPod/routes but keeps checkout, record, and data; full \`workspace down\` deletes runtime/routes and removes only a clean, unlocked worktree, then its record; \`down --keep-worktree\` keeps checkout and record. Dirty or locked full down fails before side effects.
245
+ - **Cleanup**: owner status is \`present\`, \`missing\`, \`locked\`, or \`conflict\`. \`workspace gc\` is a dry-run report; \`--yes\` deletes only exact ledger-owned missing/prunable resources and their records. GC never removes Git worktrees, branches, or prune state. Git has no worktree-removal hook.
246
+ - **Boundary**: workspace commands require Git. Normal config, app, status, and doctor flows remain usable from a \`.devrouter.yml\` folder without \`.git\`.
245
247
 
246
248
  ## Secret manager interop (Infisical/Doppler)
247
249
 
@@ -312,8 +314,10 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
312
314
  - \`devrouter app rm <name> [--keep-config]\`: remove app entry (\`--keep-config\` frees only the live route/hostname, leaves \`.devrouter.yml\` untouched)
313
315
  - \`devrouter workspace up <branch> [--path <dir>] [--no-devpod] [--open]\`: create a worktree and start/prove it unless create-only mode is requested
314
316
  - \`devrouter workspace ensure [path] [--open]\`: start/reconcile and prove an existing linked worktree environment
315
- - \`devrouter workspace ls [--json]\`: list git worktrees with workspace token + route count
316
- - \`devrouter workspace down <workspace|branch> [--keep-worktree] [--keep-devpod]\`: free routes + stop devpod + remove worktree
317
+ - \`devrouter workspace ls [--json]\`: list ownership, Git, DevPod, route, path, and branch evidence
318
+ - \`devrouter workspace stop <workspace|branch>\`: stop DevPod and routes; preserve checkout, owner record, and data
319
+ - \`devrouter workspace down <workspace|branch> [--keep-worktree]\`: delete runtime/routes and optionally remove the clean worktree and record
320
+ - \`devrouter workspace gc [--json] [--yes]\`: report missing owners by default; apply exact eligible cleanup with \`--yes\`
317
321
 
318
322
  ## Validation workflow
319
323
 
@@ -1787,7 +1791,7 @@ function loadRepoConfig(repoPath) {
1787
1791
  const config = parseConfig(parsed ?? {}, configPath);
1788
1792
  const requiredVersion = config.devrouter?.version;
1789
1793
  if (requiredVersion && !hasWarnedVersionMismatch) {
1790
- const cliVersion = true ? "0.0.30" : "0.0.0-dev";
1794
+ const cliVersion = true ? "0.0.31" : "0.0.0-dev";
1791
1795
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
1792
1796
  hasWarnedVersionMismatch = true;
1793
1797
  process.stderr.write(
@@ -2287,11 +2291,13 @@ function buildOnboardingPrompt(options = {}) {
2287
2291
  "- `envMap` on dependency references (config-level) aliases per-dep vars after dependency env resolution. `envMap` fails fast when source var is missing.",
2288
2292
  "",
2289
2293
  "Workspace isolation (parallel git worktrees / agents):",
2290
- `- A "workspace token" lets several worktrees of one repo run in parallel without host/route collisions. Each linked worktree persists one authoritative identity spanning the DevPod id, devrouter routes, the \`${WORKSPACE_PLACEHOLDER}\` proxy upstream, and devcontainer aliases.`,
2291
- "- First use reuses an exact-path DevPod or derives a sanitized branch/path identity. Later flags or `DEVROUTER_WORKSPACE` may repeat but cannot rename the persisted identity. Ambiguous identities fail closed. The primary checkout stays non-namespaced.",
2294
+ `- A "workspace token" lets several worktrees of one repo run in parallel without host/route collisions. Each managed linked worktree has a local Git token plus a durable owner record in the repository's Git common directory spanning the DevPod id, devrouter routes, the \`${WORKSPACE_PLACEHOLDER}\` proxy upstream, and devcontainer aliases.`,
2295
+ "- 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.",
2292
2296
  `- 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. The runtime config is computed in memory only \u2014 the committed \`.devrouter.yml\` is never rewritten.`,
2293
2297
  "- 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.",
2294
- "- Lifecycle: `devrouter workspace up <branch>` creates and starts a new worktree; `devrouter workspace ensure .` is the canonical start/reconcile command inside an existing linked worktree; `workspace ls` reports state; `workspace down` serializes teardown with ensure.",
2298
+ "- Lifecycle: `workspace up` creates and starts a worktree; `workspace ensure .` starts/reconciles an existing linked worktree; `workspace ls` reports owner/Git/DevPod/route state; `workspace stop` preserves checkout, record, and data; full `workspace down` deletes runtime/routes and removes only a clean, unlocked worktree; `down --keep-worktree` retains checkout and record.",
2299
+ "- 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.",
2300
+ "- 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.",
2295
2301
  "- 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.",
2296
2302
  "",
2297
2303
  "Secret Manager Integration (config-based):",
@@ -2458,11 +2464,19 @@ var init_ai_prompt = __esm({
2458
2464
  },
2459
2465
  {
2460
2466
  command: "devrouter workspace ls [--json]",
2461
- purpose: "List git worktrees with their resolved workspace token and active route count."
2467
+ purpose: "List owner, Git, DevPod, route, path, and branch evidence for managed workspaces."
2462
2468
  },
2463
2469
  {
2464
- command: "devrouter workspace down <workspace|branch> [--keep-worktree] [--keep-devpod]",
2465
- purpose: "Free a workspace's routes, stop its devpod, and remove its worktree (routes are freed by state-file workspace tag, no config load)."
2470
+ command: "devrouter workspace stop <workspace|branch>",
2471
+ purpose: "Stop the exact DevPod and remove exact routes while preserving the worktree, owner record, and data."
2472
+ },
2473
+ {
2474
+ command: "devrouter workspace down <workspace|branch> [--keep-worktree]",
2475
+ purpose: "Delete exact runtime/routes and remove only a clean, unlocked worktree and its owner record; --keep-worktree preserves checkout and record."
2476
+ },
2477
+ {
2478
+ command: "devrouter workspace gc [--json] [--yes]",
2479
+ purpose: "Report missing owners without mutation by default; --yes deletes only exact eligible ledger-owned resources and records, never Git worktrees."
2466
2480
  }
2467
2481
  ];
2468
2482
  }
@@ -3396,12 +3410,6 @@ function reconcileRouteRunConflict(repoPath, app) {
3396
3410
  }
3397
3411
  }
3398
3412
  }
3399
- function isOrphanedWorkspaceProxyRoute(route) {
3400
- return route.mode === "proxy" && route.workspace !== void 0 && !import_node_fs9.default.existsSync(route.repoPath);
3401
- }
3402
- function findOrphanedWorkspaceProxyRoutes(routes = listHostRouteState()) {
3403
- return routes.filter((route) => isOrphanedWorkspaceProxyRoute(route));
3404
- }
3405
3413
  var import_node_fs9;
3406
3414
  var init_route_state = __esm({
3407
3415
  "src/core/route-state.ts"() {
@@ -4064,6 +4072,634 @@ var init_tool_diagnostics = __esm({
4064
4072
  }
4065
4073
  });
4066
4074
 
4075
+ // src/core/devpod-workspaces.ts
4076
+ function listDevpodWorkspaces() {
4077
+ const result = (0, import_node_child_process6.spawnSync)("devpod", ["list", "--output", "json"], { encoding: "utf-8" });
4078
+ if (result.status !== 0) {
4079
+ const details = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
4080
+ throw new Error(`devpod list failed: ${details || "devpod is not installed or unavailable"}`);
4081
+ }
4082
+ let parsed;
4083
+ try {
4084
+ parsed = JSON.parse(result.stdout);
4085
+ } catch {
4086
+ throw new Error("devpod list returned invalid JSON.");
4087
+ }
4088
+ if (!Array.isArray(parsed)) {
4089
+ throw new Error("devpod list returned an unexpected response.");
4090
+ }
4091
+ return parsed.map((entry) => {
4092
+ const candidate = entry;
4093
+ if (typeof candidate.id !== "string" || !candidate.source || typeof candidate.source.localFolder !== "string") {
4094
+ throw new Error("devpod list returned a workspace without id/source.localFolder.");
4095
+ }
4096
+ return candidate;
4097
+ });
4098
+ }
4099
+ function inspectDevpodWorkspaceOwnership(workspaces, devpodId, worktreePath) {
4100
+ const idOwners = workspaces.filter((workspace) => workspace.id === devpodId);
4101
+ const pathOwners = workspaces.filter(
4102
+ (workspace) => sameWorkspacePath(workspace.source.localFolder, worktreePath)
4103
+ );
4104
+ const exact = idOwners.filter(
4105
+ (workspace) => sameWorkspacePath(workspace.source.localFolder, worktreePath)
4106
+ );
4107
+ if (idOwners.length > 1 || pathOwners.length > 1 || idOwners[0] && exact.length === 0 || pathOwners[0] && pathOwners[0].id !== devpodId) {
4108
+ return {
4109
+ status: "conflict",
4110
+ reason: `DevPod '${devpodId}' and worktree '${worktreePath}' do not have one exact owner.`
4111
+ };
4112
+ }
4113
+ return exact[0] ? { status: "owned", workspace: exact[0] } : { status: "absent" };
4114
+ }
4115
+ function selectDevpodWorkspace(workspaces, repoPath) {
4116
+ const matches = workspaces.filter(
4117
+ (workspace) => sameWorkspacePath(workspace.source.localFolder, repoPath)
4118
+ );
4119
+ if (matches.length > 1) {
4120
+ throw new Error(
4121
+ `Multiple DevPod workspaces reference '${repoPath}': ${matches.map((match) => match.id).join(", ")}`
4122
+ );
4123
+ }
4124
+ return matches[0];
4125
+ }
4126
+ function runDevpodWorkspaceAction(action2, devpodId) {
4127
+ const args = action2 === "delete" ? [action2, devpodId, "--ignore-not-found"] : [action2, devpodId];
4128
+ const result = (0, import_node_child_process6.spawnSync)("devpod", args, { encoding: "utf-8" });
4129
+ if (result.status !== 0) {
4130
+ const detail = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
4131
+ throw new Error(`devpod ${action2} failed for '${devpodId}': ${detail || "unknown error"}`);
4132
+ }
4133
+ }
4134
+ var import_node_child_process6;
4135
+ var init_devpod_workspaces = __esm({
4136
+ "src/core/devpod-workspaces.ts"() {
4137
+ "use strict";
4138
+ import_node_child_process6 = require("child_process");
4139
+ init_workspace();
4140
+ }
4141
+ });
4142
+
4143
+ // src/core/workspace-ownership.ts
4144
+ function commandError(command, repoPath, stderr) {
4145
+ return new Error(
4146
+ `${command} failed for '${repoPath}': ${stderr?.trim() || "not a Git repository"}`
4147
+ );
4148
+ }
4149
+ function resolveGitCommonDir(repoPath) {
4150
+ const result = (0, import_node_child_process7.spawnSync)("git", ["-C", repoPath, "rev-parse", "--git-common-dir"], {
4151
+ encoding: "utf-8"
4152
+ });
4153
+ const output2 = result.stdout.trim();
4154
+ if (result.status !== 0 || !output2) {
4155
+ throw commandError("Could not resolve the Git common directory", repoPath, result.stderr);
4156
+ }
4157
+ return comparableWorkspacePath(import_node_path10.default.isAbsolute(output2) ? output2 : import_node_path10.default.resolve(repoPath, output2));
4158
+ }
4159
+ function listGitWorktrees(repoPath) {
4160
+ const result = (0, import_node_child_process7.spawnSync)("git", ["-C", repoPath, "worktree", "list", "--porcelain"], {
4161
+ encoding: "utf-8"
4162
+ });
4163
+ if (result.status !== 0) {
4164
+ throw commandError("git worktree list", repoPath, result.stderr);
4165
+ }
4166
+ const worktrees = [];
4167
+ let current = {};
4168
+ const finish = () => {
4169
+ if (!current.path) return;
4170
+ worktrees.push({
4171
+ path: comparableWorkspacePath(current.path),
4172
+ branch: current.branch,
4173
+ locked: current.locked ?? false,
4174
+ prunable: current.prunable ?? false
4175
+ });
4176
+ current = {};
4177
+ };
4178
+ for (const line of `${result.stdout}
4179
+ `.split("\n")) {
4180
+ if (line.startsWith("worktree ")) {
4181
+ finish();
4182
+ current.path = line.slice("worktree ".length).trim();
4183
+ } else if (line.startsWith("branch ")) {
4184
+ current.branch = line.slice("branch ".length).trim().replace(/^refs\/heads\//, "");
4185
+ } else if (line === "locked" || line.startsWith("locked ")) {
4186
+ current.locked = true;
4187
+ } else if (line === "prunable" || line.startsWith("prunable ")) {
4188
+ current.prunable = true;
4189
+ } else if (line === "") {
4190
+ finish();
4191
+ }
4192
+ }
4193
+ return worktrees;
4194
+ }
4195
+ function ownershipDirectory(repoPath) {
4196
+ return import_node_path10.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
4197
+ }
4198
+ function validateWorkspace(value, label) {
4199
+ if (typeof value !== "string" || wsFromBranch(value) !== value) {
4200
+ throw new Error(`invalid workspace ownership ${label}`);
4201
+ }
4202
+ return value;
4203
+ }
4204
+ function validateTimestamp(value, label) {
4205
+ if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
4206
+ throw new Error(`invalid workspace ownership ${label}`);
4207
+ }
4208
+ return value;
4209
+ }
4210
+ function validateRecord(value, expectedWorkspace) {
4211
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
4212
+ throw new Error("invalid workspace ownership record");
4213
+ }
4214
+ const candidate = value;
4215
+ if (candidate.version !== OWNERSHIP_VERSION) {
4216
+ throw new Error(`unsupported workspace ownership version '${String(candidate.version)}'`);
4217
+ }
4218
+ const workspace = validateWorkspace(candidate.workspace, "workspace");
4219
+ if (expectedWorkspace && workspace !== expectedWorkspace) {
4220
+ throw new Error(
4221
+ `workspace ownership file '${expectedWorkspace}' contains identity '${workspace}'`
4222
+ );
4223
+ }
4224
+ if (typeof candidate.worktreePath !== "string" || !import_node_path10.default.isAbsolute(candidate.worktreePath)) {
4225
+ throw new Error("invalid workspace ownership worktreePath");
4226
+ }
4227
+ if (candidate.branch !== null && typeof candidate.branch !== "string") {
4228
+ throw new Error("invalid workspace ownership branch");
4229
+ }
4230
+ const devpodId = validateWorkspace(candidate.devpodId, "devpodId");
4231
+ return {
4232
+ version: OWNERSHIP_VERSION,
4233
+ workspace,
4234
+ worktreePath: comparableWorkspacePath(candidate.worktreePath),
4235
+ branch: candidate.branch,
4236
+ devpodId,
4237
+ createdAt: validateTimestamp(candidate.createdAt, "createdAt"),
4238
+ updatedAt: validateTimestamp(candidate.updatedAt, "updatedAt")
4239
+ };
4240
+ }
4241
+ function readRecordFile(filePath, expectedWorkspace) {
4242
+ let parsed;
4243
+ try {
4244
+ parsed = JSON.parse(import_node_fs13.default.readFileSync(filePath, "utf-8"));
4245
+ } catch (error) {
4246
+ if (error instanceof SyntaxError) {
4247
+ throw new Error(`invalid workspace ownership JSON at '${filePath}'`);
4248
+ }
4249
+ throw error;
4250
+ }
4251
+ return validateRecord(parsed, expectedWorkspace);
4252
+ }
4253
+ function listWorkspaceOwnership(repoPath) {
4254
+ const directory = ownershipDirectory(repoPath);
4255
+ return listWorkspaceOwnershipInDirectory(directory);
4256
+ }
4257
+ function listWorkspaceOwnershipInDirectory(directory) {
4258
+ let entries;
4259
+ try {
4260
+ entries = import_node_fs13.default.readdirSync(directory, { withFileTypes: true });
4261
+ } catch (error) {
4262
+ if (error.code === "ENOENT") return [];
4263
+ throw error;
4264
+ }
4265
+ return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
4266
+ const workspace = entry.name.slice(0, -".json".length);
4267
+ validateWorkspace(workspace, "filename");
4268
+ return readRecordFile(import_node_path10.default.join(directory, entry.name), workspace);
4269
+ });
4270
+ }
4271
+ function writeWorkspaceOwnershipInDirectory(directory, input2) {
4272
+ const workspace = validateWorkspace(input2.workspace, "workspace");
4273
+ const devpodId = validateWorkspace(input2.devpodId, "devpodId");
4274
+ const worktreePath = comparableWorkspacePath(input2.worktreePath);
4275
+ const filePath = import_node_path10.default.join(directory, `${workspace}.json`);
4276
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4277
+ const records = listWorkspaceOwnershipInDirectory(directory);
4278
+ const existing = records.find((record2) => record2.workspace === workspace);
4279
+ if (existing && !sameWorkspacePath(existing.worktreePath, worktreePath)) {
4280
+ throw new Error(
4281
+ `Workspace '${workspace}' already belongs to '${existing.worktreePath}', refusing '${worktreePath}'.`
4282
+ );
4283
+ }
4284
+ const pathOwner = records.find(
4285
+ (record2) => record2.workspace !== workspace && sameWorkspacePath(record2.worktreePath, worktreePath)
4286
+ );
4287
+ if (pathOwner) {
4288
+ throw new Error(
4289
+ `Worktree '${worktreePath}' is already owned by workspace '${pathOwner.workspace}'.`
4290
+ );
4291
+ }
4292
+ if (existing && existing.devpodId !== devpodId) {
4293
+ throw new Error(
4294
+ `Workspace '${workspace}' already owns DevPod '${existing.devpodId}', refusing '${devpodId}'.`
4295
+ );
4296
+ }
4297
+ const record = {
4298
+ version: OWNERSHIP_VERSION,
4299
+ workspace,
4300
+ worktreePath,
4301
+ branch: input2.branch ?? null,
4302
+ devpodId,
4303
+ createdAt: existing?.createdAt ?? validateTimestamp(now, "createdAt"),
4304
+ updatedAt: validateTimestamp(now, "updatedAt")
4305
+ };
4306
+ const tempPath = `${filePath}.${process.pid}.${(0, import_node_crypto3.randomUUID)()}.tmp`;
4307
+ try {
4308
+ import_node_fs13.default.writeFileSync(tempPath, `${JSON.stringify(record, null, 2)}
4309
+ `, {
4310
+ encoding: "utf-8",
4311
+ flag: "wx"
4312
+ });
4313
+ import_node_fs13.default.renameSync(tempPath, filePath);
4314
+ } finally {
4315
+ import_node_fs13.default.rmSync(tempPath, { force: true });
4316
+ }
4317
+ return record;
4318
+ }
4319
+ function removeWorkspaceOwnershipInDirectory(directory, workspace) {
4320
+ const filePath = import_node_path10.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
4321
+ try {
4322
+ import_node_fs13.default.rmSync(filePath);
4323
+ return true;
4324
+ } catch (error) {
4325
+ if (error.code === "ENOENT") return false;
4326
+ throw error;
4327
+ }
4328
+ }
4329
+ function sameOwnershipRecord(left, right) {
4330
+ 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;
4331
+ }
4332
+ function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
4333
+ const filePath = import_node_path10.default.join(
4334
+ directory,
4335
+ `${validateWorkspace(expected.workspace, "workspace")}.json`
4336
+ );
4337
+ let current;
4338
+ try {
4339
+ current = readRecordFile(filePath, expected.workspace);
4340
+ } catch (error) {
4341
+ if (error.code === "ENOENT") return "absent";
4342
+ throw error;
4343
+ }
4344
+ if (!sameOwnershipRecord(current, expected)) return "changed";
4345
+ import_node_fs13.default.rmSync(filePath);
4346
+ return "removed";
4347
+ }
4348
+ function withWorkspaceOwnershipTransaction(repoPath, operation) {
4349
+ const directory = ownershipDirectory(repoPath);
4350
+ import_node_fs13.default.mkdirSync(directory, { recursive: true });
4351
+ return withFileLockSync(
4352
+ import_node_path10.default.join(directory, ".lock"),
4353
+ { activity: "workspace ownership transaction", target: `'${repoPath}'`, waitMs: 5e3 },
4354
+ () => operation({
4355
+ list: () => listWorkspaceOwnershipInDirectory(directory),
4356
+ write: (input2) => writeWorkspaceOwnershipInDirectory(directory, input2),
4357
+ remove: (workspace) => removeWorkspaceOwnershipInDirectory(directory, workspace),
4358
+ removeIfMatches: (expected) => removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected)
4359
+ })
4360
+ );
4361
+ }
4362
+ function writeWorkspaceOwnership(repoPath, input2) {
4363
+ return withWorkspaceOwnershipTransaction(repoPath, (transaction) => transaction.write(input2));
4364
+ }
4365
+ function removeWorkspaceOwnership(repoPath, workspace) {
4366
+ return withWorkspaceOwnershipTransaction(
4367
+ repoPath,
4368
+ (transaction) => transaction.remove(workspace)
4369
+ );
4370
+ }
4371
+ function inspectWorkspaceOwnership(record, worktrees, devpods) {
4372
+ const worktree = worktrees.find(
4373
+ (candidate) => sameWorkspacePath(candidate.path, record.worktreePath)
4374
+ );
4375
+ const devpodOwnership = devpods ? inspectDevpodWorkspaceOwnership(devpods, record.devpodId, record.worktreePath) : void 0;
4376
+ const devpodStatus = devpodOwnership?.status ?? "unknown";
4377
+ if (devpodStatus === "conflict") {
4378
+ return { ownerStatus: "conflict", devpodStatus, worktree };
4379
+ }
4380
+ if (worktree?.locked) {
4381
+ return { ownerStatus: "locked", devpodStatus, worktree };
4382
+ }
4383
+ if ((!worktree || worktree.prunable) && import_node_fs13.default.existsSync(record.worktreePath)) {
4384
+ return { ownerStatus: "conflict", devpodStatus, worktree };
4385
+ }
4386
+ if (!worktree || worktree.prunable) {
4387
+ return { ownerStatus: "missing", devpodStatus, worktree };
4388
+ }
4389
+ let persisted;
4390
+ try {
4391
+ persisted = readPersistedWorkspace(worktree.path);
4392
+ } catch {
4393
+ return { ownerStatus: "conflict", devpodStatus, worktree };
4394
+ }
4395
+ return {
4396
+ ownerStatus: persisted === record.workspace ? "present" : "conflict",
4397
+ devpodStatus,
4398
+ worktree
4399
+ };
4400
+ }
4401
+ function listMissingWorkspaceOwnership(repoPath) {
4402
+ const worktrees = listGitWorktrees(repoPath);
4403
+ return listWorkspaceOwnership(repoPath).filter(
4404
+ (record) => inspectWorkspaceOwnership(record, worktrees, void 0).ownerStatus === "missing"
4405
+ );
4406
+ }
4407
+ var import_node_child_process7, import_node_crypto3, import_node_fs13, import_node_path10, OWNERSHIP_VERSION, OWNERSHIP_DIR;
4408
+ var init_workspace_ownership = __esm({
4409
+ "src/core/workspace-ownership.ts"() {
4410
+ "use strict";
4411
+ import_node_child_process7 = require("child_process");
4412
+ import_node_crypto3 = require("crypto");
4413
+ import_node_fs13 = __toESM(require("fs"));
4414
+ import_node_path10 = __toESM(require("path"));
4415
+ init_devpod_workspaces();
4416
+ init_file_lock();
4417
+ init_workspace();
4418
+ OWNERSHIP_VERSION = 1;
4419
+ OWNERSHIP_DIR = import_node_path10.default.join("devrouter", "workspaces");
4420
+ }
4421
+ });
4422
+
4423
+ // src/core/workspace-gc.ts
4424
+ function routesForOwner(routes, workspace, worktreePath) {
4425
+ return routes.filter(
4426
+ (route) => route.workspace === workspace && sameWorkspacePath(route.repoPath, worktreePath)
4427
+ );
4428
+ }
4429
+ function inRepositoryWorkspaceScope(repoPath, worktreePath, livePaths) {
4430
+ if (livePaths.some((candidate) => sameWorkspacePath(candidate, worktreePath))) return true;
4431
+ const comparableRepo = comparableWorkspacePath(repoPath);
4432
+ const comparableWorktree = comparableWorkspacePath(worktreePath);
4433
+ const localRoot = import_node_path11.default.join(comparableRepo, "trees") + import_node_path11.default.sep;
4434
+ if (comparableWorktree.startsWith(localRoot)) return true;
4435
+ const legacyPrefix = `${import_node_path11.default.basename(comparableRepo)}-`;
4436
+ return import_node_path11.default.dirname(comparableWorktree) === import_node_path11.default.dirname(comparableRepo) && import_node_path11.default.basename(comparableWorktree).startsWith(legacyPrefix);
4437
+ }
4438
+ function previewActions(devpodStatus, routeCount, includeRecord) {
4439
+ const actions = [
4440
+ {
4441
+ resource: "devpod",
4442
+ status: devpodStatus === "owned" ? "would-delete" : "already-absent"
4443
+ },
4444
+ {
4445
+ resource: "routes",
4446
+ status: routeCount > 0 ? "would-delete" : "already-absent",
4447
+ count: routeCount
4448
+ }
4449
+ ];
4450
+ if (includeRecord) actions.push({ resource: "record", status: "would-delete" });
4451
+ return actions;
4452
+ }
4453
+ function ownedReason(ownerStatus) {
4454
+ switch (ownerStatus) {
4455
+ case "missing":
4456
+ return "Ownership record is missing from live Git registration or marked prunable.";
4457
+ case "present":
4458
+ return "Registered worktree is present.";
4459
+ case "locked":
4460
+ return "Git worktree is locked and protected from garbage collection.";
4461
+ case "conflict":
4462
+ return "Ownership conflicts with live Git, persisted token, or DevPod source evidence.";
4463
+ }
4464
+ }
4465
+ function ownedCandidate(record, worktrees, devpods, routes) {
4466
+ const status = inspectWorkspaceOwnership(record, worktrees, devpods);
4467
+ const routeCount = routesForOwner(routes, record.workspace, record.worktreePath).length;
4468
+ const eligible = status.ownerStatus === "missing" && status.devpodStatus !== "conflict";
4469
+ return {
4470
+ kind: "owned",
4471
+ workspace: record.workspace,
4472
+ devpodId: record.devpodId,
4473
+ worktreePath: record.worktreePath,
4474
+ ownerStatus: status.ownerStatus,
4475
+ devpodStatus: status.devpodStatus,
4476
+ routeCount,
4477
+ eligible,
4478
+ reason: ownedReason(status.ownerStatus),
4479
+ actions: eligible ? previewActions(status.devpodStatus, routeCount, true) : [
4480
+ { resource: "devpod", status: "skipped" },
4481
+ { resource: "routes", status: "skipped", count: routeCount },
4482
+ { resource: "record", status: "skipped" }
4483
+ ]
4484
+ };
4485
+ }
4486
+ function legacyCandidates(repoPath, records, worktrees, devpods, routes) {
4487
+ const livePaths = worktrees.map((worktree) => worktree.path);
4488
+ const evidence = /* @__PURE__ */ new Map();
4489
+ const isRecordedRoute = (workspace, worktreePath) => records.some(
4490
+ (record) => record.workspace === workspace && sameWorkspacePath(record.worktreePath, worktreePath)
4491
+ );
4492
+ const isRecordedDevpod = (devpodId, worktreePath) => records.some(
4493
+ (record) => record.devpodId === devpodId && sameWorkspacePath(record.worktreePath, worktreePath)
4494
+ );
4495
+ const key = (workspace, worktreePath) => `${workspace}\0${comparableWorkspacePath(worktreePath)}`;
4496
+ for (const devpod of devpods) {
4497
+ if (isRecordedDevpod(devpod.id, devpod.source.localFolder) || !inRepositoryWorkspaceScope(repoPath, devpod.source.localFolder, livePaths)) {
4498
+ continue;
4499
+ }
4500
+ evidence.set(key(devpod.id, devpod.source.localFolder), {
4501
+ workspace: devpod.id,
4502
+ worktreePath: comparableWorkspacePath(devpod.source.localFolder),
4503
+ devpodStatus: "owned",
4504
+ routeCount: 0
4505
+ });
4506
+ }
4507
+ for (const route of routes) {
4508
+ if (!route.workspace || isRecordedRoute(route.workspace, route.repoPath) || !inRepositoryWorkspaceScope(repoPath, route.repoPath, livePaths)) {
4509
+ continue;
4510
+ }
4511
+ const evidenceKey = key(route.workspace, route.repoPath);
4512
+ const existing = evidence.get(evidenceKey);
4513
+ evidence.set(evidenceKey, {
4514
+ workspace: route.workspace,
4515
+ worktreePath: comparableWorkspacePath(route.repoPath),
4516
+ devpodStatus: existing?.devpodStatus ?? "absent",
4517
+ routeCount: (existing?.routeCount ?? 0) + 1
4518
+ });
4519
+ }
4520
+ return Array.from(evidence.values()).map((entry) => ({
4521
+ kind: "legacy",
4522
+ ...entry,
4523
+ eligible: false,
4524
+ reason: "Legacy workspace has no ownership record; use explicit workspace down or manual cleanup.",
4525
+ actions: [
4526
+ { resource: "devpod", status: "skipped" },
4527
+ { resource: "routes", status: "skipped", count: entry.routeCount }
4528
+ ]
4529
+ }));
4530
+ }
4531
+ function revalidateCandidate(candidate, repoPath, transaction) {
4532
+ const record = transaction.list().find((entry) => entry.workspace === candidate.workspace);
4533
+ if (!record || !sameWorkspacePath(record.worktreePath, candidate.worktreePath) || record.devpodId !== candidate.devpodId) {
4534
+ return {
4535
+ status: "blocked",
4536
+ candidate: {
4537
+ ...candidate,
4538
+ eligible: false,
4539
+ reason: "Ownership record disappeared or changed before cleanup.",
4540
+ actions: [
4541
+ { resource: "devpod", status: "skipped" },
4542
+ { resource: "routes", status: "skipped", count: candidate.routeCount },
4543
+ { resource: "record", status: "skipped" }
4544
+ ]
4545
+ }
4546
+ };
4547
+ }
4548
+ const fresh = ownedCandidate(
4549
+ record,
4550
+ listGitWorktrees(repoPath),
4551
+ listDevpodWorkspaces(),
4552
+ listHostRouteState()
4553
+ );
4554
+ if (fresh.eligible) return { status: "ready", candidate: fresh, record };
4555
+ return {
4556
+ status: "blocked",
4557
+ candidate: {
4558
+ ...fresh,
4559
+ reason: `Ownership changed before cleanup: ${fresh.reason}`
4560
+ }
4561
+ };
4562
+ }
4563
+ function recheckDevpodOwnership(record) {
4564
+ const ownership = inspectDevpodWorkspaceOwnership(
4565
+ listDevpodWorkspaces(),
4566
+ record.devpodId,
4567
+ record.worktreePath
4568
+ );
4569
+ if (ownership.status === "conflict") {
4570
+ throw new Error(ownership.reason);
4571
+ }
4572
+ return ownership.status;
4573
+ }
4574
+ function failedAction(resource, error) {
4575
+ return {
4576
+ resource,
4577
+ status: "failed",
4578
+ error: error instanceof Error ? error.message : String(error)
4579
+ };
4580
+ }
4581
+ function applyCandidate(candidate, repoPath) {
4582
+ try {
4583
+ return withWorkspaceOwnershipTransaction(repoPath, (transaction) => {
4584
+ let revalidated;
4585
+ try {
4586
+ revalidated = revalidateCandidate(candidate, repoPath, transaction);
4587
+ } catch (error) {
4588
+ return {
4589
+ ...candidate,
4590
+ actions: [
4591
+ failedAction(
4592
+ "record",
4593
+ `Ownership revalidation failed: ${error instanceof Error ? error.message : String(error)}`
4594
+ )
4595
+ ]
4596
+ };
4597
+ }
4598
+ if (revalidated.status === "blocked") return revalidated.candidate;
4599
+ const { candidate: fresh, record } = revalidated;
4600
+ const actions = [];
4601
+ let devpodStatus;
4602
+ try {
4603
+ devpodStatus = recheckDevpodOwnership(record);
4604
+ if (devpodStatus === "owned") runDevpodWorkspaceAction("delete", record.devpodId);
4605
+ } catch (error) {
4606
+ return { ...fresh, actions: [failedAction("devpod", error)] };
4607
+ }
4608
+ actions.push({
4609
+ resource: "devpod",
4610
+ status: devpodStatus === "owned" ? "deleted" : "already-absent"
4611
+ });
4612
+ try {
4613
+ const routes = removeWorkspaceRoutesForWorktree(fresh.workspace, fresh.worktreePath);
4614
+ actions.push({
4615
+ resource: "routes",
4616
+ status: routes.length > 0 ? "deleted" : "already-absent",
4617
+ count: routes.length
4618
+ });
4619
+ } catch (error) {
4620
+ return { ...fresh, actions: [...actions, failedAction("routes", error)] };
4621
+ }
4622
+ try {
4623
+ const removal = transaction.removeIfMatches(record);
4624
+ if (removal === "changed") {
4625
+ throw new Error("Ownership record changed during cleanup; record was retained.");
4626
+ }
4627
+ actions.push({
4628
+ resource: "record",
4629
+ status: removal === "removed" ? "deleted" : "already-absent"
4630
+ });
4631
+ } catch (error) {
4632
+ return { ...fresh, actions: [...actions, failedAction("record", error)] };
4633
+ }
4634
+ return { ...fresh, actions };
4635
+ });
4636
+ } catch (error) {
4637
+ const detail = error instanceof Error ? error.message : String(error);
4638
+ return {
4639
+ ...candidate,
4640
+ actions: [failedAction("record", `Ownership transaction failed: ${detail}`)]
4641
+ };
4642
+ }
4643
+ }
4644
+ function summarizeGc(mode, candidates) {
4645
+ return {
4646
+ total: candidates.length,
4647
+ eligible: candidates.filter((candidate) => candidate.eligible).length,
4648
+ cleaned: mode === "apply" ? candidates.filter(
4649
+ (candidate) => candidate.actions.some(
4650
+ (action2) => action2.resource === "record" && (action2.status === "deleted" || action2.status === "already-absent")
4651
+ )
4652
+ ).length : 0,
4653
+ blocked: candidates.filter((candidate) => !candidate.eligible).length,
4654
+ errors: candidates.filter(
4655
+ (candidate) => candidate.actions.some((action2) => action2.status === "failed")
4656
+ ).length
4657
+ };
4658
+ }
4659
+ function inspectWorkspaceGc(repo) {
4660
+ const repoPath = comparableWorkspacePath(resolveRepoPath(repo));
4661
+ const worktrees = listGitWorktrees(repoPath);
4662
+ const records = listWorkspaceOwnership(repoPath);
4663
+ const devpods = listDevpodWorkspaces();
4664
+ const routes = listHostRouteState();
4665
+ const candidates = [
4666
+ ...records.map((record) => ownedCandidate(record, worktrees, devpods, routes)),
4667
+ ...legacyCandidates(repoPath, records, worktrees, devpods, routes)
4668
+ ];
4669
+ return {
4670
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4671
+ repoPath,
4672
+ mode: "dry-run",
4673
+ summary: summarizeGc("dry-run", candidates),
4674
+ candidates
4675
+ };
4676
+ }
4677
+ function applyWorkspaceGc(plan) {
4678
+ const candidates = plan.candidates.map(
4679
+ (candidate) => candidate.eligible ? applyCandidate(candidate, plan.repoPath) : candidate
4680
+ );
4681
+ return {
4682
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4683
+ repoPath: plan.repoPath,
4684
+ mode: "apply",
4685
+ summary: summarizeGc("apply", candidates),
4686
+ candidates
4687
+ };
4688
+ }
4689
+ var import_node_path11;
4690
+ var init_workspace_gc = __esm({
4691
+ "src/core/workspace-gc.ts"() {
4692
+ "use strict";
4693
+ import_node_path11 = __toESM(require("path"));
4694
+ init_devpod_workspaces();
4695
+ init_host_routes();
4696
+ init_repo_config();
4697
+ init_route_state();
4698
+ init_workspace();
4699
+ init_workspace_ownership();
4700
+ }
4701
+ });
4702
+
4067
4703
  // src/core/doctor.ts
4068
4704
  function asRecord2(value) {
4069
4705
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -4123,11 +4759,11 @@ function inspectPostgresCredentials(repoPath, config) {
4123
4759
  parseErrors.push(`${app.name}: ${composeFile} (${message})`);
4124
4760
  continue;
4125
4761
  }
4126
- if (!import_node_fs13.default.existsSync(absolutePath)) {
4762
+ if (!import_node_fs14.default.existsSync(absolutePath)) {
4127
4763
  continue;
4128
4764
  }
4129
4765
  try {
4130
- const raw = import_node_fs13.default.readFileSync(absolutePath, "utf-8");
4766
+ const raw = import_node_fs14.default.readFileSync(absolutePath, "utf-8");
4131
4767
  const parsed = import_yaml4.default.parse(raw);
4132
4768
  const root = asRecord2(parsed);
4133
4769
  const services = asRecord2(root?.services);
@@ -4390,7 +5026,7 @@ async function buildDoctorReport(options = {}) {
4390
5026
  const config = runtimeConfig.config;
4391
5027
  loadedConfig = config;
4392
5028
  loadedWorkspace = runtimeConfig.workspace;
4393
- const cliVersion = true ? "0.0.30" : "0.0.0-dev";
5029
+ const cliVersion = true ? "0.0.31" : "0.0.0-dev";
4394
5030
  const configVersion = config.devrouter?.version;
4395
5031
  if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
4396
5032
  addCheck(checks, {
@@ -4420,9 +5056,9 @@ async function buildDoctorReport(options = {}) {
4420
5056
  (app) => app.docker.composeFiles.map((filePath) => ({
4421
5057
  app: app.name,
4422
5058
  filePath,
4423
- absolutePath: import_node_path10.default.resolve(repo.path, filePath)
5059
+ absolutePath: import_node_path12.default.resolve(repo.path, filePath)
4424
5060
  }))
4425
- ).filter((entry) => !import_node_fs13.default.existsSync(entry.absolutePath));
5061
+ ).filter((entry) => !import_node_fs14.default.existsSync(entry.absolutePath));
4426
5062
  addCheck(checks, {
4427
5063
  id: "repo.compose-files",
4428
5064
  level: missingComposeFiles.length === 0 ? "ok" : "error",
@@ -4465,8 +5101,8 @@ async function buildDoctorReport(options = {}) {
4465
5101
  const missingHostCwds = config.apps.filter((app) => app.runtime === "host").map((app) => ({
4466
5102
  app: app.name,
4467
5103
  cwd: app.hostRun.cwd,
4468
- absolutePath: import_node_path10.default.resolve(repo.path, app.hostRun.cwd)
4469
- })).filter((entry) => !import_node_fs13.default.existsSync(entry.absolutePath));
5104
+ absolutePath: import_node_path12.default.resolve(repo.path, app.hostRun.cwd)
5105
+ })).filter((entry) => !import_node_fs14.default.existsSync(entry.absolutePath));
4470
5106
  addCheck(checks, {
4471
5107
  id: "repo.host-cwd",
4472
5108
  level: missingHostCwds.length === 0 ? "ok" : "error",
@@ -4569,13 +5205,37 @@ async function buildDoctorReport(options = {}) {
4569
5205
  summary: staleCount === 0 ? "No stale host route entries detected." : `${staleCount} stale host route entr${staleCount === 1 ? "y" : "ies"} detected (dead PID).`,
4570
5206
  suggestion: staleCount === 0 ? void 0 : "Re-run the affected host app(s) or remove routes with: dev app rm <name> --repo <path> --keep-config"
4571
5207
  });
4572
- const orphanedWorkspaceRoutes = findOrphanedWorkspaceProxyRoutes();
4573
- addCheck(checks, {
4574
- id: "routes.orphaned-workspace-routes",
4575
- level: orphanedWorkspaceRoutes.length === 0 ? "ok" : "warn",
4576
- summary: orphanedWorkspaceRoutes.length === 0 ? "No orphaned workspace proxy routes detected." : `${orphanedWorkspaceRoutes.length} orphaned workspace proxy route entr${orphanedWorkspaceRoutes.length === 1 ? "y" : "ies"} detected (worktree removed without 'dev workspace down').`,
4577
- suggestion: orphanedWorkspaceRoutes.length === 0 ? void 0 : "Run: dev workspace down <workspace>"
4578
- });
5208
+ let gitWorkspaceRepo = false;
5209
+ try {
5210
+ resolveGitCommonDir(resolvedRepoPath);
5211
+ gitWorkspaceRepo = true;
5212
+ } catch {
5213
+ }
5214
+ if (gitWorkspaceRepo) {
5215
+ try {
5216
+ const gc = inspectWorkspaceGc(resolvedRepoPath);
5217
+ const needsAttention = gc.candidates.filter(
5218
+ (candidate) => candidate.eligible || candidate.kind === "legacy" || candidate.ownerStatus === "conflict"
5219
+ );
5220
+ addCheck(checks, {
5221
+ id: "workspace.ownership-cleanup",
5222
+ level: needsAttention.length === 0 ? "ok" : "warn",
5223
+ summary: needsAttention.length === 0 ? "No missing or legacy workspace ownership detected." : `${needsAttention.length} workspace ownership entr${needsAttention.length === 1 ? "y needs" : "ies need"} explicit cleanup review.`,
5224
+ details: needsAttention.length === 0 ? void 0 : needsAttention.map(
5225
+ (candidate) => `${candidate.workspace}: ${candidate.worktreePath} (${candidate.reason})`
5226
+ ).join("\n"),
5227
+ suggestion: needsAttention.length === 0 ? void 0 : `Run: dev workspace gc --repo ${resolvedRepoPath}`
5228
+ });
5229
+ } catch (error) {
5230
+ addCheck(checks, {
5231
+ id: "workspace.ownership-cleanup",
5232
+ level: "warn",
5233
+ summary: "Could not inspect workspace ownership cleanup state.",
5234
+ details: error instanceof Error ? error.message : String(error),
5235
+ suggestion: `Run: dev workspace gc --repo ${resolvedRepoPath}`
5236
+ });
5237
+ }
5238
+ }
4579
5239
  const summary = collectSummary(checks);
4580
5240
  const nextSteps = collectNextSteps(checks, statusNextSteps);
4581
5241
  return {
@@ -4586,12 +5246,12 @@ async function buildDoctorReport(options = {}) {
4586
5246
  nextSteps
4587
5247
  };
4588
5248
  }
4589
- var import_node_fs13, import_node_path10, import_yaml4, POSTGRES_DEFAULTS;
5249
+ var import_node_fs14, import_node_path12, import_yaml4, POSTGRES_DEFAULTS;
4590
5250
  var init_doctor = __esm({
4591
5251
  "src/core/doctor.ts"() {
4592
5252
  "use strict";
4593
- import_node_fs13 = __toESM(require("fs"));
4594
- import_node_path10 = __toESM(require("path"));
5253
+ import_node_fs14 = __toESM(require("fs"));
5254
+ import_node_path12 = __toESM(require("path"));
4595
5255
  import_yaml4 = __toESM(require("yaml"));
4596
5256
  init_docker();
4597
5257
  init_host_routes();
@@ -4604,6 +5264,8 @@ var init_doctor = __esm({
4604
5264
  init_status();
4605
5265
  init_tls();
4606
5266
  init_tool_diagnostics();
5267
+ init_workspace_gc();
5268
+ init_workspace_ownership();
4607
5269
  POSTGRES_DEFAULTS = {
4608
5270
  POSTGRES_USER: "prisma",
4609
5271
  POSTGRES_PASSWORD: "prisma",
@@ -4840,12 +5502,12 @@ function parseSsPortListeners(stdout, port) {
4840
5502
  return listeners;
4841
5503
  }
4842
5504
  function findPortListeners(port) {
4843
- const result = (0, import_node_child_process6.spawnSync)("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN"], {
5505
+ const result = (0, import_node_child_process8.spawnSync)("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN"], {
4844
5506
  encoding: "utf-8"
4845
5507
  });
4846
5508
  if (result.error && result.error.code === "ENOENT") {
4847
5509
  if (process.platform === "linux") {
4848
- const ssResult = (0, import_node_child_process6.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
5510
+ const ssResult = (0, import_node_child_process8.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
4849
5511
  if (ssResult.status === 0 && ssResult.stdout) {
4850
5512
  return parseSsPortListeners(ssResult.stdout, port);
4851
5513
  }
@@ -4874,11 +5536,11 @@ function findPortListeners(port) {
4874
5536
  };
4875
5537
  });
4876
5538
  }
4877
- var import_node_child_process6;
5539
+ var import_node_child_process8;
4878
5540
  var init_ports = __esm({
4879
5541
  "src/util/ports.ts"() {
4880
5542
  "use strict";
4881
- import_node_child_process6 = require("child_process");
5543
+ import_node_child_process8 = require("child_process");
4882
5544
  }
4883
5545
  });
4884
5546
 
@@ -5074,7 +5736,7 @@ async function runOpenCommand(name) {
5074
5736
  );
5075
5737
  return;
5076
5738
  }
5077
- const result = (0, import_node_child_process7.spawnSync)("open", [url], { encoding: "utf-8" });
5739
+ const result = (0, import_node_child_process9.spawnSync)("open", [url], { encoding: "utf-8" });
5078
5740
  if (result.status !== 0) {
5079
5741
  const details = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
5080
5742
  throw new Error(`Unable to open '${url}': ${details || "unknown error"}`);
@@ -5082,11 +5744,11 @@ async function runOpenCommand(name) {
5082
5744
  process.stdout.write(`Opened ${url}
5083
5745
  `);
5084
5746
  }
5085
- var import_node_child_process7;
5747
+ var import_node_child_process9;
5086
5748
  var init_open = __esm({
5087
5749
  "src/commands/open.ts"() {
5088
5750
  "use strict";
5089
- import_node_child_process7 = require("child_process");
5751
+ import_node_child_process9 = require("child_process");
5090
5752
  init_docker();
5091
5753
  init_host_routes();
5092
5754
  init_repo_config();
@@ -5109,7 +5771,7 @@ async function runLogsCommand(options) {
5109
5771
  const args = ["logs", "--tail", tail, ROUTER_CONTAINER_NAME];
5110
5772
  if (options.follow) {
5111
5773
  args.splice(1, 0, "-f");
5112
- const child = (0, import_node_child_process8.spawn)("docker", args, { stdio: "inherit" });
5774
+ const child = (0, import_node_child_process10.spawn)("docker", args, { stdio: "inherit" });
5113
5775
  const onSignal = () => {
5114
5776
  child.kill("SIGTERM");
5115
5777
  };
@@ -5123,17 +5785,17 @@ async function runLogsCommand(options) {
5123
5785
  });
5124
5786
  });
5125
5787
  } else {
5126
- const result = (0, import_node_child_process8.spawnSync)("docker", args, { stdio: "inherit" });
5788
+ const result = (0, import_node_child_process10.spawnSync)("docker", args, { stdio: "inherit" });
5127
5789
  if (result.status !== 0) {
5128
5790
  throw new Error("Failed to retrieve router logs.");
5129
5791
  }
5130
5792
  }
5131
5793
  }
5132
- var import_node_child_process8;
5794
+ var import_node_child_process10;
5133
5795
  var init_logs = __esm({
5134
5796
  "src/commands/logs.ts"() {
5135
5797
  "use strict";
5136
- import_node_child_process8 = require("child_process");
5798
+ import_node_child_process10 = require("child_process");
5137
5799
  init_docker();
5138
5800
  init_router();
5139
5801
  }
@@ -5173,17 +5835,17 @@ function asRecord3(value) {
5173
5835
  return value;
5174
5836
  }
5175
5837
  function readJson(filePath) {
5176
- if (!import_node_fs14.default.existsSync(filePath)) {
5838
+ if (!import_node_fs15.default.existsSync(filePath)) {
5177
5839
  return void 0;
5178
5840
  }
5179
5841
  try {
5180
- return JSON.parse(import_node_fs14.default.readFileSync(filePath, "utf-8"));
5842
+ return JSON.parse(import_node_fs15.default.readFileSync(filePath, "utf-8"));
5181
5843
  } catch {
5182
5844
  return void 0;
5183
5845
  }
5184
5846
  }
5185
5847
  function relative(repoPath, filePath) {
5186
- return import_node_path11.default.relative(repoPath, filePath) || ".";
5848
+ return import_node_path13.default.relative(repoPath, filePath) || ".";
5187
5849
  }
5188
5850
  function redactEnvAssignments(value) {
5189
5851
  return value.replace(
@@ -5224,7 +5886,7 @@ function inspectPackageManager(repoPath, pkg) {
5224
5886
  ["bun.lock", "bun"]
5225
5887
  ];
5226
5888
  for (const [fileName, name] of lockfiles) {
5227
- if (import_node_fs14.default.existsSync(import_node_path11.default.join(repoPath, fileName))) {
5889
+ if (import_node_fs15.default.existsSync(import_node_path13.default.join(repoPath, fileName))) {
5228
5890
  return { name, source: fileName };
5229
5891
  }
5230
5892
  }
@@ -5239,9 +5901,9 @@ function inspectNode(repoPath, pkg) {
5239
5901
  if (typeof engines?.node === "string") {
5240
5902
  return { version: engines.node, source: "package.json:engines.node" };
5241
5903
  }
5242
- const nvmrc = import_node_path11.default.join(repoPath, ".nvmrc");
5243
- if (import_node_fs14.default.existsSync(nvmrc)) {
5244
- const version = import_node_fs14.default.readFileSync(nvmrc, "utf-8").trim();
5904
+ const nvmrc = import_node_path13.default.join(repoPath, ".nvmrc");
5905
+ if (import_node_fs15.default.existsSync(nvmrc)) {
5906
+ const version = import_node_fs15.default.readFileSync(nvmrc, "utf-8").trim();
5245
5907
  return { version, source: ".nvmrc" };
5246
5908
  }
5247
5909
  return void 0;
@@ -5302,7 +5964,7 @@ function configuredComposeFiles(repoPath) {
5302
5964
  const files = config.apps.filter(
5303
5965
  (app) => app.runtime === "docker"
5304
5966
  ).flatMap((app) => app.docker.composeFiles).filter(
5305
- (fileName) => !import_node_path11.default.isAbsolute(fileName) && !import_node_path11.default.normalize(fileName).startsWith("..")
5967
+ (fileName) => !import_node_path13.default.isAbsolute(fileName) && !import_node_path13.default.normalize(fileName).startsWith("..")
5306
5968
  );
5307
5969
  return Array.from(new Set(files));
5308
5970
  } catch {
@@ -5320,7 +5982,7 @@ function composeFiles(repoPath) {
5320
5982
  ...configuredComposeFiles(repoPath)
5321
5983
  ];
5322
5984
  return Array.from(new Set(candidates)).filter(
5323
- (fileName) => import_node_fs14.default.existsSync(import_node_path11.default.join(repoPath, fileName))
5985
+ (fileName) => import_node_fs15.default.existsSync(import_node_path13.default.join(repoPath, fileName))
5324
5986
  );
5325
5987
  }
5326
5988
  function stringArray(value) {
@@ -5358,7 +6020,7 @@ function inspectServices(repoPath) {
5358
6020
  const services = [];
5359
6021
  for (const fileName of composeFiles(repoPath)) {
5360
6022
  try {
5361
- const parsed = import_yaml5.default.parse(import_node_fs14.default.readFileSync(import_node_path11.default.join(repoPath, fileName), "utf-8"));
6023
+ const parsed = import_yaml5.default.parse(import_node_fs15.default.readFileSync(import_node_path13.default.join(repoPath, fileName), "utf-8"));
5362
6024
  const serviceMap = asRecord3(asRecord3(parsed)?.services);
5363
6025
  for (const [name, value] of Object.entries(serviceMap ?? {})) {
5364
6026
  const service = asRecord3(value);
@@ -5393,8 +6055,8 @@ function inspectServices(repoPath) {
5393
6055
  return services;
5394
6056
  }
5395
6057
  function inspectEnvFiles(repoPath) {
5396
- const files = import_node_fs14.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
5397
- const content = import_node_fs14.default.readFileSync(import_node_path11.default.join(repoPath, fileName), "utf-8");
6058
+ const files = import_node_fs15.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
6059
+ const content = import_node_fs15.default.readFileSync(import_node_path13.default.join(repoPath, fileName), "utf-8");
5398
6060
  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();
5399
6061
  return { path: fileName, names };
5400
6062
  });
@@ -5408,18 +6070,18 @@ function inspectEnvFiles(repoPath) {
5408
6070
  };
5409
6071
  }
5410
6072
  function inspectDevcontainer(repoPath) {
5411
- const dir = import_node_path11.default.join(repoPath, ".devcontainer");
5412
- if (!import_node_fs14.default.existsSync(dir)) {
6073
+ const dir = import_node_path13.default.join(repoPath, ".devcontainer");
6074
+ if (!import_node_fs15.default.existsSync(dir)) {
5413
6075
  return { exists: false, files: [] };
5414
6076
  }
5415
6077
  return {
5416
6078
  exists: true,
5417
- files: import_node_fs14.default.readdirSync(dir).filter((fileName) => import_node_fs14.default.statSync(import_node_path11.default.join(dir, fileName)).isFile()).sort().map((fileName) => `.devcontainer/${fileName}`)
6079
+ files: import_node_fs15.default.readdirSync(dir).filter((fileName) => import_node_fs15.default.statSync(import_node_path13.default.join(dir, fileName)).isFile()).sort().map((fileName) => `.devcontainer/${fileName}`)
5418
6080
  };
5419
6081
  }
5420
6082
  function inspectDevrouter(repoPath) {
5421
6083
  const configPath = getRepoConfigPath(repoPath);
5422
- if (!import_node_fs14.default.existsSync(configPath)) {
6084
+ if (!import_node_fs15.default.existsSync(configPath)) {
5423
6085
  return {
5424
6086
  exists: false,
5425
6087
  configPath,
@@ -5463,15 +6125,15 @@ function inspectAgentGuidance(repoPath) {
5463
6125
  ["AGENTS.md", "agents"],
5464
6126
  ["CLAUDE.md", "claude"]
5465
6127
  ]) {
5466
- if (import_node_fs14.default.existsSync(import_node_path11.default.join(repoPath, fileName))) {
6128
+ if (import_node_fs15.default.existsSync(import_node_path13.default.join(repoPath, fileName))) {
5467
6129
  results.push({ path: fileName, kind });
5468
6130
  }
5469
6131
  }
5470
- const skillsDir = import_node_path11.default.join(repoPath, ".agents", "skills");
5471
- if (import_node_fs14.default.existsSync(skillsDir)) {
5472
- for (const name of import_node_fs14.default.readdirSync(skillsDir).sort()) {
5473
- const skillPath = import_node_path11.default.join(skillsDir, name, "SKILL.md");
5474
- if (import_node_fs14.default.existsSync(skillPath)) {
6132
+ const skillsDir = import_node_path13.default.join(repoPath, ".agents", "skills");
6133
+ if (import_node_fs15.default.existsSync(skillsDir)) {
6134
+ for (const name of import_node_fs15.default.readdirSync(skillsDir).sort()) {
6135
+ const skillPath = import_node_path13.default.join(skillsDir, name, "SKILL.md");
6136
+ if (import_node_fs15.default.existsSync(skillPath)) {
5475
6137
  results.push({ path: relative(repoPath, skillPath), kind: "skill" });
5476
6138
  }
5477
6139
  }
@@ -5514,7 +6176,7 @@ function buildIssues(report) {
5514
6176
  }
5515
6177
  function inspectRepo(options = {}) {
5516
6178
  const repoPath = resolveRepoPath(options.repo);
5517
- const pkg = readJson(import_node_path11.default.join(repoPath, "package.json"));
6179
+ const pkg = readJson(import_node_path13.default.join(repoPath, "package.json"));
5518
6180
  const scripts = inspectScripts(pkg);
5519
6181
  const reportWithoutIssues = {
5520
6182
  repoPath,
@@ -5533,12 +6195,12 @@ function inspectRepo(options = {}) {
5533
6195
  issues: buildIssues(reportWithoutIssues)
5534
6196
  };
5535
6197
  }
5536
- var import_node_fs14, import_node_path11, import_yaml5;
6198
+ var import_node_fs15, import_node_path13, import_yaml5;
5537
6199
  var init_repo_inspect = __esm({
5538
6200
  "src/core/repo-inspect.ts"() {
5539
6201
  "use strict";
5540
- import_node_fs14 = __toESM(require("fs"));
5541
- import_node_path11 = __toESM(require("path"));
6202
+ import_node_fs15 = __toESM(require("fs"));
6203
+ import_node_path13 = __toESM(require("path"));
5542
6204
  import_yaml5 = __toESM(require("yaml"));
5543
6205
  init_repo_config();
5544
6206
  }
@@ -5716,7 +6378,7 @@ function requiredFileChecks(repoPath) {
5716
6378
  ".devcontainer/docker-compose.yml",
5717
6379
  ".devrouter.yml"
5718
6380
  ];
5719
- const missing = required.filter((fileName) => !import_node_fs15.default.existsSync(import_node_path12.default.join(repoPath, fileName)));
6381
+ const missing = required.filter((fileName) => !import_node_fs16.default.existsSync(import_node_path14.default.join(repoPath, fileName)));
5720
6382
  return {
5721
6383
  id: "repo.devcontainer.verify-files",
5722
6384
  level: missing.length === 0 ? "ok" : "error",
@@ -5781,7 +6443,7 @@ function routeUrl2(host) {
5781
6443
  return `${isTLSEnabled() ? "https" : "http"}://${host}`;
5782
6444
  }
5783
6445
  function curlRoute(host) {
5784
- const result = (0, import_node_child_process9.spawnSync)("curl", ["-k", "-fsS", "--max-time", "5", routeUrl2(host)], {
6446
+ const result = (0, import_node_child_process11.spawnSync)("curl", ["-k", "-fsS", "--max-time", "5", routeUrl2(host)], {
5785
6447
  encoding: "utf-8"
5786
6448
  });
5787
6449
  if (result.status === 0) {
@@ -5955,13 +6617,13 @@ async function verifyDevcontainer(options = {}) {
5955
6617
  nextSteps: collectNextSteps3(checks)
5956
6618
  };
5957
6619
  }
5958
- var import_node_child_process9, import_node_fs15, import_node_path12;
6620
+ var import_node_child_process11, import_node_fs16, import_node_path14;
5959
6621
  var init_devcontainer_verify = __esm({
5960
6622
  "src/core/devcontainer-verify.ts"() {
5961
6623
  "use strict";
5962
- import_node_child_process9 = require("child_process");
5963
- import_node_fs15 = __toESM(require("fs"));
5964
- import_node_path12 = __toESM(require("path"));
6624
+ import_node_child_process11 = require("child_process");
6625
+ import_node_fs16 = __toESM(require("fs"));
6626
+ import_node_path14 = __toESM(require("path"));
5965
6627
  init_capabilities();
5966
6628
  init_concurrency();
5967
6629
  init_doctor();
@@ -6280,7 +6942,7 @@ function packageManagerIssues(repo) {
6280
6942
  }
6281
6943
  function plannedFiles(repoPath, version) {
6282
6944
  const repo = inspectRepo({ repo: repoPath });
6283
- const projectName = sanitizeProjectName(import_node_path13.default.basename(repo.repoPath));
6945
+ const projectName = sanitizeProjectName(import_node_path15.default.basename(repo.repoPath));
6284
6946
  const nodeMajor = majorVersion(repo.node?.version, "24");
6285
6947
  const pnpmVersion = repo.packageManager?.name === "pnpm" && repo.packageManager.version ? repo.packageManager.version : DEFAULT_PNPM_VERSION;
6286
6948
  const port = inferPort2(repo);
@@ -6325,8 +6987,8 @@ function plannedFiles(repoPath, version) {
6325
6987
  };
6326
6988
  }
6327
6989
  function classifyFile(repoPath, file) {
6328
- const absolutePath = import_node_path13.default.join(repoPath, file.relativePath);
6329
- if (!import_node_fs16.default.existsSync(absolutePath)) {
6990
+ const absolutePath = import_node_path15.default.join(repoPath, file.relativePath);
6991
+ if (!import_node_fs17.default.existsSync(absolutePath)) {
6330
6992
  return {
6331
6993
  path: file.relativePath,
6332
6994
  action: "create",
@@ -6334,7 +6996,7 @@ function classifyFile(repoPath, file) {
6334
6996
  bytes: Buffer.byteLength(file.content)
6335
6997
  };
6336
6998
  }
6337
- const current = import_node_fs16.default.readFileSync(absolutePath, "utf-8");
6999
+ const current = import_node_fs17.default.readFileSync(absolutePath, "utf-8");
6338
7000
  if (!current.includes(MANAGED_MARKER)) {
6339
7001
  return {
6340
7002
  path: file.relativePath,
@@ -6391,11 +7053,11 @@ function buildPlan(repoPath, dryRun, version) {
6391
7053
  };
6392
7054
  }
6393
7055
  function writeFile(repoPath, file) {
6394
- const absolutePath = import_node_path13.default.join(repoPath, file.relativePath);
6395
- import_node_fs16.default.mkdirSync(import_node_path13.default.dirname(absolutePath), { recursive: true });
6396
- import_node_fs16.default.writeFileSync(absolutePath, file.content, "utf-8");
7056
+ const absolutePath = import_node_path15.default.join(repoPath, file.relativePath);
7057
+ import_node_fs17.default.mkdirSync(import_node_path15.default.dirname(absolutePath), { recursive: true });
7058
+ import_node_fs17.default.writeFileSync(absolutePath, file.content, "utf-8");
6397
7059
  if (file.executable) {
6398
- import_node_fs16.default.chmodSync(absolutePath, 493);
7060
+ import_node_fs17.default.chmodSync(absolutePath, 493);
6399
7061
  }
6400
7062
  }
6401
7063
  function writeDevcontainer(options = {}) {
@@ -6433,12 +7095,12 @@ function writeDevcontainer(options = {}) {
6433
7095
  nextSteps: postWriteNextSteps(repoPath)
6434
7096
  };
6435
7097
  }
6436
- var import_node_fs16, import_node_path13, MANAGED_MARKER, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
7098
+ var import_node_fs17, import_node_path15, MANAGED_MARKER, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
6437
7099
  var init_devcontainer_write = __esm({
6438
7100
  "src/core/devcontainer-write.ts"() {
6439
7101
  "use strict";
6440
- import_node_fs16 = __toESM(require("fs"));
6441
- import_node_path13 = __toESM(require("path"));
7102
+ import_node_fs17 = __toESM(require("fs"));
7103
+ import_node_path15 = __toESM(require("path"));
6442
7104
  init_repo_config();
6443
7105
  init_repo_inspect();
6444
7106
  init_workspace();
@@ -6765,7 +7427,7 @@ function sanitizeRouterId(value) {
6765
7427
  return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
6766
7428
  }
6767
7429
  function repoHash(repoPath) {
6768
- return (0, import_node_crypto3.createHash)("sha1").update(import_node_path14.default.resolve(repoPath)).digest("hex").slice(0, 12);
7430
+ return (0, import_node_crypto4.createHash)("sha1").update(import_node_path16.default.resolve(repoPath)).digest("hex").slice(0, 12);
6769
7431
  }
6770
7432
  function asDockerApp(app) {
6771
7433
  return app.runtime === "docker";
@@ -6844,11 +7506,11 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
6844
7506
  if (dockerApps.length === 0) {
6845
7507
  throw new Error("No docker apps selected to prepare compose overlay.");
6846
7508
  }
6847
- const cachePath = import_node_path14.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
6848
- import_node_fs17.default.mkdirSync(cachePath, { recursive: true });
6849
- const overlayPath = import_node_path14.default.join(cachePath, "compose.devrouter.yml");
7509
+ const cachePath = import_node_path16.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
7510
+ import_node_fs18.default.mkdirSync(cachePath, { recursive: true });
7511
+ const overlayPath = import_node_path16.default.join(cachePath, "compose.devrouter.yml");
6850
7512
  const overlayDocument = buildOverlayDocument(dockerApps, publishTcpPorts);
6851
- import_node_fs17.default.writeFileSync(overlayPath, import_yaml6.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
7513
+ import_node_fs18.default.writeFileSync(overlayPath, import_yaml6.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
6852
7514
  return {
6853
7515
  overlayPath,
6854
7516
  composeFiles: ensureComposeFiles(dockerApps),
@@ -6862,7 +7524,7 @@ function runDockerComposeUp(repoPath, composeFiles2, overlayPath, services) {
6862
7524
  fileArgs.push("-f", resolved);
6863
7525
  }
6864
7526
  const args = ["compose", ...fileArgs, "-f", overlayPath, "up", "-d", "--wait", ...services];
6865
- const result = (0, import_node_child_process10.spawnSync)("docker", args, {
7527
+ const result = (0, import_node_child_process12.spawnSync)("docker", args, {
6866
7528
  encoding: "utf-8",
6867
7529
  cwd: repoPath
6868
7530
  });
@@ -6890,7 +7552,7 @@ function queryRunningComposeServices(repoPath, composeFiles2, overlayPath, servi
6890
7552
  "--services",
6891
7553
  ...services
6892
7554
  ];
6893
- const result = (0, import_node_child_process10.spawnSync)("docker", args, {
7555
+ const result = (0, import_node_child_process12.spawnSync)("docker", args, {
6894
7556
  encoding: "utf-8",
6895
7557
  cwd: repoPath
6896
7558
  });
@@ -6916,7 +7578,7 @@ function runDockerComposeStop(repoPath, composeFiles2, overlayPath, services) {
6916
7578
  fileArgs.push("-f", resolved);
6917
7579
  }
6918
7580
  const args = ["compose", ...fileArgs, "-f", overlayPath, "stop", ...services];
6919
- const result = (0, import_node_child_process10.spawnSync)("docker", args, {
7581
+ const result = (0, import_node_child_process12.spawnSync)("docker", args, {
6920
7582
  encoding: "utf-8",
6921
7583
  cwd: repoPath
6922
7584
  });
@@ -6942,7 +7604,7 @@ function runDockerComposeLogs(repoPath, composeFiles2, overlayPath, services, ta
6942
7604
  String(tail),
6943
7605
  ...services
6944
7606
  ];
6945
- (0, import_node_child_process10.spawnSync)("docker", args, {
7607
+ (0, import_node_child_process12.spawnSync)("docker", args, {
6946
7608
  stdio: "inherit",
6947
7609
  cwd: repoPath
6948
7610
  });
@@ -6954,7 +7616,7 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
6954
7616
  fileArgs.push("-f", resolved);
6955
7617
  }
6956
7618
  const args = ["compose", ...fileArgs, "-f", overlayPath, "port", service, String(internalPort)];
6957
- const result = (0, import_node_child_process10.spawnSync)("docker", args, {
7619
+ const result = (0, import_node_child_process12.spawnSync)("docker", args, {
6958
7620
  encoding: "utf-8",
6959
7621
  cwd: repoPath
6960
7622
  });
@@ -6968,14 +7630,14 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
6968
7630
  const port = Number(match[1]);
6969
7631
  return Number.isInteger(port) && port > 0 ? port : void 0;
6970
7632
  }
6971
- var import_node_child_process10, import_node_crypto3, import_node_fs17, import_node_path14, import_yaml6;
7633
+ var import_node_child_process12, import_node_crypto4, import_node_fs18, import_node_path16, import_yaml6;
6972
7634
  var init_docker_run = __esm({
6973
7635
  "src/core/docker-run.ts"() {
6974
7636
  "use strict";
6975
- import_node_child_process10 = require("child_process");
6976
- import_node_crypto3 = require("crypto");
6977
- import_node_fs17 = __toESM(require("fs"));
6978
- import_node_path14 = __toESM(require("path"));
7637
+ import_node_child_process12 = require("child_process");
7638
+ import_node_crypto4 = require("crypto");
7639
+ import_node_fs18 = __toESM(require("fs"));
7640
+ import_node_path16 = __toESM(require("path"));
6979
7641
  import_yaml6 = __toESM(require("yaml"));
6980
7642
  init_docker_error_guidance();
6981
7643
  init_paths();
@@ -7065,7 +7727,7 @@ function isProcessRunning(pid) {
7065
7727
  }
7066
7728
  }
7067
7729
  function readProcessTree(rootPid) {
7068
- const result = (0, import_node_child_process11.spawnSync)("ps", ["-ax", "-o", "pid=,ppid="], { encoding: "utf-8" });
7730
+ const result = (0, import_node_child_process13.spawnSync)("ps", ["-ax", "-o", "pid=,ppid="], { encoding: "utf-8" });
7069
7731
  if (result.status !== 0) {
7070
7732
  return [rootPid];
7071
7733
  }
@@ -7176,12 +7838,12 @@ function detectListeningPorts(pids) {
7176
7838
  if (pids.length === 0) {
7177
7839
  return [];
7178
7840
  }
7179
- const result = (0, import_node_child_process11.spawnSync)("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", pids.join(",")], {
7841
+ const result = (0, import_node_child_process13.spawnSync)("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", pids.join(",")], {
7180
7842
  encoding: "utf-8"
7181
7843
  });
7182
7844
  if (result.error && result.error.code === "ENOENT") {
7183
7845
  if (process.platform === "linux") {
7184
- const ssResult = (0, import_node_child_process11.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
7846
+ const ssResult = (0, import_node_child_process13.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
7185
7847
  if (ssResult.status === 0 && ssResult.stdout) {
7186
7848
  return parseSsListeningPorts(ssResult.stdout, new Set(pids));
7187
7849
  }
@@ -7240,7 +7902,7 @@ async function runHostApp(repoPath, app, extraEnv = {}, secretManager, env, work
7240
7902
  app.hostRun.command,
7241
7903
  true
7242
7904
  ) : app.hostRun.command;
7243
- const child = (0, import_node_child_process11.spawn)(spawnCommand, {
7905
+ const child = (0, import_node_child_process13.spawn)(spawnCommand, {
7244
7906
  cwd: commandCwd,
7245
7907
  stdio: "inherit",
7246
7908
  shell: true,
@@ -7623,7 +8285,7 @@ async function execWithAppEnv(options) {
7623
8285
  options.command[0],
7624
8286
  true
7625
8287
  );
7626
- child = (0, import_node_child_process11.spawn)(wrapped, {
8288
+ child = (0, import_node_child_process13.spawn)(wrapped, {
7627
8289
  cwd: deps.repoPath,
7628
8290
  stdio: "inherit",
7629
8291
  shell: true,
@@ -7637,7 +8299,7 @@ async function execWithAppEnv(options) {
7637
8299
  false
7638
8300
  );
7639
8301
  const [cmd, ...wrappedArgs] = wrapped;
7640
- child = (0, import_node_child_process11.spawn)(cmd, wrappedArgs, {
8302
+ child = (0, import_node_child_process13.spawn)(cmd, wrappedArgs, {
7641
8303
  cwd: deps.repoPath,
7642
8304
  stdio: "inherit",
7643
8305
  shell: false,
@@ -7645,7 +8307,7 @@ async function execWithAppEnv(options) {
7645
8307
  });
7646
8308
  }
7647
8309
  } else if (options.shell) {
7648
- child = (0, import_node_child_process11.spawn)(options.command[0], {
8310
+ child = (0, import_node_child_process13.spawn)(options.command[0], {
7649
8311
  cwd: deps.repoPath,
7650
8312
  stdio: "inherit",
7651
8313
  shell: true,
@@ -7653,7 +8315,7 @@ async function execWithAppEnv(options) {
7653
8315
  });
7654
8316
  } else {
7655
8317
  const [command, ...args] = options.command;
7656
- child = (0, import_node_child_process11.spawn)(command, args, {
8318
+ child = (0, import_node_child_process13.spawn)(command, args, {
7657
8319
  cwd: deps.repoPath,
7658
8320
  stdio: "inherit",
7659
8321
  shell: false,
@@ -7674,11 +8336,11 @@ async function execWithAppEnv(options) {
7674
8336
  deps.stopDeps();
7675
8337
  }
7676
8338
  }
7677
- var import_node_child_process11, import_node_net, import_node_process, import_promises, POLL_INTERVAL_MS, DEFAULT_PORT_TIMEOUT_MS, PROCESS_TERMINATION_GRACE_MS;
8339
+ var import_node_child_process13, import_node_net, import_node_process, import_promises, POLL_INTERVAL_MS, DEFAULT_PORT_TIMEOUT_MS, PROCESS_TERMINATION_GRACE_MS;
7678
8340
  var init_app_run = __esm({
7679
8341
  "src/core/app-run.ts"() {
7680
8342
  "use strict";
7681
- import_node_child_process11 = require("child_process");
8343
+ import_node_child_process13 = require("child_process");
7682
8344
  import_node_net = __toESM(require("net"));
7683
8345
  import_node_process = require("process");
7684
8346
  import_promises = require("readline/promises");
@@ -7808,47 +8470,13 @@ var init_tls2 = __esm({
7808
8470
  });
7809
8471
 
7810
8472
  // src/core/workspace-ensure.ts
7811
- function selectDevpodWorkspace(workspaces, repoPath) {
7812
- const matches = workspaces.filter(
7813
- (workspace) => sameWorkspacePath(workspace.source.localFolder, repoPath)
7814
- );
7815
- if (matches.length > 1) {
7816
- throw new Error(
7817
- `Multiple DevPod workspaces reference '${repoPath}': ${matches.map((match) => match.id).join(", ")}`
7818
- );
7819
- }
7820
- return matches[0];
7821
- }
7822
- function listDevpodWorkspaces() {
7823
- const result = (0, import_node_child_process12.spawnSync)("devpod", ["list", "--output", "json"], { encoding: "utf-8" });
7824
- if (result.status !== 0) {
7825
- const details = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
7826
- throw new Error(`devpod list failed: ${details || "devpod is not installed or unavailable"}`);
7827
- }
7828
- let parsed;
7829
- try {
7830
- parsed = JSON.parse(result.stdout);
7831
- } catch {
7832
- throw new Error("devpod list returned invalid JSON.");
7833
- }
7834
- if (!Array.isArray(parsed)) {
7835
- throw new Error("devpod list returned an unexpected response.");
7836
- }
7837
- return parsed.map((entry) => {
7838
- const candidate = entry;
7839
- if (typeof candidate.id !== "string" || !candidate.source || typeof candidate.source.localFolder !== "string") {
7840
- throw new Error("devpod list returned a workspace without id/source.localFolder.");
7841
- }
7842
- return candidate;
7843
- });
7844
- }
7845
8473
  function assertOverlay(container, repoPath) {
7846
8474
  const workingDir = container.labels["com.docker.compose.project.working_dir"];
7847
- if (!workingDir || !sameWorkspacePath(workingDir, import_node_path15.default.join(repoPath, ".devcontainer"))) {
8475
+ if (!workingDir || !sameWorkspacePath(workingDir, import_node_path17.default.join(repoPath, ".devcontainer"))) {
7848
8476
  throw new Error(`Container '${container.id}' does not belong to the exact worktree.`);
7849
8477
  }
7850
8478
  const configFiles = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").filter(Boolean);
7851
- const expectedOverlay = import_node_path15.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
8479
+ const expectedOverlay = import_node_path17.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
7852
8480
  if (!configFiles.some((configFile) => sameWorkspacePath(configFile, expectedOverlay))) {
7853
8481
  throw new Error(`Container '${container.id}' was not started with ${DEVCONTAINER_OVERLAY}.`);
7854
8482
  }
@@ -7866,7 +8494,7 @@ function validateWorkspaceContainers(containers, options) {
7866
8494
  const owned = containers.filter((container) => {
7867
8495
  const workingDir = container.labels["com.docker.compose.project.working_dir"];
7868
8496
  return Boolean(
7869
- workingDir && sameWorkspacePath(workingDir, import_node_path15.default.join(options.repoPath, ".devcontainer"))
8497
+ workingDir && sameWorkspacePath(workingDir, import_node_path17.default.join(options.repoPath, ".devcontainer"))
7870
8498
  );
7871
8499
  });
7872
8500
  const appContainers = owned.filter(
@@ -7914,7 +8542,7 @@ function validateWorkspaceContainers(containers, options) {
7914
8542
  return { id: appContainer.id, workspacePath: repoMount.Destination };
7915
8543
  }
7916
8544
  function inspectWorkspaceContainers() {
7917
- const listed = (0, import_node_child_process12.spawnSync)("docker", ["ps", "-a", "--format", "{{.ID}}"], {
8545
+ const listed = (0, import_node_child_process14.spawnSync)("docker", ["ps", "-a", "--format", "{{.ID}}"], {
7918
8546
  encoding: "utf-8"
7919
8547
  });
7920
8548
  if (listed.status !== 0) {
@@ -7926,7 +8554,7 @@ function inspectWorkspaceContainers() {
7926
8554
  if (ids.length === 0) {
7927
8555
  return [];
7928
8556
  }
7929
- const inspected = (0, import_node_child_process12.spawnSync)("docker", ["inspect", "--format", SAFE_INSPECT_TEMPLATE, ...ids], {
8557
+ const inspected = (0, import_node_child_process14.spawnSync)("docker", ["inspect", "--format", SAFE_INSPECT_TEMPLATE, ...ids], {
7930
8558
  encoding: "utf-8"
7931
8559
  });
7932
8560
  if (inspected.status !== 0) {
@@ -7939,18 +8567,18 @@ function inspectWorkspaceContainers() {
7939
8567
  function sleep2(ms) {
7940
8568
  return new Promise((resolve) => setTimeout(resolve, ms));
7941
8569
  }
7942
- async function waitForContainerPreflight(repoPath, gitCommonDir2, workspace, upstreamHosts, timeoutMs) {
8570
+ async function waitForContainerPreflight(repoPath, gitCommonDir, workspace, upstreamHosts, timeoutMs) {
7943
8571
  const deadline = Date.now() + timeoutMs;
7944
8572
  let lastError;
7945
8573
  do {
7946
8574
  try {
7947
8575
  const appContainer = validateWorkspaceContainers(inspectWorkspaceContainers(), {
7948
8576
  repoPath,
7949
- gitCommonDir: gitCommonDir2,
8577
+ gitCommonDir,
7950
8578
  workspace,
7951
8579
  upstreamHosts
7952
8580
  });
7953
- const workspaceEnv = (0, import_node_child_process12.spawnSync)("docker", ["exec", appContainer.id, "printenv", "WORKSPACE"], {
8581
+ const workspaceEnv = (0, import_node_child_process14.spawnSync)("docker", ["exec", appContainer.id, "printenv", "WORKSPACE"], {
7954
8582
  encoding: "utf-8"
7955
8583
  });
7956
8584
  if (workspaceEnv.status !== 0 || workspaceEnv.stdout.trim() !== workspace) {
@@ -7958,7 +8586,7 @@ async function waitForContainerPreflight(repoPath, gitCommonDir2, workspace, ups
7958
8586
  `Workspace app container must expose WORKSPACE='${workspace}' (got '${workspaceEnv.stdout.trim() || "(empty)"}').`
7959
8587
  );
7960
8588
  }
7961
- const devrouterWorkspaceEnv = (0, import_node_child_process12.spawnSync)(
8589
+ const devrouterWorkspaceEnv = (0, import_node_child_process14.spawnSync)(
7962
8590
  "docker",
7963
8591
  ["exec", appContainer.id, "printenv", "DEVROUTER_WORKSPACE"],
7964
8592
  { encoding: "utf-8" }
@@ -7966,7 +8594,7 @@ async function waitForContainerPreflight(repoPath, gitCommonDir2, workspace, ups
7966
8594
  if (devrouterWorkspaceEnv.status !== 0 || devrouterWorkspaceEnv.stdout.trim() !== workspace) {
7967
8595
  throw new Error(`Workspace app container must expose DEVROUTER_WORKSPACE='${workspace}'.`);
7968
8596
  }
7969
- const gitCheck = (0, import_node_child_process12.spawnSync)(
8597
+ const gitCheck = (0, import_node_child_process14.spawnSync)(
7970
8598
  "docker",
7971
8599
  [
7972
8600
  "exec",
@@ -8032,7 +8660,7 @@ async function waitForHttpRoutes(apps, timeoutMs) {
8032
8660
  const deadline = Date.now() + timeoutMs;
8033
8661
  do {
8034
8662
  for (const [name, app] of pending) {
8035
- const result = (0, import_node_child_process12.spawnSync)(
8663
+ const result = (0, import_node_child_process14.spawnSync)(
8036
8664
  "curl",
8037
8665
  [
8038
8666
  "-k",
@@ -8091,17 +8719,6 @@ function resolveIdentity(repoPath) {
8091
8719
  hadExactDevpod: Boolean(existingDevpod)
8092
8720
  };
8093
8721
  }
8094
- function gitCommonDir(repoPath) {
8095
- const result = (0, import_node_child_process12.spawnSync)(
8096
- "git",
8097
- ["-C", repoPath, "rev-parse", "--path-format=absolute", "--git-common-dir"],
8098
- { encoding: "utf-8" }
8099
- );
8100
- if (result.status !== 0 || !result.stdout.trim()) {
8101
- throw new Error(`Could not resolve the Git common directory for '${repoPath}'.`);
8102
- }
8103
- return comparableWorkspacePath(result.stdout.trim());
8104
- }
8105
8722
  function startDevpod(repoPath, workspace, commonDir, recreate = false) {
8106
8723
  const args = [
8107
8724
  "up",
@@ -8117,7 +8734,7 @@ function startDevpod(repoPath, workspace, commonDir, recreate = false) {
8117
8734
  if (recreate) {
8118
8735
  args.push("--recreate");
8119
8736
  }
8120
- const result = (0, import_node_child_process12.spawnSync)("devpod", args, {
8737
+ const result = (0, import_node_child_process14.spawnSync)("devpod", args, {
8121
8738
  stdio: "inherit",
8122
8739
  env: {
8123
8740
  ...process.env,
@@ -8139,7 +8756,7 @@ function assertDevpodAttachment(repoPath, workspace) {
8139
8756
  }
8140
8757
  function openUrls(urls) {
8141
8758
  for (const url of urls) {
8142
- const opened = (0, import_node_child_process12.spawnSync)("open", [url], { encoding: "utf-8" });
8759
+ const opened = (0, import_node_child_process14.spawnSync)("open", [url], { encoding: "utf-8" });
8143
8760
  if (opened.status !== 0) {
8144
8761
  process.stderr.write(`Warning: could not open '${url}'.
8145
8762
  `);
@@ -8151,12 +8768,19 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
8151
8768
  if (!isLinkedWorktree(repoPath)) {
8152
8769
  throw new Error(`workspace ensure requires a linked Git worktree (got '${repoPath}').`);
8153
8770
  }
8771
+ const missingOwners = listMissingWorkspaceOwnership(repoPath);
8772
+ if (missingOwners.length > 0) {
8773
+ process.stderr.write(
8774
+ `Warning: ${missingOwners.length} managed workspace owner${missingOwners.length === 1 ? " is" : "s are"} missing. Review: dev workspace gc --repo ${repoPath}
8775
+ `
8776
+ );
8777
+ }
8154
8778
  return withWorkspaceLifecycleLock(repoPath, async () => {
8155
8779
  let environmentStarted = false;
8156
8780
  try {
8157
8781
  const { workspace, hadExactDevpod } = resolveIdentity(repoPath);
8158
- const overlayPath = import_node_path15.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
8159
- if (!import_node_fs18.default.existsSync(overlayPath)) {
8782
+ const overlayPath = import_node_path17.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
8783
+ if (!import_node_fs19.default.existsSync(overlayPath)) {
8160
8784
  throw new Error(`Missing required DevPod compose overlay: ${overlayPath}`);
8161
8785
  }
8162
8786
  const runtime = loadRuntimeConfig(repoPath, workspace);
@@ -8169,12 +8793,20 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
8169
8793
  );
8170
8794
  }
8171
8795
  }
8172
- const commonDir = gitCommonDir(repoPath);
8796
+ const commonDir = resolveGitCommonDir(repoPath);
8173
8797
  const upstreamHosts = parsedUpstreams.map((upstream) => upstream.host);
8798
+ const ownership = {
8799
+ workspace,
8800
+ worktreePath: repoPath,
8801
+ branch: currentBranch(repoPath),
8802
+ devpodId: workspace
8803
+ };
8804
+ writeWorkspaceOwnership(repoPath, ownership);
8174
8805
  const startAndProveAttachment = (recreate = false) => {
8175
8806
  startDevpod(repoPath, workspace, commonDir, recreate);
8176
8807
  environmentStarted = true;
8177
8808
  assertDevpodAttachment(repoPath, workspace);
8809
+ writeWorkspaceOwnership(repoPath, ownership);
8178
8810
  };
8179
8811
  const recreateAndWait = async () => {
8180
8812
  startAndProveAttachment(true);
@@ -8252,19 +8884,21 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
8252
8884
  }
8253
8885
  });
8254
8886
  }
8255
- var import_node_child_process12, import_node_fs18, import_node_path15, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS2, SAFE_INSPECT_TEMPLATE;
8887
+ var import_node_child_process14, import_node_fs19, import_node_path17, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS2, SAFE_INSPECT_TEMPLATE;
8256
8888
  var init_workspace_ensure = __esm({
8257
8889
  "src/core/workspace-ensure.ts"() {
8258
8890
  "use strict";
8259
- import_node_child_process12 = require("child_process");
8260
- import_node_fs18 = __toESM(require("fs"));
8261
- import_node_path15 = __toESM(require("path"));
8891
+ import_node_child_process14 = require("child_process");
8892
+ import_node_fs19 = __toESM(require("fs"));
8893
+ import_node_path17 = __toESM(require("path"));
8894
+ init_devpod_workspaces();
8262
8895
  init_docker();
8263
8896
  init_host_routes();
8264
8897
  init_repo_config();
8265
8898
  init_router();
8266
8899
  init_tls();
8267
8900
  init_workspace();
8901
+ init_workspace_ownership();
8268
8902
  DEVCONTAINER_OVERLAY = "docker-compose.devrouter.yml";
8269
8903
  DEFAULT_READINESS_TIMEOUT_MS = 12e4;
8270
8904
  POLL_INTERVAL_MS2 = 1e3;
@@ -8273,49 +8907,29 @@ var init_workspace_ensure = __esm({
8273
8907
  });
8274
8908
 
8275
8909
  // src/core/workspace-lifecycle.ts
8276
- function hasDevpod() {
8277
- const result = (0, import_node_child_process13.spawnSync)("devpod", ["version"], { encoding: "utf-8" });
8278
- return result.status === 0;
8279
- }
8280
- function listGitWorktrees(repoPath) {
8281
- const result = (0, import_node_child_process13.spawnSync)("git", ["-C", repoPath, "worktree", "list", "--porcelain"], {
8282
- encoding: "utf-8"
8283
- });
8284
- if (result.status !== 0) {
8285
- return [];
8286
- }
8287
- const worktrees = [];
8288
- let current = {};
8289
- for (const line of result.stdout.split("\n")) {
8290
- if (line.startsWith("worktree ")) {
8291
- current = { path: line.slice("worktree ".length).trim() };
8292
- } else if (line.startsWith("branch ")) {
8293
- current.branch = line.slice("branch ".length).trim().replace(/^refs\/heads\//, "");
8294
- } else if (line.trim() === "" && current.path) {
8295
- worktrees.push({ path: current.path, branch: current.branch });
8296
- current = {};
8297
- }
8298
- }
8299
- if (current.path) {
8300
- worktrees.push({ path: current.path, branch: current.branch });
8301
- }
8302
- return worktrees;
8910
+ function warnMissingWorkspaceOwnership(repoPath) {
8911
+ const missing = listMissingWorkspaceOwnership(repoPath);
8912
+ if (missing.length === 0) return;
8913
+ process.stderr.write(
8914
+ `Warning: ${missing.length} managed workspace owner${missing.length === 1 ? " is" : "s are"} missing. Review: dev workspace gc --repo ${repoPath}
8915
+ `
8916
+ );
8303
8917
  }
8304
8918
  function defaultWorktreePath(mainRepo, ws) {
8305
- return import_node_path16.default.join(mainRepo, "trees", ws);
8919
+ return import_node_path18.default.join(mainRepo, "trees", ws);
8306
8920
  }
8307
8921
  function assertDefaultWorktreeRootIgnored(mainRepo) {
8308
- const ignored = (0, import_node_child_process13.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
8922
+ const ignored = (0, import_node_child_process15.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
8309
8923
  encoding: "utf-8"
8310
8924
  });
8311
8925
  if (ignored.status !== 0) {
8312
8926
  throw new Error(
8313
- `Default worktree root '${import_node_path16.default.join(mainRepo, "trees")}' is not ignored. Add 'trees/' to '${import_node_path16.default.join(mainRepo, ".gitignore")}' or use --path.`
8927
+ `Default worktree root '${import_node_path18.default.join(mainRepo, "trees")}' is not ignored. Add 'trees/' to '${import_node_path18.default.join(mainRepo, ".gitignore")}' or use --path.`
8314
8928
  );
8315
8929
  }
8316
8930
  }
8317
8931
  function legacyDefaultWorktreePath(mainRepo, ws) {
8318
- return import_node_path16.default.join(import_node_path16.default.dirname(mainRepo), `${import_node_path16.default.basename(mainRepo)}-${ws}`);
8932
+ return import_node_path18.default.join(import_node_path18.default.dirname(mainRepo), `${import_node_path18.default.basename(mainRepo)}-${ws}`);
8319
8933
  }
8320
8934
  function teardownFallbackPath(mainRepo, workspace) {
8321
8935
  const candidates = [
@@ -8344,14 +8958,119 @@ function oneWorkspaceMatch(target, matches) {
8344
8958
  }
8345
8959
  return matches[0];
8346
8960
  }
8961
+ function oneRecordMatch(target, matches) {
8962
+ if (matches.length > 1) {
8963
+ throw new Error(
8964
+ `Workspace target '${target}' is ambiguous: ${matches.map((match) => match.worktreePath).join(", ")}`
8965
+ );
8966
+ }
8967
+ return matches[0];
8968
+ }
8969
+ function resolveWorkspaceTarget(mainRepo, target, worktrees, records) {
8970
+ const requestedWorkspace = wsFromBranch(target);
8971
+ if (!requestedWorkspace) {
8972
+ throw new Error(`'${target}' does not yield a valid workspace token.`);
8973
+ }
8974
+ const liveBranchWorktree = oneWorkspaceMatch(
8975
+ target,
8976
+ worktrees.filter((worktree2) => worktree2.workspace !== void 0 && worktree2.branch === target)
8977
+ );
8978
+ if (liveBranchWorktree) {
8979
+ const liveWorkspace = liveBranchWorktree.workspace;
8980
+ if (!liveWorkspace) {
8981
+ throw new Error(`Live branch '${target}' is not an isolated workspace.`);
8982
+ }
8983
+ const liveRecord = oneRecordMatch(
8984
+ target,
8985
+ records.filter((record2) => sameWorkspacePath(record2.worktreePath, liveBranchWorktree.path))
8986
+ );
8987
+ return {
8988
+ workspace: liveRecord?.workspace ?? liveWorkspace,
8989
+ worktreePath: liveBranchWorktree.path,
8990
+ worktree: liveBranchWorktree,
8991
+ record: liveRecord
8992
+ };
8993
+ }
8994
+ const record = records.find((candidate) => candidate.workspace === requestedWorkspace);
8995
+ if (record) {
8996
+ return {
8997
+ workspace: record.workspace,
8998
+ worktreePath: record.worktreePath,
8999
+ worktree: worktreeForRecord(worktrees, record),
9000
+ record
9001
+ };
9002
+ }
9003
+ const linked = worktrees.filter((worktree2) => worktree2.workspace !== void 0);
9004
+ const worktree = oneWorkspaceMatch(
9005
+ target,
9006
+ linked.filter((candidate) => candidate.workspace === requestedWorkspace)
9007
+ ) ?? oneWorkspaceMatch(
9008
+ target,
9009
+ linked.filter(
9010
+ (candidate) => candidate.branch !== void 0 && wsFromBranch(candidate.branch) === requestedWorkspace
9011
+ )
9012
+ );
9013
+ const workspace = worktree?.workspace ?? requestedWorkspace;
9014
+ return {
9015
+ workspace,
9016
+ worktreePath: worktree?.path ?? teardownFallbackPath(mainRepo, workspace),
9017
+ worktree,
9018
+ record: void 0
9019
+ };
9020
+ }
9021
+ function worktreeForRecord(worktrees, record) {
9022
+ return worktrees.find((candidate) => sameWorkspacePath(candidate.path, record.worktreePath));
9023
+ }
9024
+ function devpodForTarget(target, worktrees, devpods) {
9025
+ if (target.record) {
9026
+ const status = inspectWorkspaceOwnership(target.record, worktrees, devpods);
9027
+ if (status.ownerStatus === "conflict") {
9028
+ throw new Error(
9029
+ `Workspace '${target.workspace}' ownership conflicts with live Git or DevPod evidence; no resources were changed.`
9030
+ );
9031
+ }
9032
+ return status.devpodStatus === "owned" ? devpods.find((devpod) => devpod.id === target.record?.devpodId) : void 0;
9033
+ }
9034
+ const devpodId = target.workspace;
9035
+ const ownership = inspectDevpodWorkspaceOwnership(devpods, devpodId, target.worktreePath);
9036
+ if (ownership.status === "conflict") {
9037
+ throw new Error(ownership.reason);
9038
+ }
9039
+ return ownership.status === "owned" ? ownership.workspace : void 0;
9040
+ }
9041
+ function assertFullDownPreflight(mainRepo, target) {
9042
+ if (sameWorkspacePath(target.worktreePath, mainRepo)) {
9043
+ throw new Error("Refusing to remove the primary Git checkout.");
9044
+ }
9045
+ if (!target.worktree || target.worktree.prunable || !import_node_fs20.default.existsSync(target.worktreePath)) return;
9046
+ if (target.worktree.locked) {
9047
+ throw new Error(
9048
+ `Worktree '${target.worktreePath}' is locked; unlock it before workspace down.`
9049
+ );
9050
+ }
9051
+ const status = (0, import_node_child_process15.spawnSync)(
9052
+ "git",
9053
+ ["-C", target.worktreePath, "status", "--porcelain", "--untracked-files=normal"],
9054
+ { encoding: "utf-8" }
9055
+ );
9056
+ if (status.status !== 0) {
9057
+ const detail = [status.error?.message, status.stderr].filter(Boolean).join("\n").trim();
9058
+ throw new Error(`git status failed for '${target.worktreePath}': ${detail || "unknown error"}`);
9059
+ }
9060
+ if (status.stdout.trim()) {
9061
+ throw new Error(
9062
+ `Worktree '${target.worktreePath}' has uncommitted changes; use --keep-worktree or clean it before workspace down.`
9063
+ );
9064
+ }
9065
+ }
8347
9066
  async function workspaceUp(branch, opts = {}) {
8348
9067
  const mainRepo = resolveRepoPath(opts.repoPath);
8349
9068
  const ws = wsFromBranch(branch);
8350
9069
  if (!ws) {
8351
9070
  throw new Error(`Branch '${branch}' does not yield a valid workspace token.`);
8352
9071
  }
8353
- const worktreePath = opts.path ? import_node_path16.default.resolve(opts.path) : defaultWorktreePath(mainRepo, ws);
8354
- if (import_node_fs19.default.existsSync(worktreePath)) {
9072
+ const worktreePath = opts.path ? import_node_path18.default.resolve(opts.path) : defaultWorktreePath(mainRepo, ws);
9073
+ if (import_node_fs20.default.existsSync(worktreePath)) {
8355
9074
  const registered = listGitWorktrees(mainRepo).find(
8356
9075
  (worktree) => sameWorkspacePath(worktree.path, worktreePath)
8357
9076
  );
@@ -8369,11 +9088,11 @@ async function workspaceUp(branch, opts = {}) {
8369
9088
  if (!opts.path) {
8370
9089
  assertDefaultWorktreeRootIgnored(mainRepo);
8371
9090
  }
8372
- const add = (0, import_node_child_process13.spawnSync)("git", ["-C", mainRepo, "worktree", "add", worktreePath, branch], {
9091
+ const add = (0, import_node_child_process15.spawnSync)("git", ["-C", mainRepo, "worktree", "add", worktreePath, branch], {
8373
9092
  encoding: "utf-8"
8374
9093
  });
8375
9094
  if (add.status !== 0) {
8376
- const addNew = (0, import_node_child_process13.spawnSync)(
9095
+ const addNew = (0, import_node_child_process15.spawnSync)(
8377
9096
  "git",
8378
9097
  ["-C", mainRepo, "worktree", "add", "-b", branch, worktreePath],
8379
9098
  {
@@ -8389,6 +9108,7 @@ async function workspaceUp(branch, opts = {}) {
8389
9108
  `);
8390
9109
  }
8391
9110
  if (opts.noDevpod) {
9111
+ warnMissingWorkspaceOwnership(mainRepo);
8392
9112
  process.stdout.write("Skipped environment startup; no routes were changed.\n");
8393
9113
  return;
8394
9114
  }
@@ -8405,79 +9125,104 @@ ${ensured.urls.map((url) => ` ${url}`).join("\n")}
8405
9125
  function workspaceLs(repoPath) {
8406
9126
  const mainRepo = resolveRepoPath(repoPath);
8407
9127
  const worktrees = identifyGitWorktrees(mainRepo);
8408
- const routesByWorktreePath = listRoutesForWorktreePaths(
8409
- worktrees.map((worktree) => worktree.path)
9128
+ const records = listWorkspaceOwnership(mainRepo);
9129
+ let devpods;
9130
+ try {
9131
+ devpods = listDevpodWorkspaces();
9132
+ } catch {
9133
+ devpods = void 0;
9134
+ }
9135
+ const paths = Array.from(
9136
+ /* @__PURE__ */ new Set([
9137
+ ...worktrees.map((worktree) => worktree.path),
9138
+ ...records.map((record) => record.worktreePath)
9139
+ ])
8410
9140
  );
8411
- return worktrees.map((wt) => {
8412
- const wsRoutes = routesByWorktreePath.get(wt.path) ?? [];
9141
+ const routesByWorktreePath = listRoutesForWorktreePaths(paths);
9142
+ const worktreesByPath = new Map(worktrees.map((worktree) => [worktree.path, worktree]));
9143
+ const recordsByPath = new Map(records.map((record) => [record.worktreePath, record]));
9144
+ return paths.map((worktreePath) => {
9145
+ const worktree = worktreesByPath.get(worktreePath);
9146
+ const record = recordsByPath.get(worktreePath);
9147
+ const status = record ? inspectWorkspaceOwnership(record, worktrees, devpods) : void 0;
9148
+ const matchingDevpods = devpods?.filter(
9149
+ (devpod) => sameWorkspacePath(devpod.source.localFolder, worktreePath)
9150
+ );
9151
+ const wsRoutes = routesByWorktreePath.get(worktreePath) ?? [];
8413
9152
  return {
8414
- workspace: wt.workspace,
8415
- branch: wt.branch,
8416
- worktreePath: wt.path,
9153
+ workspace: record?.workspace ?? worktree?.workspace,
9154
+ branch: worktree?.branch ?? record?.branch ?? void 0,
9155
+ worktreePath,
9156
+ legacy: !record && worktree?.workspace !== void 0,
9157
+ ownerStatus: status?.ownerStatus,
9158
+ devpodStatus: status?.devpodStatus ?? (devpods === void 0 ? "unknown" : matchingDevpods?.length === 0 ? "absent" : matchingDevpods?.length === 1 ? "owned" : "conflict"),
8417
9159
  routeCount: wsRoutes.length,
8418
9160
  hosts: wsRoutes.map((route) => route.host)
8419
9161
  };
8420
9162
  });
8421
9163
  }
8422
- async function workspaceDown(target, opts = {}) {
8423
- const requestedWorkspace = wsFromBranch(target);
8424
- if (!requestedWorkspace) {
8425
- throw new Error(`'${target}' does not yield a valid workspace token.`);
8426
- }
9164
+ async function runWorkspaceLifecycle(action2, target, opts = {}) {
8427
9165
  const mainRepo = resolveRepoPath(opts.repoPath);
8428
- const worktrees = identifyGitWorktrees(mainRepo).filter(
8429
- (worktree) => worktree.workspace !== void 0
8430
- );
8431
- const exactBranchMatch = worktrees.find((worktree) => worktree.branch === target);
8432
- const match = exactBranchMatch ?? oneWorkspaceMatch(
8433
- target,
8434
- worktrees.filter((worktree) => worktree.workspace === requestedWorkspace)
8435
- ) ?? oneWorkspaceMatch(
8436
- target,
8437
- worktrees.filter(
8438
- (worktree) => worktree.branch !== void 0 && wsFromBranch(worktree.branch) === requestedWorkspace
8439
- )
8440
- );
8441
- const workspace = match?.workspace ?? requestedWorkspace;
8442
- const worktreePath = match?.path ?? teardownFallbackPath(mainRepo, workspace);
8443
- const teardown = async () => {
8444
- const routes = removeWorkspaceRoutesForWorktree(workspace, worktreePath);
8445
- process.stdout.write(`Freed ${routes.length} route(s) for workspace '${workspace}'.
8446
- `);
8447
- if (!opts.keepDevpod && hasDevpod()) {
8448
- (0, import_node_child_process13.spawnSync)("devpod", ["stop", workspace], { stdio: "inherit" });
8449
- }
8450
- if (!opts.keepWorktree) {
8451
- if (import_node_fs19.default.existsSync(worktreePath) && worktreePath !== mainRepo) {
8452
- const rm = (0, import_node_child_process13.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", worktreePath], {
9166
+ const worktrees = identifyGitWorktrees(mainRepo);
9167
+ const records = listWorkspaceOwnership(mainRepo);
9168
+ const resolved = resolveWorkspaceTarget(mainRepo, target, worktrees, records);
9169
+ const operation = async () => {
9170
+ const removeWorktree = action2 === "down" && !opts.keepWorktree;
9171
+ const devpodAction = action2 === "stop" ? "stop" : "delete";
9172
+ if (removeWorktree) {
9173
+ assertFullDownPreflight(mainRepo, resolved);
9174
+ }
9175
+ const devpods = listDevpodWorkspaces();
9176
+ const devpod = devpodForTarget(resolved, worktrees, devpods);
9177
+ if (devpod) {
9178
+ runDevpodWorkspaceAction(devpodAction, devpod.id);
9179
+ }
9180
+ const routes = removeWorkspaceRoutesForWorktree(resolved.workspace, resolved.worktreePath);
9181
+ process.stdout.write(
9182
+ `Freed ${routes.length} route(s) for workspace '${resolved.workspace}'.
9183
+ `
9184
+ );
9185
+ if (removeWorktree) {
9186
+ if (resolved.worktree && !resolved.worktree.prunable && import_node_fs20.default.existsSync(resolved.worktreePath)) {
9187
+ const rm = (0, import_node_child_process15.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
8453
9188
  encoding: "utf-8"
8454
9189
  });
8455
- if (rm.status === 0) {
8456
- process.stdout.write(`Removed worktree ${worktreePath}.
8457
- `);
8458
- } else {
8459
- process.stderr.write(
8460
- `Warning: could not remove worktree ${worktreePath}: ${rm.stderr || "unknown error"}
8461
- `
9190
+ if (rm.status !== 0) {
9191
+ const detail = [rm.error?.message, rm.stderr].filter(Boolean).join("\n").trim();
9192
+ throw new Error(
9193
+ `git worktree remove failed for '${resolved.worktreePath}': ${detail || "unknown error"}`
8462
9194
  );
8463
9195
  }
9196
+ process.stdout.write(`Removed worktree ${resolved.worktreePath}.
9197
+ `);
9198
+ }
9199
+ if (resolved.record) {
9200
+ removeWorkspaceOwnership(mainRepo, resolved.workspace);
8464
9201
  }
8465
9202
  }
8466
- return { freedRoutes: routes.length, workspace };
9203
+ return { freedRoutes: routes.length, workspace: resolved.workspace };
8467
9204
  };
8468
- return match && import_node_fs19.default.existsSync(worktreePath) ? withWorkspaceLifecycleLock(worktreePath, teardown) : teardown();
9205
+ return resolved.worktree && !resolved.worktree.prunable && import_node_fs20.default.existsSync(resolved.worktreePath) ? withWorkspaceLifecycleLock(resolved.worktreePath, operation) : operation();
9206
+ }
9207
+ async function workspaceStop(target, opts = {}) {
9208
+ return runWorkspaceLifecycle("stop", target, opts);
9209
+ }
9210
+ async function workspaceDown(target, opts = {}) {
9211
+ return runWorkspaceLifecycle("down", target, opts);
8469
9212
  }
8470
- var import_node_child_process13, import_node_fs19, import_node_path16;
9213
+ var import_node_child_process15, import_node_fs20, import_node_path18;
8471
9214
  var init_workspace_lifecycle = __esm({
8472
9215
  "src/core/workspace-lifecycle.ts"() {
8473
9216
  "use strict";
8474
- import_node_child_process13 = require("child_process");
8475
- import_node_fs19 = __toESM(require("fs"));
8476
- import_node_path16 = __toESM(require("path"));
9217
+ import_node_child_process15 = require("child_process");
9218
+ import_node_fs20 = __toESM(require("fs"));
9219
+ import_node_path18 = __toESM(require("path"));
9220
+ init_devpod_workspaces();
8477
9221
  init_repo_config();
8478
9222
  init_route_state();
8479
9223
  init_workspace();
8480
9224
  init_workspace_ensure();
9225
+ init_workspace_ownership();
8481
9226
  }
8482
9227
  });
8483
9228
 
@@ -8486,19 +9231,34 @@ var workspace_exports = {};
8486
9231
  __export(workspace_exports, {
8487
9232
  runWorkspaceDownCommand: () => runWorkspaceDownCommand,
8488
9233
  runWorkspaceEnsureCommand: () => runWorkspaceEnsureCommand,
9234
+ runWorkspaceGcCommand: () => runWorkspaceGcCommand,
8489
9235
  runWorkspaceLsCommand: () => runWorkspaceLsCommand,
9236
+ runWorkspaceStopCommand: () => runWorkspaceStopCommand,
8490
9237
  runWorkspaceUpCommand: () => runWorkspaceUpCommand
8491
9238
  });
9239
+ function resolveGitWorkspaceRepo(repoPath) {
9240
+ const resolved = resolveRepoPath(repoPath);
9241
+ try {
9242
+ resolveGitCommonDir(resolved);
9243
+ } catch (error) {
9244
+ throw new Error(`Workspace commands require a Git repository: '${resolved}'.`, {
9245
+ cause: error
9246
+ });
9247
+ }
9248
+ return resolved;
9249
+ }
8492
9250
  async function runWorkspaceUpCommand(branch, options) {
9251
+ const repoPath = resolveGitWorkspaceRepo(options.repo);
8493
9252
  await workspaceUp(branch, {
8494
9253
  path: options.path,
8495
9254
  noDevpod: options.noDevpod,
8496
9255
  open: options.open,
8497
- repoPath: options.repo
9256
+ repoPath
8498
9257
  });
8499
9258
  }
8500
9259
  async function runWorkspaceEnsureCommand(options) {
8501
- const result = await workspaceEnsure(options.path, { open: options.open });
9260
+ const repoPath = resolveGitWorkspaceRepo(options.path);
9261
+ const result = await workspaceEnsure(repoPath, { open: options.open });
8502
9262
  process.stdout.write(
8503
9263
  `Workspace '${result.workspace}' is ready (${result.devpodId}).
8504
9264
  ${result.urls.map((url) => ` ${url}`).join("\n")}
@@ -8506,7 +9266,8 @@ ${result.urls.map((url) => ` ${url}`).join("\n")}
8506
9266
  );
8507
9267
  }
8508
9268
  function runWorkspaceLsCommand(options) {
8509
- const rows = workspaceLs(options.repo);
9269
+ const repoPath = resolveGitWorkspaceRepo(options.repo);
9270
+ const rows = workspaceLs(repoPath);
8510
9271
  if (options.json) {
8511
9272
  process.stdout.write(`${JSON.stringify(rows, null, 2)}
8512
9273
  `);
@@ -8518,24 +9279,56 @@ function runWorkspaceLsCommand(options) {
8518
9279
  }
8519
9280
  for (const row of rows) {
8520
9281
  const label = row.workspace ?? "(primary)";
9282
+ const ownership = row.ownerStatus ?? (row.legacy ? "legacy" : "unmanaged");
8521
9283
  process.stdout.write(
8522
- `${label} ${row.branch ?? "-"} ${row.routeCount} route(s) ${row.worktreePath}
9284
+ `${label} ${row.branch ?? "-"} ${ownership} devpod:${row.devpodStatus} ${row.routeCount} route(s) ${row.worktreePath}
8523
9285
  `
8524
9286
  );
8525
9287
  }
8526
9288
  }
8527
9289
  async function runWorkspaceDownCommand(target, options) {
9290
+ const repoPath = resolveGitWorkspaceRepo(options.repo);
8528
9291
  await workspaceDown(target, {
8529
9292
  keepWorktree: options.keepWorktree,
8530
- keepDevpod: options.keepDevpod,
8531
- repoPath: options.repo
9293
+ repoPath
8532
9294
  });
8533
9295
  }
9296
+ async function runWorkspaceStopCommand(target, options) {
9297
+ const repoPath = resolveGitWorkspaceRepo(options.repo);
9298
+ await workspaceStop(target, { repoPath });
9299
+ }
9300
+ function runWorkspaceGcCommand(options) {
9301
+ const repoPath = resolveGitWorkspaceRepo(options.repo);
9302
+ const plan = inspectWorkspaceGc(repoPath);
9303
+ const report = options.yes ? applyWorkspaceGc(plan) : plan;
9304
+ if (options.json) {
9305
+ process.stdout.write(`${JSON.stringify(report, null, 2)}
9306
+ `);
9307
+ } else {
9308
+ process.stdout.write(
9309
+ `Workspace GC ${report.mode}: ${report.summary.eligible} eligible, ${report.summary.blocked} blocked, ${report.summary.cleaned} cleaned, ${report.summary.errors} error(s).
9310
+ `
9311
+ );
9312
+ for (const candidate of report.candidates) {
9313
+ process.stdout.write(
9314
+ `${candidate.workspace} ${candidate.kind} ${candidate.ownerStatus ?? "legacy"} ${candidate.eligible ? "eligible" : "blocked"} ${candidate.worktreePath}
9315
+ `
9316
+ );
9317
+ }
9318
+ if (!options.yes && report.summary.eligible > 0) {
9319
+ process.stdout.write("Dry run only. Re-run with --yes to apply eligible cleanup.\n");
9320
+ }
9321
+ }
9322
+ if (report.summary.errors > 0) process.exitCode = 1;
9323
+ }
8534
9324
  var init_workspace2 = __esm({
8535
9325
  "src/commands/workspace.ts"() {
8536
9326
  "use strict";
9327
+ init_repo_config();
8537
9328
  init_workspace_ensure();
9329
+ init_workspace_gc();
8538
9330
  init_workspace_lifecycle();
9331
+ init_workspace_ownership();
8539
9332
  }
8540
9333
  });
8541
9334
 
@@ -8577,7 +9370,7 @@ var init_version = __esm({
8577
9370
 
8578
9371
  // src/cli.ts
8579
9372
  var import_commander = require("commander");
8580
- var CLI_VERSION = true ? "0.0.30" : "0.0.0-dev";
9373
+ var CLI_VERSION = true ? "0.0.31" : "0.0.0-dev";
8581
9374
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
8582
9375
  function withErrorHandling(action2) {
8583
9376
  return async (...args) => {
@@ -8817,7 +9610,21 @@ workspaceCommand.command("ls").description("List git worktrees with their worksp
8817
9610
  runWorkspaceLsCommand2(options);
8818
9611
  })
8819
9612
  );
8820
- workspaceCommand.command("down").description("Free a workspace's routes, stop its devpod, and remove its worktree").argument("<workspace>", "Workspace token or branch name").option("--keep-worktree", "Leave the git worktree in place").option("--keep-devpod", "Leave the devpod workspace running").option("--repo <path>", "Main repository path (defaults to current directory)").action(
9613
+ workspaceCommand.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(
9614
+ withErrorHandling(async (_options, command) => {
9615
+ const options = command.opts();
9616
+ const { runWorkspaceGcCommand: runWorkspaceGcCommand2 } = await Promise.resolve().then(() => (init_workspace2(), workspace_exports));
9617
+ runWorkspaceGcCommand2(options);
9618
+ })
9619
+ );
9620
+ workspaceCommand.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(
9621
+ withErrorHandling(async (target, _options, command) => {
9622
+ const options = command.opts();
9623
+ const { runWorkspaceStopCommand: runWorkspaceStopCommand2 } = await Promise.resolve().then(() => (init_workspace2(), workspace_exports));
9624
+ await runWorkspaceStopCommand2(target, options);
9625
+ })
9626
+ );
9627
+ workspaceCommand.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(
8821
9628
  withErrorHandling(async (target, _options, command) => {
8822
9629
  const options = command.opts();
8823
9630
  const { runWorkspaceDownCommand: runWorkspaceDownCommand2 } = await Promise.resolve().then(() => (init_workspace2(), workspace_exports));