@devrouter/cli 0.2.0 → 0.3.0

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
@@ -610,11 +610,11 @@ var init_file_lock = __esm({
610
610
 
611
611
  // src/core/workspace.ts
612
612
  function comparableWorkspacePath(filePath) {
613
- const resolved = import_node_path3.default.resolve(filePath);
613
+ const resolved2 = import_node_path3.default.resolve(filePath);
614
614
  try {
615
- return import_node_fs3.default.realpathSync.native(resolved);
615
+ return import_node_fs3.default.realpathSync.native(resolved2);
616
616
  } catch {
617
- return resolved;
617
+ return resolved2;
618
618
  }
619
619
  }
620
620
  function sameWorkspacePath(left, right) {
@@ -1991,7 +1991,7 @@ function parseEnrollment(value, index, domains) {
1991
1991
  label2
1992
1992
  );
1993
1993
  const repoPath = parseCanonicalAbsolutePath(enrollment.repoPath, `${label2}.repoPath`);
1994
- const gitCommonDir = parseCanonicalAbsolutePath(enrollment.gitCommonDir, `${label2}.gitCommonDir`);
1994
+ const gitCommonDir2 = parseCanonicalAbsolutePath(enrollment.gitCommonDir, `${label2}.gitCommonDir`);
1995
1995
  const workspace = parseBoundedString(enrollment.workspace, `${label2}.workspace`, 32);
1996
1996
  if (!WORKSPACE_TOKEN_RE.test(workspace)) {
1997
1997
  throw new Error(`${label2}.workspace must be an existing workspace token.`);
@@ -2032,7 +2032,7 @@ function parseEnrollment(value, index, domains) {
2032
2032
  );
2033
2033
  return {
2034
2034
  repoPath,
2035
- gitCommonDir,
2035
+ gitCommonDir: gitCommonDir2,
2036
2036
  workspace,
2037
2037
  provider,
2038
2038
  providerId,
@@ -3163,10 +3163,10 @@ function normalizeOverridePattern(line) {
3163
3163
  const converted = `**/${rest}`;
3164
3164
  return isValidWorktreePattern(converted) ? converted : void 0;
3165
3165
  }
3166
- function readDisposableOverride(gitCommonDir) {
3166
+ function readDisposableOverride(gitCommonDir2) {
3167
3167
  let content;
3168
3168
  try {
3169
- content = import_node_fs7.default.readFileSync(import_node_path9.default.join(gitCommonDir, DISPOSABLE_OVERRIDE_FILE), "utf-8");
3169
+ content = import_node_fs7.default.readFileSync(import_node_path9.default.join(gitCommonDir2, DISPOSABLE_OVERRIDE_FILE), "utf-8");
3170
3170
  } catch {
3171
3171
  return [];
3172
3172
  }
@@ -3202,8 +3202,8 @@ function inspectLocalWorktreeSafety(worktreePath, options = {}) {
3202
3202
  "--git-dir",
3203
3203
  "--git-common-dir"
3204
3204
  ]);
3205
- const [gitDir, gitCommonDir] = dirs.stdout.split("\n").map((line) => line.trim());
3206
- if (!dirs.ok || !gitDir || !gitCommonDir) {
3205
+ const [gitDir, gitCommonDir2] = dirs.stdout.split("\n").map((line) => line.trim());
3206
+ if (!dirs.ok || !gitDir || !gitCommonDir2) {
3207
3207
  return { codes: ["git-error"], examples: {} };
3208
3208
  }
3209
3209
  if (!git(worktreePath, ["symbolic-ref", "-q", "HEAD"]).ok) safety.codes.push("detached");
@@ -3224,7 +3224,7 @@ function inspectLocalWorktreeSafety(worktreePath, options = {}) {
3224
3224
  }
3225
3225
  const disposable = [
3226
3226
  ...BUILTIN_DISPOSABLE_PATTERNS,
3227
- ...readDisposableOverride(gitCommonDir),
3227
+ ...readDisposableOverride(gitCommonDir2),
3228
3228
  ...options.extraDisposablePatterns ?? []
3229
3229
  ];
3230
3230
  const entries = status.stdout.split("\0");
@@ -3315,8 +3315,8 @@ function classifyWorktreeSafety(input2) {
3315
3315
  }
3316
3316
  return result(change);
3317
3317
  }
3318
- function readLedgerWorktreePaths(gitCommonDir) {
3319
- const directory = import_node_path9.default.join(gitCommonDir, "devrouter", "workspaces");
3318
+ function readLedgerWorktreePaths(gitCommonDir2) {
3319
+ const directory = import_node_path9.default.join(gitCommonDir2, "devrouter", "workspaces");
3320
3320
  let names;
3321
3321
  try {
3322
3322
  names = import_node_fs7.default.readdirSync(directory).filter((name) => name.endsWith(".json"));
@@ -4764,7 +4764,7 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs8.default.read
4764
4764
  const config = parseConfig(parsed ?? {}, configPath);
4765
4765
  const requiredVersion = config.devrouter?.version;
4766
4766
  if (requiredVersion && !hasWarnedVersionMismatch) {
4767
- const cliVersion = true ? "0.2.0" : "0.0.0-dev";
4767
+ const cliVersion = true ? "0.3.0" : "0.0.0-dev";
4768
4768
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
4769
4769
  hasWarnedVersionMismatch = true;
4770
4770
  process.stderr.write(
@@ -5041,13 +5041,13 @@ function applyWorkspace(config, workspace, repoPath) {
5041
5041
  app.host = namespaced;
5042
5042
  }
5043
5043
  if ("upstream" in app && app.upstream.includes(WORKSPACE_PLACEHOLDER)) {
5044
- const resolved = app.upstream.replaceAll(WORKSPACE_PLACEHOLDER, substitutionToken);
5044
+ const resolved2 = app.upstream.replaceAll(WORKSPACE_PLACEHOLDER, substitutionToken);
5045
5045
  try {
5046
- parseUpstream(resolved);
5046
+ parseUpstream(resolved2);
5047
5047
  } catch (err) {
5048
- throw new Error(`Resolved upstream '${resolved}' is invalid: ${err.message}`);
5048
+ throw new Error(`Resolved upstream '${resolved2}' is invalid: ${err.message}`);
5049
5049
  }
5050
- app.upstream = resolved;
5050
+ app.upstream = resolved2;
5051
5051
  }
5052
5052
  if (workspace && app.runtime === "docker") {
5053
5053
  app.docker.router = `${app.docker.router ?? app.name}-${workspace}`;
@@ -5056,11 +5056,11 @@ function applyWorkspace(config, workspace, repoPath) {
5056
5056
  return next;
5057
5057
  }
5058
5058
  function loadRuntimeConfig(repoPath, workspaceOverride, profileOverride) {
5059
- const resolved = resolveRepoPath(repoPath);
5060
- const raw = loadRepoConfig(resolved);
5059
+ const resolved2 = resolveRepoPath(repoPath);
5060
+ const raw = loadRepoConfig(resolved2);
5061
5061
  const resolvedProfile = resolveProfile(raw, profileOverride);
5062
- const workspace = resolveWorkspace(resolved, workspaceOverride);
5063
- const config = applyProfile(applyWorkspace(raw, workspace, resolved), resolvedProfile.profile);
5062
+ const workspace = resolveWorkspace(resolved2, workspaceOverride);
5063
+ const config = applyProfile(applyWorkspace(raw, workspace, resolved2), resolvedProfile.profile);
5064
5064
  return {
5065
5065
  config,
5066
5066
  workspace,
@@ -5213,28 +5213,28 @@ function selectedReadiness(config, profileName, profile, routedApps) {
5213
5213
  return sortedUnique(readiness);
5214
5214
  }
5215
5215
  function buildProfileResolutionReport(config, repoPath, profileOverride) {
5216
- const resolved = resolveProfile(config, profileOverride);
5217
- const filtered = applyProfile(config, resolved.profile);
5216
+ const resolved2 = resolveProfile(config, profileOverride);
5217
+ const filtered = applyProfile(config, resolved2.profile);
5218
5218
  const routedApps = filtered.apps.filter((app) => app.kind !== "dependency");
5219
5219
  const dependencies = filtered.apps.filter((app) => app.kind === "dependency");
5220
- const readiness = selectedReadiness(config, resolved.name, resolved.profile, routedApps);
5220
+ const readiness = selectedReadiness(config, resolved2.name, resolved2.profile, routedApps);
5221
5221
  const managedRuntime = config.managedRuntime;
5222
5222
  const baseServices = sortedUnique(managedRuntime?.devcontainer.baseServices ?? []);
5223
5223
  const profileServices = managedRuntime ? selectedManagedResources(
5224
- resolved.profile,
5225
- resolved.profile?.devcontainerServices,
5224
+ resolved2.profile,
5225
+ resolved2.profile?.devcontainerServices,
5226
5226
  managedRuntime.devcontainer.profileServices
5227
5227
  ) : [];
5228
5228
  const processes = managedRuntime ? selectedManagedResources(
5229
- resolved.profile,
5230
- resolved.profile?.processes,
5229
+ resolved2.profile,
5230
+ resolved2.profile?.processes,
5231
5231
  managedRuntime.processes
5232
5232
  ) : [];
5233
- const notices = managedFullExpansionNotices(config, resolved.name);
5233
+ const notices = managedFullExpansionNotices(config, resolved2.name);
5234
5234
  return {
5235
5235
  schemaVersion: 1,
5236
5236
  repoPath,
5237
- profile: resolved.name,
5237
+ profile: resolved2.name,
5238
5238
  apps: sortedUnique(routedApps.map((app) => app.name)),
5239
5239
  dependencies: sortedUnique(dependencies.map((app) => app.name)),
5240
5240
  readiness: sortedUnique(readiness),
@@ -6763,7 +6763,7 @@ var init_reliability_contract = __esm({
6763
6763
 
6764
6764
  // src/core/reliability-operation-store.ts
6765
6765
  function installedCliVersion() {
6766
- return true ? "0.2.0" : "0.0.0-dev";
6766
+ return true ? "0.3.0" : "0.0.0-dev";
6767
6767
  }
6768
6768
  function compareCliVersions(left, right) {
6769
6769
  const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
@@ -7529,9 +7529,9 @@ async function resolveCapacityEnrollment(policy, request2, signal, bindingResolv
7529
7529
  ["-C", environment.repoPath, "rev-parse", "--path-format=absolute", "--git-common-dir"],
7530
7530
  signal
7531
7531
  )).trim();
7532
- const gitCommonDir = import_node_fs13.default.realpathSync(common);
7532
+ const gitCommonDir2 = import_node_fs13.default.realpathSync(common);
7533
7533
  const enrollment = policy.enrollments.find(
7534
- (entry) => entry.repoPath === environment.repoPath && entry.gitCommonDir === gitCommonDir && entry.workspace === environment.workspace && entry.provider === environment.provider && entry.providerId === environment.providerId
7534
+ (entry) => entry.repoPath === environment.repoPath && entry.gitCommonDir === gitCommonDir2 && entry.workspace === environment.workspace && entry.provider === environment.provider && entry.providerId === environment.providerId
7535
7535
  );
7536
7536
  if (!enrollment?.profiles.includes(environment.profile))
7537
7537
  throw new Error("Canonical runtime profile is not enrolled for capacity admission.");
@@ -7550,8 +7550,8 @@ async function enrollCapacityLifecycle(policy, request2, signal, directory = imp
7550
7550
  const expectedPolicy = JSON.stringify(policy);
7551
7551
  if (policy.admissions !== "enabled" || JSON.stringify(readCapacityPolicy(directory)) !== expectedPolicy)
7552
7552
  throw new Error("Capacity enrollment policy is not current and enabled.");
7553
- const resolved = await resolveCapacityEnrollment(policy, request2, signal, bindingResolver);
7554
- const { environment, enrollment } = resolved;
7553
+ const resolved2 = await resolveCapacityEnrollment(policy, request2, signal, bindingResolver);
7554
+ const { environment, enrollment } = resolved2;
7555
7555
  const runtime = policy.domains[enrollment.runtimeDomain];
7556
7556
  if (policy.admissions !== "enabled" || runtime?.kind !== "runtime")
7557
7557
  throw new Error("Capacity enrollment policy is not enabled.");
@@ -7575,7 +7575,7 @@ async function enrollCapacityLifecycle(policy, request2, signal, directory = imp
7575
7575
  daemonId: runtime.daemonId,
7576
7576
  estimatesDigest: enrollment.estimatesDigest
7577
7577
  });
7578
- return resolved;
7578
+ return resolved2;
7579
7579
  }
7580
7580
  var import_node_fs13, import_node_path15;
7581
7581
  var init_capacity_enrollment = __esm({
@@ -10000,9 +10000,9 @@ function writeManifestRecord(cacheRoot, manifest) {
10000
10000
  function pinnedManifest(version) {
10001
10001
  return version === PINNED_DEVSY_AGENT_VERSION ? { version, origin: "pinned", assets: DEVSY_AGENT_ASSETS } : void 0;
10002
10002
  }
10003
- function manifestForVersion(resolved, version) {
10004
- if (resolved.assets) return { version, origin: "pinned", assets: resolved.assets };
10005
- return pinnedManifest(version) ?? readManifestRecord(resolved.cacheRoot, version);
10003
+ function manifestForVersion(resolved2, version) {
10004
+ if (resolved2.assets) return { version, origin: "pinned", assets: resolved2.assets };
10005
+ return pinnedManifest(version) ?? readManifestRecord(resolved2.cacheRoot, version);
10006
10006
  }
10007
10007
  async function resolveReleaseDevsyAgentManifest(version, fetcher) {
10008
10008
  const url = `${DEVSY_RELEASE_METADATA_BASE_URL}/${DEVSY_RELEASE_TAG_PREFIX}${version}`;
@@ -10124,9 +10124,9 @@ function resolvedOptions(options) {
10124
10124
  };
10125
10125
  }
10126
10126
  function inspectDevsyAgent(options = {}) {
10127
- const resolved = resolvedOptions(options);
10128
- const installedVersion = parseVersion2(resolved.versionOutput);
10129
- const explicitPath = resolved.env.DEVSY_AGENT_BINARY?.trim();
10127
+ const resolved2 = resolvedOptions(options);
10128
+ const installedVersion = parseVersion2(resolved2.versionOutput);
10129
+ const explicitPath = resolved2.env.DEVSY_AGENT_BINARY?.trim();
10130
10130
  const source = explicitPath ? "explicit" : "managed";
10131
10131
  if (!installedVersion) {
10132
10132
  return {
@@ -10143,11 +10143,11 @@ function inspectDevsyAgent(options = {}) {
10143
10143
  installedVersion
10144
10144
  };
10145
10145
  }
10146
- const manifest = manifestForVersion(resolved, installedVersion);
10146
+ const manifest = manifestForVersion(resolved2, installedVersion);
10147
10147
  if (!manifest) {
10148
10148
  if (explicitPath) {
10149
10149
  return {
10150
- ...inspectBinary(explicitPath, resolved.assets ?? DEVSY_AGENT_ASSETS, "invalid"),
10150
+ ...inspectBinary(explicitPath, resolved2.assets ?? DEVSY_AGENT_ASSETS, "invalid"),
10151
10151
  source,
10152
10152
  binaryPath: explicitPath,
10153
10153
  installedVersion
@@ -10161,13 +10161,13 @@ function inspectDevsyAgent(options = {}) {
10161
10161
  drift: { installed: installedVersion, supported: PINNED_DEVSY_AGENT_VERSION }
10162
10162
  };
10163
10163
  }
10164
- const expectedName = resolved.nativeAssetName ?? nativeAssetName(resolved.platform, resolved.arch);
10164
+ const expectedName = resolved2.nativeAssetName ?? nativeAssetName(resolved2.platform, resolved2.arch);
10165
10165
  const asset = manifest.assets.find((candidate) => candidate.name === expectedName);
10166
10166
  if (!asset) {
10167
10167
  return {
10168
10168
  state: "stale",
10169
10169
  source,
10170
- reason: `Devsy ${installedVersion} has no official agent for ${resolved.platform}/${resolved.arch}`,
10170
+ reason: `Devsy ${installedVersion} has no official agent for ${resolved2.platform}/${resolved2.arch}`,
10171
10171
  installedVersion,
10172
10172
  manifestOrigin: manifest.origin
10173
10173
  };
@@ -10181,7 +10181,7 @@ function inspectDevsyAgent(options = {}) {
10181
10181
  manifestOrigin: manifest.origin
10182
10182
  };
10183
10183
  }
10184
- const binaryPath = managedBinaryPath(resolved.cacheRoot, installedVersion, asset);
10184
+ const binaryPath = managedBinaryPath(resolved2.cacheRoot, installedVersion, asset);
10185
10185
  return {
10186
10186
  ...inspectBinary(binaryPath, [asset], "missing"),
10187
10187
  source,
@@ -10354,28 +10354,28 @@ function preparedFromInspection(inspection) {
10354
10354
  transport: "existing"
10355
10355
  };
10356
10356
  }
10357
- function expectedAgentName(resolved) {
10358
- return resolved.nativeAssetName ?? nativeAssetName(resolved.platform, resolved.arch);
10357
+ function expectedAgentName(resolved2) {
10358
+ return resolved2.nativeAssetName ?? nativeAssetName(resolved2.platform, resolved2.arch);
10359
10359
  }
10360
- async function loadOrResolveManifest(resolved, version, options) {
10361
- const manifest = manifestForVersion(resolved, version);
10360
+ async function loadOrResolveManifest(resolved2, version, options) {
10361
+ const manifest = manifestForVersion(resolved2, version);
10362
10362
  if (manifest) return manifest;
10363
10363
  const release = await resolveReleaseDevsyAgentManifest(version, options.fetcher ?? fetch);
10364
- writeManifestRecord(resolved.cacheRoot, release);
10364
+ writeManifestRecord(resolved2.cacheRoot, release);
10365
10365
  return release;
10366
10366
  }
10367
10367
  async function prepareDevsyAgent(options = {}) {
10368
- const resolved = resolvedOptions(options);
10369
- const inspectOptions = resolved;
10368
+ const resolved2 = resolvedOptions(options);
10369
+ const inspectOptions = resolved2;
10370
10370
  const before = inspectDevsyAgent(inspectOptions);
10371
10371
  if (before.state === "stale") throw new DevsyAgentReadinessError(before);
10372
10372
  if (isReadyInspection(before) && before.source !== "host") {
10373
10373
  return preparedFromInspection(before);
10374
10374
  }
10375
10375
  const lock = options.withLock ?? ((operation) => {
10376
- import_node_fs17.default.mkdirSync(import_node_path23.default.dirname(resolved.lockPath), { recursive: true });
10376
+ import_node_fs17.default.mkdirSync(import_node_path23.default.dirname(resolved2.lockPath), { recursive: true });
10377
10377
  return withFileLock(
10378
- resolved.lockPath,
10378
+ resolved2.lockPath,
10379
10379
  {
10380
10380
  activity: "Devsy agent acquisition",
10381
10381
  waitMs: DEVSY_AGENT_LOCK_WAIT_MS,
@@ -10393,27 +10393,27 @@ async function prepareDevsyAgent(options = {}) {
10393
10393
  }
10394
10394
  const version = current.installedVersion;
10395
10395
  if (!version) throw new DevsyAgentReadinessError(current);
10396
- const manifest = await loadOrResolveManifest(resolved, version, options);
10396
+ const manifest = await loadOrResolveManifest(resolved2, version, options);
10397
10397
  if (current.source === "explicit") {
10398
10398
  const verified = inspectDevsyAgent(inspectOptions);
10399
10399
  if (isReadyInspection(verified)) return preparedFromInspection(verified);
10400
10400
  throw new DevsyAgentReadinessError(verified);
10401
10401
  }
10402
10402
  const asset = manifest.assets.find(
10403
- (candidate) => candidate.name === expectedAgentName(resolved)
10403
+ (candidate) => candidate.name === expectedAgentName(resolved2)
10404
10404
  );
10405
10405
  if (!asset) {
10406
10406
  throw new DevsyAgentReadinessError({
10407
10407
  state: "stale",
10408
10408
  source: current.source,
10409
- reason: `Devsy ${version} has no official agent for ${resolved.platform}/${resolved.arch}`,
10409
+ reason: `Devsy ${version} has no official agent for ${resolved2.platform}/${resolved2.arch}`,
10410
10410
  installedVersion: version,
10411
10411
  manifestOrigin: manifest.origin
10412
10412
  });
10413
10413
  }
10414
10414
  const transport = await downloadAndPublish(
10415
10415
  asset,
10416
- managedBinaryPath(resolved.cacheRoot, version, asset),
10416
+ managedBinaryPath(resolved2.cacheRoot, version, asset),
10417
10417
  options.fetcher ?? fetch,
10418
10418
  options.githubCliDownloader ?? downloadWithGitHubCli
10419
10419
  );
@@ -10525,11 +10525,11 @@ var init_devcontainer_config = __esm({
10525
10525
  // src/core/paths.ts
10526
10526
  function assertPathWithinRepo(filePath, repoRoot, label2) {
10527
10527
  const resolvedRoot = import_node_path25.default.resolve(repoRoot);
10528
- const resolved = import_node_path25.default.resolve(repoRoot, filePath);
10529
- if (resolved !== resolvedRoot && !resolved.startsWith(resolvedRoot + import_node_path25.default.sep)) {
10528
+ const resolved2 = import_node_path25.default.resolve(repoRoot, filePath);
10529
+ if (resolved2 !== resolvedRoot && !resolved2.startsWith(resolvedRoot + import_node_path25.default.sep)) {
10530
10530
  throw new Error(`${label2} path '${filePath}' escapes the repository root.`);
10531
10531
  }
10532
- return resolved;
10532
+ return resolved2;
10533
10533
  }
10534
10534
  var import_node_path25;
10535
10535
  var init_paths = __esm({
@@ -10586,15 +10586,15 @@ function resolveComposeFiles(source, repoPath, linked) {
10586
10586
  }
10587
10587
  const directory = import_node_path26.default.join(repoPath, ".devcontainer");
10588
10588
  const files = references.map((reference) => {
10589
- const resolved2 = assertPathWithinRepo(
10589
+ const resolved3 = assertPathWithinRepo(
10590
10590
  resolveComposeReference(reference, linked),
10591
10591
  directory,
10592
10592
  "dockerComposeFile"
10593
10593
  );
10594
- if (!import_node_fs19.default.existsSync(resolved2) || !import_node_fs19.default.lstatSync(resolved2).isFile()) {
10595
- throw new Error(`Managed Dev Container compose file does not exist: ${resolved2}`);
10594
+ if (!import_node_fs19.default.existsSync(resolved3) || !import_node_fs19.default.lstatSync(resolved3).isFile()) {
10595
+ throw new Error(`Managed Dev Container compose file does not exist: ${resolved3}`);
10596
10596
  }
10597
- return resolved2;
10597
+ return resolved3;
10598
10598
  });
10599
10599
  for (const file of files) {
10600
10600
  let parsed;
@@ -10610,7 +10610,7 @@ function resolveComposeFiles(source, repoPath, linked) {
10610
10610
  }
10611
10611
  }
10612
10612
  const fileArgs = files.flatMap((file) => ["-f", file]);
10613
- const resolved = (0, import_node_child_process10.spawnSync)(
10613
+ const resolved2 = (0, import_node_child_process10.spawnSync)(
10614
10614
  "docker",
10615
10615
  [
10616
10616
  "compose",
@@ -10626,10 +10626,10 @@ function resolveComposeFiles(source, repoPath, linked) {
10626
10626
  ],
10627
10627
  { cwd: directory, encoding: "utf-8" }
10628
10628
  );
10629
- if (resolved.status !== 0) {
10629
+ if (resolved2.status !== 0) {
10630
10630
  throw new Error("Could not resolve the effective managed Dev Container Compose model.");
10631
10631
  }
10632
- const services = String(resolved.stdout ?? "").split(/\r?\n/).map((service) => service.trim()).filter(Boolean);
10632
+ const services = String(resolved2.stdout ?? "").split(/\r?\n/).map((service) => service.trim()).filter(Boolean);
10633
10633
  if (services.length === 0 || new Set(services).size !== services.length) {
10634
10634
  throw new Error("The effective managed Dev Container Compose model has no unique services.");
10635
10635
  }
@@ -12882,7 +12882,7 @@ function proveInitialManagedDevsyAbsence(repoPath, expectedId) {
12882
12882
  const ownership = inspectDevsyWorkspaceOwnership(listDevsyWorkspaces(), devsyId, repoPath);
12883
12883
  if (ownership.status === "conflict") throw new Error(ownership.reason);
12884
12884
  if (ownership.status !== "absent") return void 0;
12885
- const gitCommonDir = resolveGitCommonDir(repoPath);
12885
+ const gitCommonDir2 = resolveGitCommonDir(repoPath);
12886
12886
  const endpoint = resolveManagedStopEndpoint();
12887
12887
  if (!supportsManagedStopBaseline(endpoint))
12888
12888
  throw new Error("Initial managed stop requires a local Docker endpoint.");
@@ -12894,7 +12894,7 @@ function proveInitialManagedDevsyAbsence(repoPath, expectedId) {
12894
12894
  const legacyWorkspaces = readLegacy();
12895
12895
  const observe = () => {
12896
12896
  resetWorkspaceRuntimeCaches();
12897
- if (resolveWorkspaceRuntimeOrDefault(repoPath) !== "devsy" || !isLinkedWorktree(repoPath) || resolveWorktreeWorkspace(repoPath) !== workspace || resolveGitCommonDir(repoPath) !== gitCommonDir || !(0, import_node_util10.isDeepStrictEqual)(readWorkspaceOwnership(repoPath, workspace), record5) || inspectWorkspaceOwnership(record5, listGitWorktrees(repoPath), void 0).ownerStatus !== "present" || readManagedRuntimeState(repoPath, workspace))
12897
+ if (resolveWorkspaceRuntimeOrDefault(repoPath) !== "devsy" || !isLinkedWorktree(repoPath) || resolveWorktreeWorkspace(repoPath) !== workspace || resolveGitCommonDir(repoPath) !== gitCommonDir2 || !(0, import_node_util10.isDeepStrictEqual)(readWorkspaceOwnership(repoPath, workspace), record5) || inspectWorkspaceOwnership(record5, listGitWorktrees(repoPath), void 0).ownerStatus !== "present" || readManagedRuntimeState(repoPath, workspace))
12898
12898
  throw new Error("Initial managed stop ownership or retained state changed.");
12899
12899
  if (devpodRegistryRoot() !== legacyHome || !(0, import_node_util10.isDeepStrictEqual)(readLegacy(), legacyWorkspaces))
12900
12900
  throw new Error("Initial managed stop legacy registry evidence changed.");
@@ -12920,7 +12920,7 @@ function proveInitialManagedDevsyAbsence(repoPath, expectedId) {
12920
12920
  return {
12921
12921
  workspace,
12922
12922
  record: record5,
12923
- gitCommonDir,
12923
+ gitCommonDir: gitCommonDir2,
12924
12924
  endpoint,
12925
12925
  daemon,
12926
12926
  routes,
@@ -12949,8 +12949,8 @@ function stopInitialManagedDevsyWorkspace(repoPath, devsyId) {
12949
12949
  };
12950
12950
  };
12951
12951
  const authority = readAuthority();
12952
- const gitCommonDir = linked ? resolveGitCommonDir(repoPath) : void 0;
12953
- const workspaceEnv = workspace && gitCommonDir ? { token: workspace, gitCommonDir } : void 0;
12952
+ const gitCommonDir2 = linked ? resolveGitCommonDir(repoPath) : void 0;
12953
+ const workspaceEnv = workspace && gitCommonDir2 ? { token: workspace, gitCommonDir: gitCommonDir2 } : void 0;
12954
12954
  function canonicalProfile(recorded) {
12955
12955
  return resolveProfile(loadRepoConfig(repoPath), recorded).name;
12956
12956
  }
@@ -12963,7 +12963,7 @@ function stopInitialManagedDevsyWorkspace(repoPath, devsyId) {
12963
12963
  throw new Error("Initial managed stop requires one exact Devsy registration.");
12964
12964
  if (linked) {
12965
12965
  const record5 = workspace ? readWorkspaceOwnership(repoPath, workspace) : void 0;
12966
- if (!record5 || record5.devpodId !== devsyId || !sameWorkspacePath(record5.worktreePath, repoPath) || resolveWorktreeWorkspace(repoPath) !== workspace || resolveGitCommonDir(repoPath) !== gitCommonDir || inspectWorkspaceOwnership(record5, listGitWorktrees(repoPath), void 0).ownerStatus !== "present")
12966
+ if (!record5 || record5.devpodId !== devsyId || !sameWorkspacePath(record5.worktreePath, repoPath) || resolveWorktreeWorkspace(repoPath) !== workspace || resolveGitCommonDir(repoPath) !== gitCommonDir2 || inspectWorkspaceOwnership(record5, listGitWorktrees(repoPath), void 0).ownerStatus !== "present")
12967
12967
  throw new Error("Initial managed stop workspace ownership changed.");
12968
12968
  }
12969
12969
  return owner2.workspace;
@@ -14185,9 +14185,9 @@ function validateRoot(root) {
14185
14185
  if (typeof root !== "string" || !import_node_path36.default.isAbsolute(root) || root.includes("\0")) {
14186
14186
  throw claimError("invalid-input", "network claim root is invalid");
14187
14187
  }
14188
- const resolved = import_node_path36.default.resolve(root);
14189
- if (resolved !== root) throw claimError("invalid-input", "network claim root is invalid");
14190
- return resolved;
14188
+ const resolved2 = import_node_path36.default.resolve(root);
14189
+ if (resolved2 !== root) throw claimError("invalid-input", "network claim root is invalid");
14190
+ return resolved2;
14191
14191
  }
14192
14192
  function hashKey(value) {
14193
14193
  return (0, import_node_crypto15.createHash)("sha256").update(value).digest("hex");
@@ -14526,6 +14526,25 @@ function inspectExactOwnership(devsyId, worktreePath) {
14526
14526
  if (ownership.status === "conflict") throw new Error(ownership.reason);
14527
14527
  return ownership;
14528
14528
  }
14529
+ function checkoutVanished(worktreePath) {
14530
+ try {
14531
+ import_node_fs30.default.lstatSync(worktreePath);
14532
+ return false;
14533
+ } catch (error) {
14534
+ return error.code === "ENOENT";
14535
+ }
14536
+ }
14537
+ function deleteVanishedCheckoutRegistration(devsyId, worktreePath, originalError) {
14538
+ if (!checkoutVanished(worktreePath)) throw originalError;
14539
+ const runtime = inspectDevsyRuntimeStatus(devsyId);
14540
+ if (runtime !== "stopped" && runtime !== "not-found") throw originalError;
14541
+ runDevsyAction("delete", devsyId, true, worktreePath);
14542
+ if (inspectExactOwnership(devsyId, worktreePath).status !== "absent") {
14543
+ throw new Error(
14544
+ `Devsy workspace '${devsyId}' still owns vanished checkout '${worktreePath}' after forced provider delete.`
14545
+ );
14546
+ }
14547
+ }
14529
14548
  function mutateOwnedDevsyWorkspace(action2, devsyId, worktreePath) {
14530
14549
  return withMutationLock(`Devsy ${action2}`, worktreePath, () => {
14531
14550
  restoreRecordedManagedDevcontainerConfig(worktreePath);
@@ -14539,7 +14558,13 @@ function mutateOwnedDevsyWorkspace(action2, devsyId, worktreePath) {
14539
14558
  }
14540
14559
  const before = inspectExactOwnership(devsyId, worktreePath);
14541
14560
  if (before.status === "absent") return { status: "absent" };
14542
- runDevsyAction(action2, devsyId, false, worktreePath);
14561
+ try {
14562
+ runDevsyAction(action2, devsyId, false, worktreePath);
14563
+ } catch (error) {
14564
+ if (action2 !== "delete") throw error;
14565
+ deleteVanishedCheckoutRegistration(devsyId, worktreePath, error);
14566
+ return { status: "changed" };
14567
+ }
14543
14568
  let after = inspectExactOwnership(devsyId, worktreePath);
14544
14569
  if (action2 === "stop" && after.status !== "owned") {
14545
14570
  throw new Error(
@@ -19554,9 +19579,9 @@ function createCapacityController(options) {
19554
19579
  const resolveCapacityTarget = async (environment, signal) => {
19555
19580
  const current = readCapacityPolicy(options.directory);
19556
19581
  if (current?.admissions !== "enabled" || !(0, import_node_util13.isDeepStrictEqual)(current, policy)) return void 0;
19557
- let resolved;
19582
+ let resolved2;
19558
19583
  try {
19559
- resolved = await resolveCapacityEnrollment(
19584
+ resolved2 = await resolveCapacityEnrollment(
19560
19585
  current,
19561
19586
  { path: environment.repoPath, profile: environment.profile, require: [] },
19562
19587
  signal,
@@ -19565,12 +19590,12 @@ function createCapacityController(options) {
19565
19590
  } catch {
19566
19591
  return void 0;
19567
19592
  }
19568
- if (signal.aborted || !(0, import_node_util13.isDeepStrictEqual)(resolved.environment, environment)) return void 0;
19593
+ if (signal.aborted || !(0, import_node_util13.isDeepStrictEqual)(resolved2.environment, environment)) return void 0;
19569
19594
  if (!(0, import_node_util13.isDeepStrictEqual)(readCapacityPolicy(options.directory), current)) return void 0;
19570
- const runtime = current.domains[resolved.enrollment.runtimeDomain];
19571
- if (runtime?.kind !== "runtime" || runtime.hostDomain !== resolved.enrollment.hostDomain || current.domains[runtime.hostDomain]?.kind !== "host" || !current.enrollments.some((entry) => (0, import_node_util13.isDeepStrictEqual)(entry, resolved.enrollment)))
19595
+ const runtime = current.domains[resolved2.enrollment.runtimeDomain];
19596
+ if (runtime?.kind !== "runtime" || runtime.hostDomain !== resolved2.enrollment.hostDomain || current.domains[runtime.hostDomain]?.kind !== "host" || !current.enrollments.some((entry) => (0, import_node_util13.isDeepStrictEqual)(entry, resolved2.enrollment)))
19572
19597
  return void 0;
19573
- return { current, resolved, runtime };
19598
+ return { current, resolved: resolved2, runtime };
19574
19599
  };
19575
19600
  const capacityPool = (runtime, runtimeDomain) => ({
19576
19601
  daemonId: runtime.daemonId,
@@ -19692,7 +19717,7 @@ function createCapacityController(options) {
19692
19717
  if (current?.admissions !== "enabled" || !(0, import_node_util13.isDeepStrictEqual)(current, policy))
19693
19718
  throw new Error("Capacity controller policy changed.");
19694
19719
  validate3();
19695
- const resolved = await enrollCapacityLifecycle(
19720
+ const resolved2 = await enrollCapacityLifecycle(
19696
19721
  current,
19697
19722
  {
19698
19723
  path: environment.repoPath,
@@ -19703,16 +19728,16 @@ function createCapacityController(options) {
19703
19728
  options.directory,
19704
19729
  bindingResolver
19705
19730
  );
19706
- if (!(0, import_node_util13.isDeepStrictEqual)(resolved.environment, environment) || signal.aborted)
19731
+ if (!(0, import_node_util13.isDeepStrictEqual)(resolved2.environment, environment) || signal.aborted)
19707
19732
  throw new Error("Capacity submission binding changed.");
19708
19733
  if (!(0, import_node_util13.isDeepStrictEqual)(readCapacityPolicy(options.directory), current))
19709
19734
  throw new Error("Capacity submission policy changed during resolution.");
19710
- const runtime = current.domains[resolved.enrollment.runtimeDomain];
19711
- if (runtime?.kind !== "runtime" || runtime.hostDomain !== resolved.enrollment.hostDomain || current.domains[runtime.hostDomain]?.kind !== "host" || !current.enrollments.some((entry) => (0, import_node_util13.isDeepStrictEqual)(entry, resolved.enrollment)))
19735
+ const runtime = current.domains[resolved2.enrollment.runtimeDomain];
19736
+ if (runtime?.kind !== "runtime" || runtime.hostDomain !== resolved2.enrollment.hostDomain || current.domains[runtime.hostDomain]?.kind !== "host" || !current.enrollments.some((entry) => (0, import_node_util13.isDeepStrictEqual)(entry, resolved2.enrollment)))
19712
19737
  throw new Error("Capacity pool does not match policy enrollment.");
19713
19738
  const pool = {
19714
19739
  daemonId: runtime.daemonId,
19715
- runtimeDomain: resolved.enrollment.runtimeDomain,
19740
+ runtimeDomain: resolved2.enrollment.runtimeDomain,
19716
19741
  hostDomain: runtime.hostDomain,
19717
19742
  hostChargeCeilingBytes: runtime.hostChargeCeilingBytes
19718
19743
  };
@@ -19723,11 +19748,11 @@ function createCapacityController(options) {
19723
19748
  settlePreparedLifecycleCapacity({
19724
19749
  identity: identity2,
19725
19750
  controller,
19726
- estimates: resolved.estimates,
19727
- enrollment: resolved.enrollment,
19751
+ estimates: resolved2.estimates,
19752
+ enrollment: resolved2.enrollment,
19728
19753
  directory: options.directory
19729
19754
  });
19730
- const charge = capacityRequest(resolved.estimates, resolved.enrollment, {
19755
+ const charge = capacityRequest(resolved2.estimates, resolved2.enrollment, {
19731
19756
  environmentId: record5.state.environmentId,
19732
19757
  profile: environment.profile,
19733
19758
  ...record5.activeProfile ? { activeProfile: record5.activeProfile } : {},
@@ -19751,8 +19776,8 @@ function createCapacityController(options) {
19751
19776
  try {
19752
19777
  await publishQueuedStartupWitness({
19753
19778
  identity: identity2,
19754
- provider: resolved.enrollment.provider,
19755
- providerId: resolved.enrollment.providerId,
19779
+ provider: resolved2.enrollment.provider,
19780
+ providerId: resolved2.enrollment.providerId,
19756
19781
  operationId: prepared.operationId,
19757
19782
  fence: prepared.request.fence,
19758
19783
  profile: environment.profile,
@@ -19786,7 +19811,7 @@ function createCapacityController(options) {
19786
19811
  reservationId: (0, import_node_crypto24.randomUUID)(),
19787
19812
  policyRevision: current.revision
19788
19813
  },
19789
- { estimates: resolved.estimates, enrollment: resolved.enrollment, pool }
19814
+ { estimates: resolved2.estimates, enrollment: resolved2.enrollment, pool }
19790
19815
  );
19791
19816
  acceptedPayloads.set(prepared.operationId, signature);
19792
19817
  for (const id5 of acceptedPayloads.keys()) {
@@ -19851,9 +19876,9 @@ function createCapacityController(options) {
19851
19876
  }
19852
19877
  if (!failedCapabilities.some((capability) => observed.failedCapabilities.includes(capability)))
19853
19878
  return;
19854
- let resolved;
19879
+ let resolved2;
19855
19880
  try {
19856
- resolved = await resolveCapacityEnrollment(
19881
+ resolved2 = await resolveCapacityEnrollment(
19857
19882
  current,
19858
19883
  { path: environment.repoPath, profile: environment.profile, require: [] },
19859
19884
  signal,
@@ -19862,15 +19887,15 @@ function createCapacityController(options) {
19862
19887
  } catch {
19863
19888
  return;
19864
19889
  }
19865
- if (signal.aborted || !(0, import_node_util13.isDeepStrictEqual)(resolved.environment, environment)) return;
19890
+ if (signal.aborted || !(0, import_node_util13.isDeepStrictEqual)(resolved2.environment, environment)) return;
19866
19891
  if (!(0, import_node_util13.isDeepStrictEqual)(readCapacityPolicy(options.directory), current)) return;
19867
- const runtime = current.domains[resolved.enrollment.runtimeDomain];
19868
- if (runtime?.kind !== "runtime" || runtime.hostDomain !== resolved.enrollment.hostDomain || current.domains[runtime.hostDomain]?.kind !== "host" || !current.enrollments.some((entry) => (0, import_node_util13.isDeepStrictEqual)(entry, resolved.enrollment)))
19892
+ const runtime = current.domains[resolved2.enrollment.runtimeDomain];
19893
+ if (runtime?.kind !== "runtime" || runtime.hostDomain !== resolved2.enrollment.hostDomain || current.domains[runtime.hostDomain]?.kind !== "host" || !current.enrollments.some((entry) => (0, import_node_util13.isDeepStrictEqual)(entry, resolved2.enrollment)))
19869
19894
  return;
19870
19895
  const identity2 = environmentIdentity(environment);
19871
19896
  const record5 = readReliabilityOperation(identity2);
19872
19897
  const durable = record5?.enrollment;
19873
- if (!record5 || !durable || durable.policyRevision !== current.revision || durable.gitCommonDir !== resolved.enrollment.gitCommonDir || durable.providerId !== resolved.enrollment.providerId || durable.hostDomain !== resolved.enrollment.hostDomain || durable.runtimeDomain !== resolved.enrollment.runtimeDomain || durable.endpoint !== runtime.endpoint || durable.daemonId !== runtime.daemonId || durable.estimatesDigest !== resolved.enrollment.estimatesDigest)
19898
+ if (!record5 || !durable || durable.policyRevision !== current.revision || durable.gitCommonDir !== resolved2.enrollment.gitCommonDir || durable.providerId !== resolved2.enrollment.providerId || durable.hostDomain !== resolved2.enrollment.hostDomain || durable.runtimeDomain !== resolved2.enrollment.runtimeDomain || durable.endpoint !== runtime.endpoint || durable.daemonId !== runtime.daemonId || durable.estimatesDigest !== resolved2.enrollment.estimatesDigest)
19874
19899
  return;
19875
19900
  if (record5.state.operation && queue.hasOperation(record5.state.operation.id)) return;
19876
19901
  let proof;
@@ -19885,7 +19910,7 @@ function createCapacityController(options) {
19885
19910
  if (required.length === 0) return;
19886
19911
  const pool = {
19887
19912
  daemonId: runtime.daemonId,
19888
- runtimeDomain: resolved.enrollment.runtimeDomain,
19913
+ runtimeDomain: resolved2.enrollment.runtimeDomain,
19889
19914
  hostDomain: runtime.hostDomain,
19890
19915
  hostChargeCeilingBytes: runtime.hostChargeCeilingBytes
19891
19916
  };
@@ -19924,7 +19949,7 @@ function createCapacityController(options) {
19924
19949
  return;
19925
19950
  }
19926
19951
  if (!prepared) return;
19927
- const charge = capacityRequest(resolved.estimates, resolved.enrollment, {
19952
+ const charge = capacityRequest(resolved2.estimates, resolved2.enrollment, {
19928
19953
  environmentId: record5.state.environmentId,
19929
19954
  profile: environment.profile,
19930
19955
  ...record5.activeProfile ? { activeProfile: record5.activeProfile } : {},
@@ -19939,7 +19964,7 @@ function createCapacityController(options) {
19939
19964
  reservationId: (0, import_node_crypto24.randomUUID)(),
19940
19965
  policyRevision: current.revision
19941
19966
  },
19942
- { estimates: resolved.estimates, enrollment: resolved.enrollment, pool }
19967
+ { estimates: resolved2.estimates, enrollment: resolved2.enrollment, pool }
19943
19968
  );
19944
19969
  } catch {
19945
19970
  try {
@@ -20968,7 +20993,7 @@ async function runController(options) {
20968
20993
  );
20969
20994
  void initial.then(async ({ environment, identity: identity2, revision, requirements: requirements2 }) => {
20970
20995
  let persisted;
20971
- const resolved = await resolve(
20996
+ const resolved2 = await resolve(
20972
20997
  {
20973
20998
  path: environment.repoPath,
20974
20999
  profile: environment.profile,
@@ -20980,7 +21005,7 @@ async function runController(options) {
20980
21005
  }
20981
21006
  );
20982
21007
  assertActive();
20983
- if (!persisted || JSON.stringify(resolved) !== JSON.stringify(environment))
21008
+ if (!persisted || JSON.stringify(resolved2) !== JSON.stringify(environment))
20984
21009
  throw new Error("Protection ownership evidence unavailable or changed.");
20985
21010
  const proof = persisted;
20986
21011
  const finish2 = serial.then(() => {
@@ -21979,12 +22004,13 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
21979
22004
  - \`devrouter workspace up <branch> [--path <dir>] [--no-devpod] [--open]\`: create a worktree and start/prove it unless create-only mode is requested
21980
22005
  - \`devrouter workspace ensure [path] [--profile <name>] [--repair] [--open] [--json]\`: compatibility alias of \`devrouter ensure\`
21981
22006
  - \`devrouter workspace ls [--json]\`: list ownership, Git, workspace runtime, route, path, and branch evidence
21982
- - \`devrouter workspace cleanup [--repo .] [--inactive-for 30d] [--check-merged] [--measure-size] [--all-worktrees] [--manifest [--action reclaim|trim] [--output <file>]] [--json]\`: report-only cleanup evidence and exact guarded suggestions; \`--manifest\` emits an approval manifest instead of the report
22007
+ - \`devrouter workspace cleanup [--repo .] [--inactive-for 30d] [--check-merged] [--measure-size] [--all-worktrees] [--manifest [--action reclaim|trim|leftover] [--output <file>]] [--json]\`: report-only cleanup evidence and exact guarded suggestions; \`--all-worktrees\` also lists Docker leftovers of deleted checkouts; \`--manifest\` emits an approval manifest instead of the report
21983
22008
  - \`devrouter workspace stop <workspace|branch>\`: stop DevPod and routes; preserve checkout, owner record, and data
21984
22009
  - \`devrouter workspace down <workspace|branch> [--keep-worktree]\`: delete runtime/routes and optionally remove the clean worktree and record
21985
22010
  - \`devrouter workspace gc [--json] [--yes]\`: report missing owners by default; apply exact eligible cleanup with \`--yes\`
21986
22011
  - \`devrouter workspace manifest hash|select <file>\`: print a manifest's SHA-256, or a canonical subset of exact candidate IDs (\`--expected-sha256\`, \`--id\`)
21987
22012
  - \`devrouter workspace reclaim|trim --manifest <file> --sha256 <hash> [--veto-command <abs>] [--yes] [--json]\`: apply an approved manifest; dry run without \`--yes\`
22013
+ - \`devrouter workspace leftover --manifest <file> --sha256 <hash> [--yes] [--json]\`: remove the Docker containers, volumes and networks of an approved leftover manifest; dry run without \`--yes\`
21988
22014
 
21989
22015
  ## Agent harness gating
21990
22016
 
@@ -22461,8 +22487,8 @@ var init_ai_prompt = __esm({
22461
22487
  purpose: "List owner, Git, workspace runtime, route, path, and branch evidence for managed workspaces."
22462
22488
  },
22463
22489
  {
22464
- command: "devrouter workspace cleanup [--repo <path>] [--inactive-for 30d] [--check-merged] [--measure-size] [--all-worktrees] [--manifest [--action reclaim|trim] [--output <file>]] [--json]",
22465
- purpose: "Report-only evidence for managed linked workspaces; activity is advisory, --check-merged alone enables read-only origin/forge checks, --all-worktrees adds RECLAIM/KEEP/PRUNE verdicts for every linked worktree, and --manifest emits an approval manifest instead of the report."
22490
+ command: "devrouter workspace cleanup [--repo <path>] [--inactive-for 30d] [--check-merged] [--measure-size] [--all-worktrees] [--manifest [--action reclaim|trim|leftover] [--output <file>]] [--json]",
22491
+ purpose: "Report-only evidence for managed linked workspaces; activity is advisory, --check-merged alone enables read-only origin/forge checks, --all-worktrees adds RECLAIM/KEEP/PRUNE verdicts for every linked worktree and lists Docker leftovers of deleted checkouts, and --manifest emits an approval manifest instead of the report."
22466
22492
  },
22467
22493
  {
22468
22494
  command: "devrouter workspace stop <workspace|branch>",
@@ -22480,6 +22506,10 @@ var init_ai_prompt = __esm({
22480
22506
  command: "devrouter workspace reclaim|trim --manifest <file> --sha256 <hash> [--veto-command <abs>] [--yes] [--json]",
22481
22507
  purpose: "Apply a person-approved manifest by its SHA-256; dry run without --yes, never adds targets, and re-checks each target inside its lifecycle lock."
22482
22508
  },
22509
+ {
22510
+ command: "devrouter workspace leftover --manifest <file> --sha256 <hash> [--yes] [--json]",
22511
+ purpose: "Remove the Docker containers, volumes and networks of a person-approved leftover manifest; dry run without --yes, and each stack is re-derived inside the provider locks and skipped if it changed."
22512
+ },
22483
22513
  {
22484
22514
  command: "devrouter workspace journal settle [path] [--json]",
22485
22515
  purpose: "Settle a lifecycle operation whose worker is provably gone as unobservable so ensure/stop can proceed; never hand-edit the devrouter state directory."
@@ -22661,6 +22691,48 @@ ${renderTable(
22661
22691
  if (!report.checkMerged) {
22662
22692
  process.stdout.write("Forge not checked: rerun with --check-merged for RECLAIM verdicts.\n");
22663
22693
  }
22694
+ printDockerLeftovers(report);
22695
+ }
22696
+ function printDockerLeftovers(report) {
22697
+ const leftovers = report.dockerLeftovers;
22698
+ if (!leftovers) return;
22699
+ if (leftovers.status === "unavailable") {
22700
+ process.stdout.write(`
22701
+ Docker leftovers: not checked (${leftovers.reason}).
22702
+ `);
22703
+ return;
22704
+ }
22705
+ const rows2 = [
22706
+ ...leftovers.leftovers.map((entry) => [
22707
+ entry.project,
22708
+ entry.checkout,
22709
+ `removable: ${entry.containers.length} container(s), ${entry.volumes.length} volume(s), ${entry.networks.length} network(s)`
22710
+ ]),
22711
+ ...leftovers.blocked.map((entry) => [
22712
+ entry.project,
22713
+ entry.checkout,
22714
+ `blocked: ${entry.reason}; next: ${entry.next}`
22715
+ ])
22716
+ ];
22717
+ if (rows2.length) {
22718
+ process.stdout.write(
22719
+ `
22720
+ Docker leftovers of deleted checkouts:
22721
+ ${renderTable(["PROJECT", "CHECKOUT", "STATUS"], rows2)}
22722
+ `
22723
+ );
22724
+ }
22725
+ if (leftovers.leftovers.length) {
22726
+ process.stdout.write(
22727
+ "Approve removal with `workspace cleanup --all-worktrees --manifest --action leftover --output <file>`, then `workspace leftover`.\n"
22728
+ );
22729
+ }
22730
+ if (leftovers.unattributedVolumes.length) {
22731
+ process.stdout.write(
22732
+ `${leftovers.unattributedVolumes.length} unused Compose volume(s) cannot be traced to a checkout and are never removed by devrouter; list them with --json.
22733
+ `
22734
+ );
22735
+ }
22664
22736
  }
22665
22737
  function printReclaimReport(report, verb = "reclaim") {
22666
22738
  process.stdout.write(
@@ -23357,8 +23429,8 @@ function buildHostPortClaimsCheck(repoPath, workspace) {
23357
23429
  profile: runtime.resolvedProfile,
23358
23430
  linked
23359
23431
  });
23360
- const gitCommonDir = linked ? resolveGitCommonDir(repoPath) : void 0;
23361
- const workspaceEnv = linked && workspace && gitCommonDir ? { token: workspace, gitCommonDir } : void 0;
23432
+ const gitCommonDir2 = linked ? resolveGitCommonDir(repoPath) : void 0;
23433
+ const workspaceEnv = linked && workspace && gitCommonDir2 ? { token: workspace, gitCommonDir: gitCommonDir2 } : void 0;
23362
23434
  const conflicts = detectHostPortClaimConflicts({ repoPath, plan, workspace: workspaceEnv });
23363
23435
  if (conflicts.length === 0) {
23364
23436
  return {
@@ -24134,7 +24206,7 @@ function probeInstalledCliVersion(executable) {
24134
24206
  }
24135
24207
  function buildCliPathCheck(options = {}) {
24136
24208
  const id5 = "global.cli-path";
24137
- const runningVersion = options.runningVersion ?? (true ? "0.2.0" : "0.0.0-dev");
24209
+ const runningVersion = options.runningVersion ?? (true ? "0.3.0" : "0.0.0-dev");
24138
24210
  if (runningVersion === "0.0.0-dev") {
24139
24211
  return {
24140
24212
  id: id5,
@@ -24155,12 +24227,12 @@ function buildCliPathCheck(options = {}) {
24155
24227
  if (!isExecutableFile(executable)) {
24156
24228
  continue;
24157
24229
  }
24158
- const resolved = realpathOrUndefined(executable) ?? executable;
24159
- if (seen.has(resolved)) {
24230
+ const resolved2 = realpathOrUndefined(executable) ?? executable;
24231
+ if (seen.has(resolved2)) {
24160
24232
  continue;
24161
24233
  }
24162
- seen.add(resolved);
24163
- const running = runningEntry !== void 0 && resolved === runningEntry;
24234
+ seen.add(resolved2);
24235
+ const running = runningEntry !== void 0 && resolved2 === runningEntry;
24164
24236
  entries.push({
24165
24237
  executable,
24166
24238
  running,
@@ -25030,7 +25102,7 @@ async function buildDoctorReport(options = {}) {
25030
25102
  suggestion: `Remedy ${expansionNotice.remedy.code}: set a named default profile other than '${expansionNotice.profile}'${remedyCandidates}.`
25031
25103
  });
25032
25104
  }
25033
- const cliVersion = true ? "0.2.0" : "0.0.0-dev";
25105
+ const cliVersion = true ? "0.3.0" : "0.0.0-dev";
25034
25106
  const configVersion = config.devrouter?.version;
25035
25107
  const installedKnown = cliVersion !== "0.0.0-dev";
25036
25108
  const versionComparison = installedKnown && configVersion ? compareSemver(configVersion, cliVersion) : void 0;
@@ -25689,11 +25761,11 @@ var init_up = __esm({
25689
25761
 
25690
25762
  // src/commands/environment-path.ts
25691
25763
  function resolveGitCheckoutPath(repoPath) {
25692
- const resolved = resolveRepoPath(repoPath);
25764
+ const resolved2 = resolveRepoPath(repoPath);
25693
25765
  try {
25694
- return resolveGitTopLevel(resolved);
25766
+ return resolveGitTopLevel(resolved2);
25695
25767
  } catch (error) {
25696
- throw new Error(`Environment commands require a Git repository: '${resolved}'.`, {
25768
+ throw new Error(`Environment commands require a Git repository: '${resolved2}'.`, {
25697
25769
  cause: error
25698
25770
  });
25699
25771
  }
@@ -28714,8 +28786,8 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
28714
28786
  function runDockerComposeUp(repoPath, composeFiles2, overlayPath, services) {
28715
28787
  const fileArgs = [];
28716
28788
  for (const composeFile of composeFiles2) {
28717
- const resolved = assertPathWithinRepo(composeFile, repoPath, "composeFiles");
28718
- fileArgs.push("-f", resolved);
28789
+ const resolved2 = assertPathWithinRepo(composeFile, repoPath, "composeFiles");
28790
+ fileArgs.push("-f", resolved2);
28719
28791
  }
28720
28792
  const args = ["compose", ...fileArgs, "-f", overlayPath, "up", "-d", "--wait", ...services];
28721
28793
  const result = (0, import_node_child_process36.spawnSync)("docker", args, {
@@ -28732,8 +28804,8 @@ function runDockerComposeUp(repoPath, composeFiles2, overlayPath, services) {
28732
28804
  function queryRunningComposeServices(repoPath, composeFiles2, overlayPath, services) {
28733
28805
  const fileArgs = [];
28734
28806
  for (const composeFile of composeFiles2) {
28735
- const resolved = assertPathWithinRepo(composeFile, repoPath, "composeFiles");
28736
- fileArgs.push("-f", resolved);
28807
+ const resolved2 = assertPathWithinRepo(composeFile, repoPath, "composeFiles");
28808
+ fileArgs.push("-f", resolved2);
28737
28809
  }
28738
28810
  const args = [
28739
28811
  "compose",
@@ -28768,8 +28840,8 @@ function queryRunningComposeServices(repoPath, composeFiles2, overlayPath, servi
28768
28840
  function runDockerComposeStop(repoPath, composeFiles2, overlayPath, services) {
28769
28841
  const fileArgs = [];
28770
28842
  for (const composeFile of composeFiles2) {
28771
- const resolved = assertPathWithinRepo(composeFile, repoPath, "composeFiles");
28772
- fileArgs.push("-f", resolved);
28843
+ const resolved2 = assertPathWithinRepo(composeFile, repoPath, "composeFiles");
28844
+ fileArgs.push("-f", resolved2);
28773
28845
  }
28774
28846
  const args = ["compose", ...fileArgs, "-f", overlayPath, "stop", ...services];
28775
28847
  const result = (0, import_node_child_process36.spawnSync)("docker", args, {
@@ -28785,8 +28857,8 @@ function runDockerComposeStop(repoPath, composeFiles2, overlayPath, services) {
28785
28857
  function runDockerComposeLogs(repoPath, composeFiles2, overlayPath, services, tail = 20) {
28786
28858
  const fileArgs = [];
28787
28859
  for (const composeFile of composeFiles2) {
28788
- const resolved = assertPathWithinRepo(composeFile, repoPath, "composeFiles");
28789
- fileArgs.push("-f", resolved);
28860
+ const resolved2 = assertPathWithinRepo(composeFile, repoPath, "composeFiles");
28861
+ fileArgs.push("-f", resolved2);
28790
28862
  }
28791
28863
  const args = [
28792
28864
  "compose",
@@ -28806,8 +28878,8 @@ function runDockerComposeLogs(repoPath, composeFiles2, overlayPath, services, ta
28806
28878
  function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internalPort) {
28807
28879
  const fileArgs = [];
28808
28880
  for (const composeFile of composeFiles2) {
28809
- const resolved = assertPathWithinRepo(composeFile, repoPath, "composeFiles");
28810
- fileArgs.push("-f", resolved);
28881
+ const resolved2 = assertPathWithinRepo(composeFile, repoPath, "composeFiles");
28882
+ fileArgs.push("-f", resolved2);
28811
28883
  }
28812
28884
  const args = ["compose", ...fileArgs, "-f", overlayPath, "port", service, String(internalPort)];
28813
28885
  const result = (0, import_node_child_process36.spawnSync)("docker", args, {
@@ -29647,133 +29719,6 @@ var init_tls2 = __esm({
29647
29719
  }
29648
29720
  });
29649
29721
 
29650
- // src/core/workspace-consumption.ts
29651
- function measureWorktreeConsumption(worktreePath, options) {
29652
- const deadlineMs = options?.deadlineMs ?? DEFAULT_DEADLINE_MS;
29653
- const startedAt = Date.now();
29654
- let rootStat;
29655
- try {
29656
- rootStat = import_node_fs53.default.lstatSync(worktreePath);
29657
- } catch (error) {
29658
- return { status: "unknown", reason: describeError(error, worktreePath) };
29659
- }
29660
- let rootEntries;
29661
- try {
29662
- rootEntries = import_node_fs53.default.readdirSync(worktreePath, { withFileTypes: true });
29663
- } catch (error) {
29664
- return { status: "unknown", reason: describeError(error, worktreePath) };
29665
- }
29666
- const seenInodes = /* @__PURE__ */ new Set();
29667
- let bytes2 = 0;
29668
- let timedOut = false;
29669
- let unreadableReason = null;
29670
- const accumulate = (stat) => {
29671
- if (stat.nlink > 1) {
29672
- const key = `${stat.dev}:${stat.ino}`;
29673
- if (seenInodes.has(key)) return;
29674
- seenInodes.add(key);
29675
- }
29676
- bytes2 += stat.blocks * BLOCK_SIZE_BYTES;
29677
- };
29678
- const deadlineExceeded = () => Date.now() - startedAt >= deadlineMs;
29679
- accumulate(rootStat);
29680
- const stack = [{ dirPath: worktreePath, entries: rootEntries }];
29681
- while (stack.length > 0 && !timedOut && !unreadableReason) {
29682
- const { dirPath, entries } = stack.pop();
29683
- for (const entry of entries) {
29684
- if (deadlineExceeded()) {
29685
- timedOut = true;
29686
- break;
29687
- }
29688
- const entryPath = import_node_path66.default.join(dirPath, entry.name);
29689
- let entryStat;
29690
- try {
29691
- entryStat = import_node_fs53.default.lstatSync(entryPath);
29692
- } catch (error) {
29693
- if (error?.code === "ENOENT") continue;
29694
- unreadableReason = describeIncompleteWalk(error);
29695
- break;
29696
- }
29697
- accumulate(entryStat);
29698
- if (!entryStat.isDirectory()) continue;
29699
- let childEntries;
29700
- try {
29701
- childEntries = import_node_fs53.default.readdirSync(entryPath, { withFileTypes: true });
29702
- } catch (error) {
29703
- unreadableReason = describeIncompleteWalk(error);
29704
- break;
29705
- }
29706
- stack.push({ dirPath: entryPath, entries: childEntries });
29707
- }
29708
- }
29709
- if (timedOut) {
29710
- return { status: "unknown", reason: `exceeded deadline of ${deadlineMs}ms` };
29711
- }
29712
- if (unreadableReason) {
29713
- return { status: "unknown", reason: unreadableReason };
29714
- }
29715
- return { status: "measured", bytes: bytes2 };
29716
- }
29717
- function describeIncompleteWalk(error) {
29718
- return `could not read every path inside the worktree: ${error?.message ?? String(error)}`;
29719
- }
29720
- function measureContainerConsumption(worktreePaths, dependencies) {
29721
- const byWorktree = /* @__PURE__ */ new Map();
29722
- if (worktreePaths.length === 0) return byWorktree;
29723
- const inspect = dependencies?.inspect ?? inspectWorkspaceContainers;
29724
- const containers2 = inspect();
29725
- const attributedIds = /* @__PURE__ */ new Set();
29726
- for (const worktreePath of worktreePaths) {
29727
- for (const container of workspaceAppContainers(containers2, worktreePath)) {
29728
- attributedIds.add(container.id);
29729
- }
29730
- }
29731
- const sized = attributedIds.size === 0 ? [] : inspect({ withSize: true, ids: Array.from(attributedIds) });
29732
- for (const worktreePath of worktreePaths) {
29733
- byWorktree.set(worktreePath, summarizeContainers(workspaceAppContainers(sized, worktreePath)));
29734
- }
29735
- return byWorktree;
29736
- }
29737
- function summarizeContainers(containers2) {
29738
- let writable = 0;
29739
- let shared = 0;
29740
- for (const container of containers2) {
29741
- const { sizeRw, sizeRootFs } = container;
29742
- if (typeof sizeRw !== "number" || typeof sizeRootFs !== "number" || !Number.isFinite(sizeRw) || !Number.isFinite(sizeRootFs) || sizeRootFs < sizeRw) {
29743
- const unknown = {
29744
- status: "unknown",
29745
- reason: `container ${container.id.slice(0, 12)} reported no usable size`
29746
- };
29747
- return { containerWritable: unknown, imageShared: unknown };
29748
- }
29749
- writable += sizeRw;
29750
- shared += sizeRootFs - sizeRw;
29751
- }
29752
- return {
29753
- containerWritable: { status: "measured", bytes: writable },
29754
- imageShared: { status: "measured", bytes: shared }
29755
- };
29756
- }
29757
- function describeError(error, worktreePath) {
29758
- const code = error?.code;
29759
- if (code === "ENOENT") return `worktree path '${worktreePath}' does not exist`;
29760
- if (code === "EACCES" || code === "EPERM") {
29761
- return `permission denied reading worktree path '${worktreePath}'`;
29762
- }
29763
- return `could not stat worktree path '${worktreePath}': ${error?.message ?? String(error)}`;
29764
- }
29765
- var import_node_fs53, import_node_path66, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
29766
- var init_workspace_consumption = __esm({
29767
- "src/core/workspace-consumption.ts"() {
29768
- "use strict";
29769
- import_node_fs53 = __toESM(require("fs"));
29770
- import_node_path66 = __toESM(require("path"));
29771
- init_devpod_environment();
29772
- DEFAULT_DEADLINE_MS = 1e4;
29773
- BLOCK_SIZE_BYTES = 512;
29774
- }
29775
- });
29776
-
29777
29722
  // src/core/worktree-reclaim.ts
29778
29723
  function sortKeys(value) {
29779
29724
  if (Array.isArray(value)) return value.map(sortKeys);
@@ -29854,7 +29799,7 @@ function validateCandidate(value, index, action2, legacy) {
29854
29799
  }
29855
29800
  }
29856
29801
  for (const key of ["repo", "path", "commonDir", "gitDir"]) {
29857
- if (!import_node_path67.default.isAbsolute(candidate[key])) {
29802
+ if (!import_node_path66.default.isAbsolute(candidate[key])) {
29858
29803
  throw new Error(`${label2}.${key} must be an absolute path`);
29859
29804
  }
29860
29805
  }
@@ -29960,7 +29905,7 @@ function validateManifest(value) {
29960
29905
  return root;
29961
29906
  }
29962
29907
  function readManifest(file) {
29963
- const raw = import_node_fs54.default.readFileSync(file);
29908
+ const raw = import_node_fs53.default.readFileSync(file);
29964
29909
  let parsed;
29965
29910
  try {
29966
29911
  parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(raw));
@@ -30007,7 +29952,7 @@ function gitLine(cwd, args) {
30007
29952
  return result.status === 0 && !result.error && output2 ? output2 : void 0;
30008
29953
  }
30009
29954
  function readWorktreeIdentity(worktreePath) {
30010
- if (!import_node_fs54.default.existsSync(worktreePath)) return void 0;
29955
+ if (!import_node_fs53.default.existsSync(worktreePath)) return void 0;
30011
29956
  const real = comparableWorkspacePath(worktreePath);
30012
29957
  const commonDir = gitLine(real, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
30013
29958
  const gitDir = gitLine(real, ["rev-parse", "--path-format=absolute", "--git-dir"]);
@@ -30027,13 +29972,13 @@ function identityMismatch(expected, actual) {
30027
29972
  function unmanagedActivityEpoch(worktreePath, gitDir) {
30028
29973
  let latest;
30029
29974
  for (const file of [
30030
- import_node_path67.default.join(gitDir, "index"),
30031
- import_node_path67.default.join(gitDir, "HEAD"),
30032
- import_node_path67.default.join(gitDir, "logs", "HEAD"),
29975
+ import_node_path66.default.join(gitDir, "index"),
29976
+ import_node_path66.default.join(gitDir, "HEAD"),
29977
+ import_node_path66.default.join(gitDir, "logs", "HEAD"),
30033
29978
  worktreePath
30034
29979
  ]) {
30035
29980
  try {
30036
- const seconds = Math.floor(import_node_fs54.default.statSync(file).mtimeMs / 1e3);
29981
+ const seconds = Math.floor(import_node_fs53.default.statSync(file).mtimeMs / 1e3);
30037
29982
  latest = latest === void 0 ? seconds : Math.max(latest, seconds);
30038
29983
  } catch {
30039
29984
  }
@@ -30087,12 +30032,12 @@ function buildReclaimManifest(report, createdAtEpoch) {
30087
30032
  return canonicalJson(manifest);
30088
30033
  }
30089
30034
  function receiptPath(commonDir, manifestSha256, id5) {
30090
- return import_node_path67.default.join(commonDir, "worktree-reclaim", "receipts", manifestSha256, `${id5}.json`);
30035
+ return import_node_path66.default.join(commonDir, "worktree-reclaim", "receipts", manifestSha256, `${id5}.json`);
30091
30036
  }
30092
30037
  function matchingReceipt(candidate, manifestSha256) {
30093
30038
  try {
30094
30039
  const receipt = JSON.parse(
30095
- import_node_fs54.default.readFileSync(receiptPath(candidate.commonDir, manifestSha256, candidate.id), "utf-8")
30040
+ import_node_fs53.default.readFileSync(receiptPath(candidate.commonDir, manifestSha256, candidate.id), "utf-8")
30096
30041
  );
30097
30042
  return receipt.kind === RECEIPT_KIND && receipt.schemaVersion === SCHEMA_VERSION && receipt.manifestSha256 === manifestSha256 && receipt.candidateId === candidate.id && receipt.path === candidate.path && receipt.head === candidate.head;
30098
30043
  } catch {
@@ -30101,7 +30046,7 @@ function matchingReceipt(candidate, manifestSha256) {
30101
30046
  }
30102
30047
  function writeReceipt(candidate, manifestSha256, steps, nowEpoch) {
30103
30048
  const file = receiptPath(candidate.commonDir, manifestSha256, candidate.id);
30104
- import_node_fs54.default.mkdirSync(import_node_path67.default.dirname(file), { recursive: true });
30049
+ import_node_fs53.default.mkdirSync(import_node_path66.default.dirname(file), { recursive: true });
30105
30050
  writeFileAtomically(
30106
30051
  file,
30107
30052
  canonicalJson({
@@ -30132,9 +30077,9 @@ function runVetoCommand(command, worktreePath) {
30132
30077
  return result.status === 0 && !result.error && result.signal === null;
30133
30078
  }
30134
30079
  function assertVetoCommand(command) {
30135
- if (!import_node_path67.default.isAbsolute(command)) throw new Error("--veto-command must be an absolute path");
30080
+ if (!import_node_path66.default.isAbsolute(command)) throw new Error("--veto-command must be an absolute path");
30136
30081
  try {
30137
- import_node_fs54.default.accessSync(command, import_node_fs54.default.constants.X_OK);
30082
+ import_node_fs53.default.accessSync(command, import_node_fs53.default.constants.X_OK);
30138
30083
  } catch {
30139
30084
  throw new Error(`--veto-command '${command}' is missing or not executable`);
30140
30085
  }
@@ -30216,7 +30161,7 @@ async function applyReclaimManifest(options, dependencies) {
30216
30161
  outcome.status = "not-attempted";
30217
30162
  continue;
30218
30163
  }
30219
- if (!import_node_fs54.default.existsSync(candidate.path) && matchingReceipt(candidate, manifestSha256)) {
30164
+ if (!import_node_fs53.default.existsSync(candidate.path) && matchingReceipt(candidate, manifestSha256)) {
30220
30165
  outcome.status = "already-done";
30221
30166
  continue;
30222
30167
  }
@@ -30285,14 +30230,14 @@ async function applyReclaimManifest(options, dependencies) {
30285
30230
  }
30286
30231
  return { manifestSha256, dryRun: !options.yes, targets, stoppedEarly };
30287
30232
  }
30288
- var import_node_child_process38, import_node_crypto30, import_node_fs54, import_node_path67, MANIFEST_KIND, LEGACY_MANIFEST_KIND, RECEIPT_KIND, SCHEMA_VERSION, HEX_SHA, HEX_ID, VETO_TIMEOUT_MS, FUTURE_TOLERANCE_SECONDS, ROOT_KEYS, SESSION_ROOT_KEYS, LEGACY_ROOT_KEYS, CANDIDATE_KEYS, SESSION_CANDIDATE_KEYS, OPTIONAL_CANDIDATE_KEYS, ReclaimSkip;
30233
+ var import_node_child_process38, import_node_crypto30, import_node_fs53, import_node_path66, MANIFEST_KIND, LEGACY_MANIFEST_KIND, RECEIPT_KIND, SCHEMA_VERSION, HEX_SHA, HEX_ID, VETO_TIMEOUT_MS, FUTURE_TOLERANCE_SECONDS, ROOT_KEYS, SESSION_ROOT_KEYS, LEGACY_ROOT_KEYS, CANDIDATE_KEYS, SESSION_CANDIDATE_KEYS, OPTIONAL_CANDIDATE_KEYS, ReclaimSkip;
30289
30234
  var init_worktree_reclaim = __esm({
30290
30235
  "src/core/worktree-reclaim.ts"() {
30291
30236
  "use strict";
30292
30237
  import_node_child_process38 = require("child_process");
30293
30238
  import_node_crypto30 = require("crypto");
30294
- import_node_fs54 = __toESM(require("fs"));
30295
- import_node_path67 = __toESM(require("path"));
30239
+ import_node_fs53 = __toESM(require("fs"));
30240
+ import_node_path66 = __toESM(require("path"));
30296
30241
  init_atomic_file();
30297
30242
  init_workspace();
30298
30243
  MANIFEST_KIND = "worktree-reclaim-manifest";
@@ -30339,72 +30284,801 @@ var init_worktree_reclaim = __esm({
30339
30284
  }
30340
30285
  });
30341
30286
 
30342
- // src/core/workspace-cleanup.ts
30343
- function isRecord9(value) {
30344
- return value !== null && typeof value === "object" && !Array.isArray(value);
30287
+ // src/core/docker-leftovers.ts
30288
+ function dockerOutput(run, args) {
30289
+ const result = run(args);
30290
+ if (result.status !== 0) {
30291
+ throw new Error(`docker ${args[0]} failed: ${result.stderr.trim() || "unknown error"}`);
30292
+ }
30293
+ return result.stdout;
30345
30294
  }
30346
- function isSha2(value) {
30347
- return typeof value === "string" && /^[0-9a-f]{40,64}$/i.test(value);
30295
+ function listIds(run, args, kind) {
30296
+ const ids2 = dockerOutput(run, args).split("\n").map((line) => line.trim()).filter(Boolean);
30297
+ if (ids2.length > MAX_DOCKER_OBJECTS) {
30298
+ throw new DockerListingBoundError(
30299
+ `docker lists ${ids2.length} ${kind}; the leftover report reads at most ${MAX_DOCKER_OBJECTS}`
30300
+ );
30301
+ }
30302
+ return ids2;
30348
30303
  }
30349
- function parseTimestamp(value) {
30350
- if (typeof value !== "string") return void 0;
30351
- const timestamp = Date.parse(value);
30352
- return Number.isFinite(timestamp) ? timestamp : void 0;
30304
+ function inspectAll(run, command, ids2) {
30305
+ const objects = [];
30306
+ for (let start = 0; start < ids2.length; start += 200) {
30307
+ const parsed = JSON.parse(dockerOutput(run, [...command, ...ids2.slice(start, start + 200)]));
30308
+ if (!Array.isArray(parsed)) throw new Error(`docker ${command.join(" ")} returned no array`);
30309
+ objects.push(...parsed);
30310
+ }
30311
+ return objects;
30353
30312
  }
30354
- function validTimestamp(value) {
30355
- return parseTimestamp(value) !== void 0;
30313
+ function labelsOf(value) {
30314
+ if (!value || typeof value !== "object") return {};
30315
+ return Object.fromEntries(
30316
+ Object.entries(value).filter(
30317
+ (entry) => typeof entry[1] === "string"
30318
+ )
30319
+ );
30356
30320
  }
30357
- function successfulOutput(result) {
30358
- if (result.status !== 0 || result.error) return void 0;
30359
- const output2 = result.stdout.trim();
30360
- return output2.length > 0 ? output2 : void 0;
30321
+ function readDockerInventory(run = defaultDockerRunner) {
30322
+ const daemonId = dockerOutput(run, ["info", "--format", "{{.ID}}"]).trim();
30323
+ if (!daemonId) throw new Error("docker info returned no daemon ID");
30324
+ const containerIds = listIds(run, ["ps", "-a", "-q", "--no-trunc"], "containers");
30325
+ const containers2 = inspectAll(run, ["container", "inspect"], containerIds).map((raw) => {
30326
+ const entry = raw;
30327
+ return {
30328
+ id: entry.Id,
30329
+ name: (entry.Name ?? "").replace(/^\//, ""),
30330
+ state: entry.State?.Status ?? "unknown",
30331
+ labels: labelsOf(entry.Config?.Labels),
30332
+ volumes: (entry.Mounts ?? []).filter((mount) => mount.Type === "volume" && mount.Name).map((mount) => mount.Name)
30333
+ };
30334
+ });
30335
+ const volumeNames = listIds(run, ["volume", "ls", "-q"], "volumes");
30336
+ const volumes = inspectAll(run, ["volume", "inspect"], volumeNames).map((raw) => {
30337
+ const entry = raw;
30338
+ return { name: entry.Name, labels: labelsOf(entry.Labels), createdAt: entry.CreatedAt ?? "" };
30339
+ });
30340
+ const networkIds = listIds(
30341
+ run,
30342
+ ["network", "ls", "-q", "--no-trunc", "--filter", `label=${PROJECT_LABEL}`],
30343
+ "networks"
30344
+ );
30345
+ const networks = inspectAll(run, ["network", "inspect"], networkIds).map((raw) => {
30346
+ const entry = raw;
30347
+ return {
30348
+ id: entry.Id,
30349
+ name: entry.Name,
30350
+ labels: labelsOf(entry.Labels),
30351
+ attached: Object.keys(entry.Containers ?? {}).length
30352
+ };
30353
+ });
30354
+ return { daemonId, containers: containers2, volumes, networks };
30361
30355
  }
30362
- function gitOutput(repoPath, args, commandRunner) {
30363
- return successfulOutput(commandRunner("git", ["-C", repoPath, ...args]));
30356
+ function checkoutOf(workingDir) {
30357
+ return workingDir.replace(/\/+$/, "").replace(/\/\.devcontainer$/, "");
30364
30358
  }
30365
- function quoteCommandArg(value) {
30366
- return /^[a-zA-Z0-9_./:@+-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
30359
+ function isInsideOrEqual(parent, child) {
30360
+ const relative2 = import_node_path67.default.relative(parent, child);
30361
+ return relative2 === "" || !relative2.startsWith("..") && !import_node_path67.default.isAbsolute(relative2);
30367
30362
  }
30368
- function workspaceCommand(repoPath, args, beforeRepo = [], afterRepo = []) {
30369
- return [
30370
- "devrouter",
30371
- "workspace",
30372
- ...args,
30373
- ...beforeRepo,
30374
- "--repo",
30375
- quoteCommandArg(repoPath),
30376
- ...afterRepo
30377
- ].join(" ");
30363
+ function isAnonymousVolume(volume) {
30364
+ return /^[0-9a-f]{64}$/.test(volume.name) && !volume.labels[PROJECT_LABEL];
30378
30365
  }
30379
- function parseInactiveFor(value = DEFAULT_INACTIVE_FOR) {
30380
- const match = /^(?<amount>[1-9]\d*)(?<unit>[smhdw])$/.exec(value);
30381
- if (!match?.groups) {
30382
- throw new Error(
30383
- "--inactive-for must be a positive integer followed by s, m, h, d, or w (for example 30d)."
30384
- );
30385
- }
30386
- const amount = Number(match.groups.amount);
30387
- const multiplier = { s: 1, m: 60, h: 3600, d: 86400, w: 604800 }[match.groups.unit];
30388
- const seconds = amount * multiplier;
30389
- if (!Number.isSafeInteger(seconds)) {
30390
- throw new Error("--inactive-for is too large.");
30366
+ function vanishedInsideTrees(checkout, context) {
30367
+ const treesReal = context.realpath(context.treesPath);
30368
+ if (!treesReal) return "out-of-scope";
30369
+ const lexical = [context.treesPath, treesReal].some(
30370
+ (prefix) => checkout !== prefix && isInsideOrEqual(prefix, checkout)
30371
+ );
30372
+ if (!lexical || context.exists(checkout)) return "out-of-scope";
30373
+ let ancestor = import_node_path67.default.dirname(checkout);
30374
+ while (!context.exists(ancestor)) {
30375
+ const parent = import_node_path67.default.dirname(ancestor);
30376
+ if (parent === ancestor) return "out-of-scope";
30377
+ ancestor = parent;
30378
+ }
30379
+ const ancestorReal = context.realpath(ancestor);
30380
+ if (ancestorReal === void 0 || !isInsideOrEqual(treesReal, ancestorReal)) {
30381
+ return "out-of-scope";
30382
+ }
30383
+ for (let current = ancestor; ; current = import_node_path67.default.dirname(current)) {
30384
+ const real = context.realpath(current);
30385
+ if (real === void 0 || real === treesReal || !isInsideOrEqual(treesReal, real)) break;
30386
+ if (context.exists(import_node_path67.default.join(current, ".git"))) return "inside-worktree";
30387
+ }
30388
+ return "vanished";
30389
+ }
30390
+ function resolved(file, context) {
30391
+ const real = context.realpath(file);
30392
+ return real && real !== import_node_path67.default.resolve(file) ? [import_node_path67.default.resolve(file), real] : [import_node_path67.default.resolve(file)];
30393
+ }
30394
+ function relation(entry, checkout, context) {
30395
+ const entries = resolved(entry, context);
30396
+ const checkouts = resolved(checkout, context);
30397
+ if (entries.some((left) => checkouts.includes(left))) return "same";
30398
+ const trees = resolved(context.treesPath, context);
30399
+ const belowTrees = (left) => trees.some((root) => left !== root && isInsideOrEqual(root, left));
30400
+ if (entries.some(
30401
+ (left) => belowTrees(left) && checkouts.some((right) => isInsideOrEqual(left, right))
30402
+ )) {
30403
+ return "contains";
30391
30404
  }
30392
- return { input: value, seconds };
30405
+ return void 0;
30393
30406
  }
30394
- function evaluateWorkspaceActivity(evidence, cutoff) {
30395
- const cutoffTime = parseTimestamp(cutoff);
30396
- if (cutoffTime === void 0) {
30397
- throw new Error(`Invalid activity cutoff '${cutoff}'.`);
30407
+ function findDockerLeftovers(inventory, context) {
30408
+ const byProject = /* @__PURE__ */ new Map();
30409
+ for (const container of inventory.containers) {
30410
+ const project = container.labels[PROJECT_LABEL];
30411
+ if (!project) continue;
30412
+ byProject.set(project, [...byProject.get(project) ?? [], container]);
30413
+ }
30414
+ const leftovers = [];
30415
+ const blocked3 = [];
30416
+ for (const [project, containers2] of [...byProject].sort(([a], [b]) => byteOrder(a, b))) {
30417
+ const checkouts = new Set(
30418
+ containers2.filter((container) => container.labels[WORKING_DIR_LABEL]).map((container) => checkoutOf(container.labels[WORKING_DIR_LABEL]))
30419
+ );
30420
+ if (checkouts.size !== 1) continue;
30421
+ if (!containers2.some((container) => container.labels[PROVIDER_LABEL])) continue;
30422
+ const [checkout] = checkouts;
30423
+ const scope = vanishedInsideTrees(checkout, context);
30424
+ if (scope === "out-of-scope") continue;
30425
+ const block = (reason, next) => blocked3.push({ project, checkout, reason, next });
30426
+ if (scope === "inside-worktree") {
30427
+ block("the missing folder lies inside an existing checkout", "investigate");
30428
+ continue;
30429
+ }
30430
+ if (!context.registriesReadable) {
30431
+ block("a provider registry could not be read", "investigate");
30432
+ continue;
30433
+ }
30434
+ const registered = (paths) => paths.map((entry) => relation(entry, checkout, context));
30435
+ const all = [
30436
+ ...registered(context.worktreePaths),
30437
+ ...registered([...context.ownershipPaths, ...context.providerPaths])
30438
+ ];
30439
+ if (all.includes("contains")) {
30440
+ block("a registered checkout contains the missing folder", "investigate");
30441
+ continue;
30442
+ }
30443
+ if (registered(context.worktreePaths).includes("same")) {
30444
+ block("Git still registers the vanished worktree", "git worktree prune");
30445
+ continue;
30446
+ }
30447
+ if (all.includes("same")) {
30448
+ block("a devrouter record or provider registration still names the checkout", "workspace gc");
30449
+ continue;
30450
+ }
30451
+ const running = containers2.filter((container) => !STOPPED_STATES.has(container.state));
30452
+ if (running.length) {
30453
+ block(`${running.length} container(s) are not stopped`, "stop the stack");
30454
+ continue;
30455
+ }
30456
+ leftovers.push(removableSet(project, checkout, containers2, inventory));
30398
30457
  }
30399
- const validEvidence = evidence.filter(
30400
- (entry) => entry.status === "valid" && validTimestamp(entry.timestamp)
30401
- );
30402
- const latestTime = validEvidence.reduce(
30403
- (latest, entry) => Math.max(
30404
- latest ?? Number.NEGATIVE_INFINITY,
30405
- parseTimestamp(entry.timestamp) ?? Number.NEGATIVE_INFINITY
30406
- ),
30407
- void 0
30458
+ return {
30459
+ daemonId: inventory.daemonId,
30460
+ leftovers,
30461
+ blocked: blocked3,
30462
+ unattributedVolumes: unattributedVolumes(inventory, byProject)
30463
+ };
30464
+ }
30465
+ function removableSet(project, checkout, containers2, inventory) {
30466
+ const own = new Set(containers2.map((container) => container.id));
30467
+ const mountedHere = new Set(containers2.flatMap((container) => container.volumes));
30468
+ const mountedElsewhere = new Set(
30469
+ inventory.containers.filter((container) => !own.has(container.id)).flatMap((container) => container.volumes)
30470
+ );
30471
+ return {
30472
+ project,
30473
+ checkout,
30474
+ containers: containers2.map(({ id: id5, name, state }) => ({ id: id5, name, state })).sort((a, b) => byteOrder(a.id, b.id)),
30475
+ volumes: inventory.volumes.filter(
30476
+ (volume) => !mountedElsewhere.has(volume.name) && (volume.labels[PROJECT_LABEL] === project && volume.name.startsWith(`${project}_`) || isAnonymousVolume(volume) && mountedHere.has(volume.name))
30477
+ ).map(({ name, createdAt }) => ({ name, createdAt })).sort((a, b) => byteOrder(a.name, b.name)),
30478
+ networks: inventory.networks.filter(
30479
+ (network) => network.labels[PROJECT_LABEL] === project && network.name.startsWith(`${project}_`) && network.attached === 0
30480
+ ).map(({ id: id5, name }) => ({ id: id5, name })).sort((a, b) => byteOrder(a.id, b.id))
30481
+ };
30482
+ }
30483
+ function unattributedVolumes(inventory, byProject) {
30484
+ const mounted = new Set(inventory.containers.flatMap((container) => container.volumes));
30485
+ return inventory.volumes.filter(
30486
+ (volume) => volume.labels[PROJECT_LABEL] !== void 0 && !byProject.has(volume.labels[PROJECT_LABEL]) && !mounted.has(volume.name)
30487
+ ).map((volume) => ({
30488
+ name: volume.name,
30489
+ project: volume.labels[PROJECT_LABEL],
30490
+ reason: "no container of its Compose project remains, so no checkout can be proven"
30491
+ })).sort((a, b) => byteOrder(a.name, b.name));
30492
+ }
30493
+ function readLeftoverContext(repoPath) {
30494
+ resetWorkspaceRuntimeCaches();
30495
+ const snapshots = getWorkspaceRegistrySnapshots();
30496
+ return {
30497
+ treesPath: import_node_path67.default.join(repoPath, "trees"),
30498
+ worktreePaths: listGitWorktrees(repoPath).map((worktree) => worktree.path),
30499
+ ownershipPaths: listWorkspaceOwnership(repoPath).map((record5) => record5.worktreePath),
30500
+ providerPaths: listMergedRuntimeWorkspaces().map((workspace) => workspace.source.localFolder),
30501
+ registriesReadable: snapshots.unavailable.length === 0,
30502
+ realpath: (file) => {
30503
+ try {
30504
+ return import_node_fs54.default.realpathSync.native(file);
30505
+ } catch {
30506
+ return void 0;
30507
+ }
30508
+ },
30509
+ exists: (file) => {
30510
+ try {
30511
+ import_node_fs54.default.lstatSync(file);
30512
+ return true;
30513
+ } catch (error) {
30514
+ return error.code !== "ENOENT";
30515
+ }
30516
+ }
30517
+ };
30518
+ }
30519
+ function leftoverCandidateId(daemonId, leftover) {
30520
+ return sha256Hex(canonicalJson({ daemonId, ...leftover }));
30521
+ }
30522
+ function buildLeftoverManifest(repo, report, createdAtEpoch) {
30523
+ const manifest = {
30524
+ kind: LEFTOVER_MANIFEST_KIND,
30525
+ schemaVersion: SCHEMA_VERSION2,
30526
+ repo,
30527
+ daemonId: report.daemonId,
30528
+ createdAtEpoch,
30529
+ candidateCount: report.leftovers.length,
30530
+ candidates: report.leftovers.map((leftover) => ({
30531
+ ...leftover,
30532
+ id: leftoverCandidateId(report.daemonId, leftover)
30533
+ }))
30534
+ };
30535
+ validateLeftoverManifest(manifest);
30536
+ return canonicalJson(manifest);
30537
+ }
30538
+ function requireExactKeys2(value, keys2, label2) {
30539
+ const actual = Object.keys(value).sort();
30540
+ if (actual.join("\0") !== [...keys2].sort().join("\0")) {
30541
+ throw new Error(`${label2} fields must be exactly: ${[...keys2].sort().join(", ")}`);
30542
+ }
30543
+ }
30544
+ function requireString(value, label2) {
30545
+ if (typeof value !== "string" || !value || value.includes("\0")) {
30546
+ throw new Error(`${label2} must be a non-empty string`);
30547
+ }
30548
+ }
30549
+ function requireSortedUnique(values, label2) {
30550
+ const sorted = [...new Set(values)].sort(byteOrder);
30551
+ if (sorted.length !== values.length || sorted.some((entry, index) => entry !== values[index])) {
30552
+ throw new Error(`${label2} must be sorted and unique`);
30553
+ }
30554
+ }
30555
+ function isLeftoverManifest(value) {
30556
+ return Boolean(value) && typeof value === "object" && value.kind === LEFTOVER_MANIFEST_KIND;
30557
+ }
30558
+ function validateLeftoverManifest(value) {
30559
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
30560
+ throw new Error("manifest root must be an object");
30561
+ }
30562
+ const root = value;
30563
+ if (root.kind !== LEFTOVER_MANIFEST_KIND) {
30564
+ throw new Error(`manifest kind must be ${LEFTOVER_MANIFEST_KIND}`);
30565
+ }
30566
+ requireExactKeys2(
30567
+ root,
30568
+ ["candidateCount", "candidates", "createdAtEpoch", "daemonId", "kind", "repo", "schemaVersion"],
30569
+ "manifest"
30570
+ );
30571
+ if (root.schemaVersion !== SCHEMA_VERSION2) {
30572
+ throw new Error(`manifest schemaVersion must be ${SCHEMA_VERSION2}`);
30573
+ }
30574
+ requireString(root.repo, "manifest repo");
30575
+ if (!import_node_path67.default.isAbsolute(root.repo)) throw new Error("manifest repo must be an absolute path");
30576
+ requireString(root.daemonId, "manifest daemonId");
30577
+ if (!Number.isInteger(root.createdAtEpoch) || root.createdAtEpoch < 0) {
30578
+ throw new Error("manifest createdAtEpoch must be a non-negative integer");
30579
+ }
30580
+ if (!Array.isArray(root.candidates) || root.candidateCount !== root.candidates.length) {
30581
+ throw new Error("manifest candidateCount does not match candidates");
30582
+ }
30583
+ const daemonId = root.daemonId;
30584
+ root.candidates.forEach((raw, index) => {
30585
+ const label2 = `candidate[${index}]`;
30586
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
30587
+ throw new Error(`${label2} must be an object`);
30588
+ }
30589
+ const candidate = raw;
30590
+ requireExactKeys2(
30591
+ candidate,
30592
+ ["checkout", "containers", "id", "networks", "project", "volumes"],
30593
+ label2
30594
+ );
30595
+ requireString(candidate.project, `${label2}.project`);
30596
+ requireString(candidate.checkout, `${label2}.checkout`);
30597
+ if (!import_node_path67.default.isAbsolute(candidate.checkout)) {
30598
+ throw new Error(`${label2}.checkout must be an absolute path`);
30599
+ }
30600
+ const members = [
30601
+ ["containers", ["id", "name", "state"], "id"],
30602
+ ["volumes", ["createdAt", "name"], "name"],
30603
+ ["networks", ["id", "name"], "id"]
30604
+ ];
30605
+ for (const [key, fields2, sortKey] of members) {
30606
+ const list2 = candidate[key];
30607
+ if (!Array.isArray(list2)) throw new Error(`${label2}.${key} must be an array`);
30608
+ for (const member of list2) {
30609
+ requireExactKeys2(member, [...fields2], `${label2}.${key}[]`);
30610
+ for (const field of fields2) {
30611
+ if (field !== "createdAt") requireString(member[field], `${label2}.${key}[].${field}`);
30612
+ }
30613
+ }
30614
+ requireSortedUnique(
30615
+ list2.map((member) => member[sortKey]),
30616
+ `${label2}.${key}`
30617
+ );
30618
+ }
30619
+ if (!candidate.containers.length) {
30620
+ throw new Error(`${label2}.containers must prove the checkout with at least one container`);
30621
+ }
30622
+ if (!HEX_ID2.test(String(candidate.id))) throw new Error(`${label2}.id is not a SHA-256`);
30623
+ const { id: id5, ...leftover } = candidate;
30624
+ if (id5 !== leftoverCandidateId(daemonId, leftover)) {
30625
+ throw new Error(`${label2}.id does not match its contents`);
30626
+ }
30627
+ });
30628
+ requireSortedUnique(
30629
+ root.candidates.map((candidate) => candidate.project),
30630
+ "manifest candidate projects"
30631
+ );
30632
+ return root;
30633
+ }
30634
+ function readLeftoverManifest(file) {
30635
+ const raw = import_node_fs54.default.readFileSync(file);
30636
+ let parsed;
30637
+ try {
30638
+ parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(raw));
30639
+ } catch (error) {
30640
+ throw new Error(`manifest is not valid UTF-8 JSON: ${error.message}`);
30641
+ }
30642
+ const manifest = validateLeftoverManifest(parsed);
30643
+ if (canonicalJson(parsed) !== raw.toString("utf-8")) {
30644
+ throw new Error("manifest is not in canonical JSON form");
30645
+ }
30646
+ return { raw, manifest };
30647
+ }
30648
+ function gitCommonDir(repo) {
30649
+ const result = (0, import_node_child_process39.spawnSync)(
30650
+ "git",
30651
+ ["-C", repo, "rev-parse", "--path-format=absolute", "--git-common-dir"],
30652
+ { encoding: "utf-8" }
30653
+ );
30654
+ const commonDir = result.stdout?.trim();
30655
+ if (result.status !== 0 || !commonDir)
30656
+ throw new Error(`cannot read the Git directory of ${repo}`);
30657
+ return commonDir;
30658
+ }
30659
+ function currentRemovals(candidate, inventory, context) {
30660
+ const report = findDockerLeftovers(inventory, context);
30661
+ const projectContainers = inventory.containers.filter(
30662
+ (container) => container.labels[PROJECT_LABEL] === candidate.project
30663
+ );
30664
+ let current;
30665
+ if (projectContainers.length) {
30666
+ const blocked3 = report.blocked.find((entry) => entry.project === candidate.project);
30667
+ if (blocked3) throw new LeftoverSkip("blocked", `${blocked3.reason}; next: ${blocked3.next}`);
30668
+ const leftover = report.leftovers.find((entry) => entry.project === candidate.project);
30669
+ if (!leftover || leftover.checkout !== candidate.checkout) {
30670
+ throw new LeftoverSkip("changed", "the project is no longer a leftover of this checkout");
30671
+ }
30672
+ current = leftover;
30673
+ } else {
30674
+ if (context.exists(candidate.checkout)) {
30675
+ throw new LeftoverSkip("changed", "the checkout folder exists again");
30676
+ }
30677
+ const mounted = new Set(inventory.containers.flatMap((container) => container.volumes));
30678
+ const ours = (object4) => object4.labels[PROJECT_LABEL] === candidate.project && object4.name.startsWith(`${candidate.project}_`);
30679
+ current = {
30680
+ project: candidate.project,
30681
+ checkout: candidate.checkout,
30682
+ containers: [],
30683
+ volumes: inventory.volumes.filter(
30684
+ (volume) => !mounted.has(volume.name) && (ours(volume) || isAnonymousVolume(volume) && candidate.volumes.some((approved) => approved.name === volume.name))
30685
+ ).map(({ name, createdAt }) => ({ name, createdAt })),
30686
+ networks: inventory.networks.filter((network) => ours(network) && network.attached === 0).map(({ id: id5, name }) => ({ id: id5, name }))
30687
+ };
30688
+ }
30689
+ const approve = (present, approved, key, kind) => {
30690
+ const allowed = new Set(approved.map(key));
30691
+ const extra = present.filter((entry) => !allowed.has(key(entry)));
30692
+ if (extra.length) {
30693
+ throw new LeftoverSkip("changed", `${extra.length} unapproved ${kind} now belong to it`);
30694
+ }
30695
+ };
30696
+ approve(current.containers, candidate.containers, (entry) => entry.id, "container(s)");
30697
+ approve(
30698
+ current.volumes,
30699
+ candidate.volumes,
30700
+ (entry) => `${entry.name}\0${entry.createdAt}`,
30701
+ "volume(s)"
30702
+ );
30703
+ approve(current.networks, candidate.networks, (entry) => entry.id, "network(s)");
30704
+ const stillPresent = (name) => inventory.volumes.some((volume) => volume.name === name);
30705
+ const notRemovable = candidate.volumes.filter(
30706
+ (volume) => stillPresent(volume.name) && !current.volumes.some((entry) => entry.name === volume.name)
30707
+ );
30708
+ if (notRemovable.length) {
30709
+ throw new LeftoverSkip(
30710
+ "changed",
30711
+ `${notRemovable.length} approved volume(s) are now in use or re-created`
30712
+ );
30713
+ }
30714
+ return current;
30715
+ }
30716
+ function removeMember(run, args, step, steps) {
30717
+ const result = run(args);
30718
+ if (result.status === 0) {
30719
+ steps.push(`removed-${step}`);
30720
+ return;
30721
+ }
30722
+ if (/no such (container|volume|network)/i.test(result.stderr)) {
30723
+ steps.push(`gone-${step}`);
30724
+ return;
30725
+ }
30726
+ throw new Error(`docker ${args.slice(0, 2).join(" ")} failed: ${result.stderr.trim()}`);
30727
+ }
30728
+ function applyLeftoverManifest(options, dependencies = {}) {
30729
+ const { raw, manifest } = readLeftoverManifest(options.manifestFile);
30730
+ const manifestSha256 = requireManifestHash(raw, options.sha256);
30731
+ const now = dependencies.nowEpoch?.() ?? Math.floor(Date.now() / 1e3);
30732
+ if (manifest.createdAtEpoch > now + FUTURE_TOLERANCE_SECONDS2) {
30733
+ throw new Error("manifest creation time is in the future");
30734
+ }
30735
+ const age = Math.max(0, now - manifest.createdAtEpoch);
30736
+ if (age > options.maxAgeSeconds) {
30737
+ throw new Error(`manifest is ${age}s old; maximum allowed age is ${options.maxAgeSeconds}s`);
30738
+ }
30739
+ if (!manifest.candidates.length) throw new Error("approved manifest is empty");
30740
+ const run = dependencies.run ?? defaultDockerRunner;
30741
+ const readInventory = dependencies.readInventory ?? (() => readDockerInventory(run));
30742
+ const readContext = dependencies.readContext ?? readLeftoverContext;
30743
+ const withLocks = dependencies.withLocks ?? defaultWithLocks;
30744
+ const targets = [];
30745
+ for (const candidate of manifest.candidates) {
30746
+ const outcome = {
30747
+ id: candidate.id,
30748
+ project: candidate.project,
30749
+ status: "skipped",
30750
+ steps: []
30751
+ };
30752
+ targets.push(outcome);
30753
+ try {
30754
+ withLocks(candidate.project, () => {
30755
+ const inventory = readInventory();
30756
+ if (inventory.daemonId !== manifest.daemonId) {
30757
+ throw new LeftoverSkip(
30758
+ "daemon-mismatch",
30759
+ "the manifest was made for another Docker engine"
30760
+ );
30761
+ }
30762
+ const current = currentRemovals(candidate, inventory, readContext(manifest.repo));
30763
+ const count = current.containers.length + current.volumes.length + current.networks.length;
30764
+ if (count === 0) {
30765
+ outcome.status = "already-done";
30766
+ return;
30767
+ }
30768
+ if (!options.yes) {
30769
+ outcome.status = "would-remove";
30770
+ outcome.reason = `${current.containers.length} container(s), ${current.volumes.length} volume(s), ${current.networks.length} network(s)`;
30771
+ return;
30772
+ }
30773
+ const proves = (id5) => Boolean(
30774
+ inventory.containers.find((container) => container.id === id5)?.labels[PROVIDER_LABEL]
30775
+ );
30776
+ const ordered = [
30777
+ ...current.containers.filter((container) => !proves(container.id)),
30778
+ ...current.containers.filter((container) => proves(container.id))
30779
+ ];
30780
+ for (const container of ordered) {
30781
+ removeMember(run, ["rm", container.id], `container:${container.id}`, outcome.steps);
30782
+ }
30783
+ for (const volume of current.volumes) {
30784
+ removeMember(run, ["volume", "rm", volume.name], `volume:${volume.name}`, outcome.steps);
30785
+ }
30786
+ for (const network of current.networks) {
30787
+ removeMember(
30788
+ run,
30789
+ ["network", "rm", network.id],
30790
+ `network:${network.name}`,
30791
+ outcome.steps
30792
+ );
30793
+ }
30794
+ writeLeftoverReceipt(
30795
+ (dependencies.commonDir ?? gitCommonDir)(manifest.repo),
30796
+ manifestSha256,
30797
+ candidate,
30798
+ outcome.steps,
30799
+ now
30800
+ );
30801
+ outcome.status = "removed";
30802
+ });
30803
+ } catch (error) {
30804
+ const message = error instanceof Error ? error.message : String(error);
30805
+ outcome.reason = message;
30806
+ if (error instanceof LeftoverSkip) {
30807
+ outcome.code = error.code;
30808
+ continue;
30809
+ }
30810
+ outcome.status = "failed";
30811
+ outcome.code = outcome.steps.length ? "teardown-failed" : "environment";
30812
+ }
30813
+ }
30814
+ return { manifestSha256, dryRun: !options.yes, targets };
30815
+ }
30816
+ function leftoverReceiptPath(commonDir, manifestSha256, id5) {
30817
+ return import_node_path67.default.join(commonDir, "devrouter-leftovers", "receipts", manifestSha256, `${id5}.json`);
30818
+ }
30819
+ function writeLeftoverReceipt(commonDir, manifestSha256, candidate, steps, nowEpoch) {
30820
+ const file = leftoverReceiptPath(commonDir, manifestSha256, candidate.id);
30821
+ import_node_fs54.default.mkdirSync(import_node_path67.default.dirname(file), { recursive: true });
30822
+ writeFileAtomically(
30823
+ file,
30824
+ canonicalJson({
30825
+ candidateId: candidate.id,
30826
+ checkout: candidate.checkout,
30827
+ completedAtEpoch: nowEpoch,
30828
+ kind: LEFTOVER_RECEIPT_KIND,
30829
+ manifestSha256,
30830
+ project: candidate.project,
30831
+ schemaVersion: SCHEMA_VERSION2,
30832
+ steps,
30833
+ tool: "devrouter"
30834
+ })
30835
+ );
30836
+ }
30837
+ var import_node_child_process39, import_node_fs54, import_node_path67, LEFTOVER_MANIFEST_KIND, LEFTOVER_RECEIPT_KIND, SCHEMA_VERSION2, MAX_DOCKER_OBJECTS, FUTURE_TOLERANCE_SECONDS2, PROJECT_LABEL, WORKING_DIR_LABEL, PROVIDER_LABEL, STOPPED_STATES, HEX_ID2, DockerListingBoundError, defaultDockerRunner, LeftoverSkip, defaultWithLocks;
30838
+ var init_docker_leftovers = __esm({
30839
+ "src/core/docker-leftovers.ts"() {
30840
+ "use strict";
30841
+ import_node_child_process39 = require("child_process");
30842
+ import_node_fs54 = __toESM(require("fs"));
30843
+ import_node_path67 = __toESM(require("path"));
30844
+ init_atomic_file();
30845
+ init_devpod_mutation();
30846
+ init_devpod_workspaces();
30847
+ init_devsy_mutation();
30848
+ init_workspace_ownership();
30849
+ init_workspace_runtime();
30850
+ init_worktree_reclaim();
30851
+ LEFTOVER_MANIFEST_KIND = "docker-leftover-manifest";
30852
+ LEFTOVER_RECEIPT_KIND = "docker-leftover-receipt";
30853
+ SCHEMA_VERSION2 = 1;
30854
+ MAX_DOCKER_OBJECTS = 5e3;
30855
+ FUTURE_TOLERANCE_SECONDS2 = 300;
30856
+ PROJECT_LABEL = "com.docker.compose.project";
30857
+ WORKING_DIR_LABEL = "com.docker.compose.project.working_dir";
30858
+ PROVIDER_LABEL = "dev.containers.id";
30859
+ STOPPED_STATES = /* @__PURE__ */ new Set(["exited", "dead", "created"]);
30860
+ HEX_ID2 = /^[0-9a-f]{64}$/;
30861
+ DockerListingBoundError = class extends Error {
30862
+ };
30863
+ defaultDockerRunner = (args) => {
30864
+ const result = (0, import_node_child_process39.spawnSync)("docker", args, {
30865
+ encoding: "utf-8",
30866
+ timeout: 6e4,
30867
+ maxBuffer: 256 * 1024 * 1024
30868
+ });
30869
+ return {
30870
+ status: result.error ? null : result.status,
30871
+ stdout: result.stdout ?? "",
30872
+ stderr: result.error?.message ?? result.stderr ?? ""
30873
+ };
30874
+ };
30875
+ LeftoverSkip = class extends Error {
30876
+ constructor(code, message) {
30877
+ super(message);
30878
+ this.code = code;
30879
+ }
30880
+ };
30881
+ defaultWithLocks = (target, operation) => withMutationLock(
30882
+ "Docker leftover removal",
30883
+ target,
30884
+ () => withMutationLock2("Docker leftover removal", target, operation)
30885
+ );
30886
+ }
30887
+ });
30888
+
30889
+ // src/core/workspace-consumption.ts
30890
+ function measureWorktreeConsumption(worktreePath, options) {
30891
+ const deadlineMs = options?.deadlineMs ?? DEFAULT_DEADLINE_MS;
30892
+ const startedAt = Date.now();
30893
+ let rootStat;
30894
+ try {
30895
+ rootStat = import_node_fs55.default.lstatSync(worktreePath);
30896
+ } catch (error) {
30897
+ return { status: "unknown", reason: describeError(error, worktreePath) };
30898
+ }
30899
+ let rootEntries;
30900
+ try {
30901
+ rootEntries = import_node_fs55.default.readdirSync(worktreePath, { withFileTypes: true });
30902
+ } catch (error) {
30903
+ return { status: "unknown", reason: describeError(error, worktreePath) };
30904
+ }
30905
+ const seenInodes = /* @__PURE__ */ new Set();
30906
+ let bytes2 = 0;
30907
+ let timedOut = false;
30908
+ let unreadableReason = null;
30909
+ const accumulate = (stat) => {
30910
+ if (stat.nlink > 1) {
30911
+ const key = `${stat.dev}:${stat.ino}`;
30912
+ if (seenInodes.has(key)) return;
30913
+ seenInodes.add(key);
30914
+ }
30915
+ bytes2 += stat.blocks * BLOCK_SIZE_BYTES;
30916
+ };
30917
+ const deadlineExceeded = () => Date.now() - startedAt >= deadlineMs;
30918
+ accumulate(rootStat);
30919
+ const stack = [{ dirPath: worktreePath, entries: rootEntries }];
30920
+ while (stack.length > 0 && !timedOut && !unreadableReason) {
30921
+ const { dirPath, entries } = stack.pop();
30922
+ for (const entry of entries) {
30923
+ if (deadlineExceeded()) {
30924
+ timedOut = true;
30925
+ break;
30926
+ }
30927
+ const entryPath = import_node_path68.default.join(dirPath, entry.name);
30928
+ let entryStat;
30929
+ try {
30930
+ entryStat = import_node_fs55.default.lstatSync(entryPath);
30931
+ } catch (error) {
30932
+ if (error?.code === "ENOENT") continue;
30933
+ unreadableReason = describeIncompleteWalk(error);
30934
+ break;
30935
+ }
30936
+ accumulate(entryStat);
30937
+ if (!entryStat.isDirectory()) continue;
30938
+ let childEntries;
30939
+ try {
30940
+ childEntries = import_node_fs55.default.readdirSync(entryPath, { withFileTypes: true });
30941
+ } catch (error) {
30942
+ unreadableReason = describeIncompleteWalk(error);
30943
+ break;
30944
+ }
30945
+ stack.push({ dirPath: entryPath, entries: childEntries });
30946
+ }
30947
+ }
30948
+ if (timedOut) {
30949
+ return { status: "unknown", reason: `exceeded deadline of ${deadlineMs}ms` };
30950
+ }
30951
+ if (unreadableReason) {
30952
+ return { status: "unknown", reason: unreadableReason };
30953
+ }
30954
+ return { status: "measured", bytes: bytes2 };
30955
+ }
30956
+ function describeIncompleteWalk(error) {
30957
+ return `could not read every path inside the worktree: ${error?.message ?? String(error)}`;
30958
+ }
30959
+ function measureContainerConsumption(worktreePaths, dependencies) {
30960
+ const byWorktree = /* @__PURE__ */ new Map();
30961
+ if (worktreePaths.length === 0) return byWorktree;
30962
+ const inspect = dependencies?.inspect ?? inspectWorkspaceContainers;
30963
+ const containers2 = inspect();
30964
+ const attributedIds = /* @__PURE__ */ new Set();
30965
+ for (const worktreePath of worktreePaths) {
30966
+ for (const container of workspaceAppContainers(containers2, worktreePath)) {
30967
+ attributedIds.add(container.id);
30968
+ }
30969
+ }
30970
+ const sized = attributedIds.size === 0 ? [] : inspect({ withSize: true, ids: Array.from(attributedIds) });
30971
+ for (const worktreePath of worktreePaths) {
30972
+ byWorktree.set(worktreePath, summarizeContainers(workspaceAppContainers(sized, worktreePath)));
30973
+ }
30974
+ return byWorktree;
30975
+ }
30976
+ function summarizeContainers(containers2) {
30977
+ let writable = 0;
30978
+ let shared = 0;
30979
+ for (const container of containers2) {
30980
+ const { sizeRw, sizeRootFs } = container;
30981
+ if (typeof sizeRw !== "number" || typeof sizeRootFs !== "number" || !Number.isFinite(sizeRw) || !Number.isFinite(sizeRootFs) || sizeRootFs < sizeRw) {
30982
+ const unknown = {
30983
+ status: "unknown",
30984
+ reason: `container ${container.id.slice(0, 12)} reported no usable size`
30985
+ };
30986
+ return { containerWritable: unknown, imageShared: unknown };
30987
+ }
30988
+ writable += sizeRw;
30989
+ shared += sizeRootFs - sizeRw;
30990
+ }
30991
+ return {
30992
+ containerWritable: { status: "measured", bytes: writable },
30993
+ imageShared: { status: "measured", bytes: shared }
30994
+ };
30995
+ }
30996
+ function describeError(error, worktreePath) {
30997
+ const code = error?.code;
30998
+ if (code === "ENOENT") return `worktree path '${worktreePath}' does not exist`;
30999
+ if (code === "EACCES" || code === "EPERM") {
31000
+ return `permission denied reading worktree path '${worktreePath}'`;
31001
+ }
31002
+ return `could not stat worktree path '${worktreePath}': ${error?.message ?? String(error)}`;
31003
+ }
31004
+ var import_node_fs55, import_node_path68, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
31005
+ var init_workspace_consumption = __esm({
31006
+ "src/core/workspace-consumption.ts"() {
31007
+ "use strict";
31008
+ import_node_fs55 = __toESM(require("fs"));
31009
+ import_node_path68 = __toESM(require("path"));
31010
+ init_devpod_environment();
31011
+ DEFAULT_DEADLINE_MS = 1e4;
31012
+ BLOCK_SIZE_BYTES = 512;
31013
+ }
31014
+ });
31015
+
31016
+ // src/core/workspace-cleanup.ts
31017
+ function isRecord9(value) {
31018
+ return value !== null && typeof value === "object" && !Array.isArray(value);
31019
+ }
31020
+ function isSha2(value) {
31021
+ return typeof value === "string" && /^[0-9a-f]{40,64}$/i.test(value);
31022
+ }
31023
+ function parseTimestamp(value) {
31024
+ if (typeof value !== "string") return void 0;
31025
+ const timestamp = Date.parse(value);
31026
+ return Number.isFinite(timestamp) ? timestamp : void 0;
31027
+ }
31028
+ function validTimestamp(value) {
31029
+ return parseTimestamp(value) !== void 0;
31030
+ }
31031
+ function successfulOutput(result) {
31032
+ if (result.status !== 0 || result.error) return void 0;
31033
+ const output2 = result.stdout.trim();
31034
+ return output2.length > 0 ? output2 : void 0;
31035
+ }
31036
+ function gitOutput(repoPath, args, commandRunner) {
31037
+ return successfulOutput(commandRunner("git", ["-C", repoPath, ...args]));
31038
+ }
31039
+ function quoteCommandArg(value) {
31040
+ return /^[a-zA-Z0-9_./:@+-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
31041
+ }
31042
+ function workspaceCommand(repoPath, args, beforeRepo = [], afterRepo = []) {
31043
+ return [
31044
+ "devrouter",
31045
+ "workspace",
31046
+ ...args,
31047
+ ...beforeRepo,
31048
+ "--repo",
31049
+ quoteCommandArg(repoPath),
31050
+ ...afterRepo
31051
+ ].join(" ");
31052
+ }
31053
+ function parseInactiveFor(value = DEFAULT_INACTIVE_FOR) {
31054
+ const match = /^(?<amount>[1-9]\d*)(?<unit>[smhdw])$/.exec(value);
31055
+ if (!match?.groups) {
31056
+ throw new Error(
31057
+ "--inactive-for must be a positive integer followed by s, m, h, d, or w (for example 30d)."
31058
+ );
31059
+ }
31060
+ const amount = Number(match.groups.amount);
31061
+ const multiplier = { s: 1, m: 60, h: 3600, d: 86400, w: 604800 }[match.groups.unit];
31062
+ const seconds = amount * multiplier;
31063
+ if (!Number.isSafeInteger(seconds)) {
31064
+ throw new Error("--inactive-for is too large.");
31065
+ }
31066
+ return { input: value, seconds };
31067
+ }
31068
+ function evaluateWorkspaceActivity(evidence, cutoff) {
31069
+ const cutoffTime = parseTimestamp(cutoff);
31070
+ if (cutoffTime === void 0) {
31071
+ throw new Error(`Invalid activity cutoff '${cutoff}'.`);
31072
+ }
31073
+ const validEvidence = evidence.filter(
31074
+ (entry) => entry.status === "valid" && validTimestamp(entry.timestamp)
31075
+ );
31076
+ const latestTime = validEvidence.reduce(
31077
+ (latest, entry) => Math.max(
31078
+ latest ?? Number.NEGATIVE_INFINITY,
31079
+ parseTimestamp(entry.timestamp) ?? Number.NEGATIVE_INFINITY
31080
+ ),
31081
+ void 0
30408
31082
  );
30409
31083
  const latestTimestamp = latestTime === void 0 ? null : validEvidence.find((entry) => parseTimestamp(entry.timestamp) === latestTime)?.timestamp ?? null;
30410
31084
  const contributingEvidence = ACTIVITY_SOURCES.filter(
@@ -30433,7 +31107,7 @@ function evaluateWorkspaceActivity(evidence, cutoff) {
30433
31107
  }
30434
31108
  function readGitSnapshot(worktree, commandRunner) {
30435
31109
  const comparablePath = comparableWorkspacePath(worktree.path);
30436
- if (worktree.prunable || !import_node_fs55.default.existsSync(comparablePath)) {
31110
+ if (worktree.prunable || !import_node_fs56.default.existsSync(comparablePath)) {
30437
31111
  return { worktree, checkout: "missing", head: null, committerDate: null };
30438
31112
  }
30439
31113
  const head = gitOutput(comparablePath, ["rev-parse", "--verify", "HEAD"], commandRunner);
@@ -31006,7 +31680,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
31006
31680
  containers2 = measureContainersFn(worktreePaths);
31007
31681
  } catch (error) {
31008
31682
  const reason = `container measurement failed: ${describeCause(error)}`;
31009
- containers2 = new Map(worktreePaths.map((path71) => [path71, unknownContainers(reason)]));
31683
+ containers2 = new Map(worktreePaths.map((path72) => [path72, unknownContainers(reason)]));
31010
31684
  }
31011
31685
  }
31012
31686
  const rows2 = records.map((record5) => {
@@ -31065,7 +31739,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
31065
31739
  dependencies.listForge ?? ((identity2) => listForgeChanges(identity2))
31066
31740
  );
31067
31741
  const sources = (dependencies.readRuntimeSources ?? readRuntimeSources)(repoPath);
31068
- const disposable = import_node_fs55.default.existsSync(import_node_path68.default.join(repoPath, ".devrouter.yml")) ? loadRepoConfig(repoPath).worktrees?.disposable ?? [] : [];
31742
+ const disposable = import_node_fs56.default.existsSync(import_node_path69.default.join(repoPath, ".devrouter.yml")) ? loadRepoConfig(repoPath).worktrees?.disposable ?? [] : [];
31069
31743
  const [primary, ...linked] = worktrees;
31070
31744
  const safetyFor = (worktree, worktreePath, evidence) => toSafety(
31071
31745
  classify({
@@ -31073,7 +31747,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
31073
31747
  branch: worktree?.branch,
31074
31748
  primary: primary !== void 0 && sameWorkspacePath(primary.path, worktreePath),
31075
31749
  locked: worktree?.locked ?? false,
31076
- missing: !worktree || worktree.prunable || !import_node_fs55.default.existsSync(worktreePath),
31750
+ missing: !worktree || worktree.prunable || !import_node_fs56.default.existsSync(worktreePath),
31077
31751
  runtimeEvidence: evidence,
31078
31752
  forge,
31079
31753
  extraDisposablePatterns: disposable
@@ -31101,9 +31775,18 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
31101
31775
  ...base,
31102
31776
  workspaces: managedRows,
31103
31777
  worktrees: unmanaged,
31778
+ dockerLeftovers: readDockerLeftoverEvidence(repoPath, dependencies),
31104
31779
  networkCapacity
31105
31780
  };
31106
31781
  }
31782
+ function readDockerLeftoverEvidence(repoPath, dependencies) {
31783
+ try {
31784
+ const read2 = dependencies.readDockerLeftovers ?? ((repo) => findDockerLeftovers(readDockerInventory(), readLeftoverContext(repo)));
31785
+ return { status: "listed", ...read2(repoPath) };
31786
+ } catch (error) {
31787
+ return { status: "unavailable", reason: describeCause(error) };
31788
+ }
31789
+ }
31107
31790
  function readPathEvidence(repoPath, worktreePath, inactiveFor, dependencies) {
31108
31791
  const records = (dependencies.listOwnership ?? listWorkspaceOwnership)(repoPath).filter(
31109
31792
  (record5) => sameWorkspacePath(record5.worktreePath, worktreePath)
@@ -31125,7 +31808,7 @@ function readPathEvidence(repoPath, worktreePath, inactiveFor, dependencies) {
31125
31808
  }
31126
31809
  const sources = (dependencies.readRuntimeSources ?? readRuntimeSources)(repoPath);
31127
31810
  const cutoffEpoch = Math.floor(Date.now() / 1e3) - parseInactiveFor(inactiveFor).seconds;
31128
- const gitDir = (0, import_node_child_process39.spawnSync)(
31811
+ const gitDir = (0, import_node_child_process40.spawnSync)(
31129
31812
  "git",
31130
31813
  ["-C", worktreePath, "rev-parse", "--path-format=absolute", "--git-dir"],
31131
31814
  { encoding: "utf-8", env: READ_ONLY_GIT_ENV2 }
@@ -31145,7 +31828,7 @@ function classifyExactPath(repoPath, worktreePath, evidence, forge, extraDisposa
31145
31828
  branch: worktree?.branch,
31146
31829
  primary: worktrees[0] !== void 0 && sameWorkspacePath(worktrees[0].path, worktreePath),
31147
31830
  locked: worktree?.locked ?? false,
31148
- missing: !worktree || worktree.prunable || !import_node_fs55.default.existsSync(worktreePath),
31831
+ missing: !worktree || worktree.prunable || !import_node_fs56.default.existsSync(worktreePath),
31149
31832
  runtimeEvidence: evidence.runtimeEvidence,
31150
31833
  forge,
31151
31834
  extraDisposablePatterns
@@ -31171,7 +31854,7 @@ function prepareReclaimVerdicts(repo, inactiveFor, dependencies = {}) {
31171
31854
  dependencies.listForge ?? ((identity2) => listForgeChanges(identity2))
31172
31855
  );
31173
31856
  const forge = evidence.status === "not-checked" ? { status: "unavailable", reason: "forge not checked" } : evidence;
31174
- const disposable = import_node_fs55.default.existsSync(import_node_path68.default.join(repoPath, ".devrouter.yml")) ? loadRepoConfig(repoPath).worktrees?.disposable ?? [] : [];
31857
+ const disposable = import_node_fs56.default.existsSync(import_node_path69.default.join(repoPath, ".devrouter.yml")) ? loadRepoConfig(repoPath).worktrees?.disposable ?? [] : [];
31175
31858
  const paths = evidenceCache(
31176
31859
  (worktreePath) => readPathEvidence(repoPath, worktreePath, inactiveFor, dependencies)
31177
31860
  );
@@ -31190,7 +31873,7 @@ function prepareReclaimVerdicts(repo, inactiveFor, dependencies = {}) {
31190
31873
  }
31191
31874
  function prepareTrimChecks(repo, inactiveFor, dependencies = {}) {
31192
31875
  const repoPath = resolveRepoPath(repo);
31193
- const configured = import_node_fs55.default.existsSync(import_node_path68.default.join(repoPath, ".devrouter.yml")) ? loadRepoConfig(repoPath).worktrees : void 0;
31876
+ const configured = import_node_fs56.default.existsSync(import_node_path69.default.join(repoPath, ".devrouter.yml")) ? loadRepoConfig(repoPath).worktrees : void 0;
31194
31877
  const patterns = configured?.trim ?? [];
31195
31878
  const paths = evidenceCache(
31196
31879
  (worktreePath) => readPathEvidence(repoPath, worktreePath, inactiveFor, dependencies)
@@ -31215,14 +31898,15 @@ function prepareTrimChecks(repo, inactiveFor, dependencies = {}) {
31215
31898
  isActive: (worktreePath) => paths.last(worktreePath).active
31216
31899
  };
31217
31900
  }
31218
- var import_node_child_process39, import_node_fs55, import_node_path68, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
31901
+ var import_node_child_process40, import_node_fs56, import_node_path69, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
31219
31902
  var init_workspace_cleanup = __esm({
31220
31903
  "src/core/workspace-cleanup.ts"() {
31221
31904
  "use strict";
31222
- import_node_child_process39 = require("child_process");
31223
- import_node_fs55 = __toESM(require("fs"));
31224
- import_node_path68 = __toESM(require("path"));
31905
+ import_node_child_process40 = require("child_process");
31906
+ import_node_fs56 = __toESM(require("fs"));
31907
+ import_node_path69 = __toESM(require("path"));
31225
31908
  init_devpod_workspaces();
31909
+ init_docker_leftovers();
31226
31910
  init_forge_changes();
31227
31911
  init_host_routes();
31228
31912
  init_network_diagnostics();
@@ -31243,7 +31927,7 @@ var init_workspace_cleanup = __esm({
31243
31927
  ];
31244
31928
  defaultCommandRunner = (command, args, input2) => {
31245
31929
  const environment = command === "git" ? READ_ONLY_GIT_ENV2 : { ...process.env, LC_ALL: "C" };
31246
- const result = (0, import_node_child_process39.spawnSync)(command, args, {
31930
+ const result = (0, import_node_child_process40.spawnSync)(command, args, {
31247
31931
  encoding: "utf-8",
31248
31932
  env: environment,
31249
31933
  ...input2 === void 0 ? {} : { input: input2 }
@@ -31337,20 +32021,20 @@ function warnMissingWorkspaceOwnership(repoPath) {
31337
32021
  );
31338
32022
  }
31339
32023
  function defaultWorktreePath(mainRepo, ws) {
31340
- return import_node_path69.default.join(mainRepo, "trees", ws);
32024
+ return import_node_path70.default.join(mainRepo, "trees", ws);
31341
32025
  }
31342
32026
  function assertDefaultWorktreeRootIgnored(mainRepo) {
31343
- const ignored = (0, import_node_child_process40.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
32027
+ const ignored = (0, import_node_child_process41.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
31344
32028
  encoding: "utf-8"
31345
32029
  });
31346
32030
  if (ignored.status !== 0) {
31347
32031
  throw new Error(
31348
- `Default worktree root '${import_node_path69.default.join(mainRepo, "trees")}' is not ignored. Add 'trees/' to '${import_node_path69.default.join(mainRepo, ".gitignore")}' or use --path.`
32032
+ `Default worktree root '${import_node_path70.default.join(mainRepo, "trees")}' is not ignored. Add 'trees/' to '${import_node_path70.default.join(mainRepo, ".gitignore")}' or use --path.`
31349
32033
  );
31350
32034
  }
31351
32035
  }
31352
32036
  function legacyDefaultWorktreePath(mainRepo, ws) {
31353
- return import_node_path69.default.join(import_node_path69.default.dirname(mainRepo), `${import_node_path69.default.basename(mainRepo)}-${ws}`);
32037
+ return import_node_path70.default.join(import_node_path70.default.dirname(mainRepo), `${import_node_path70.default.basename(mainRepo)}-${ws}`);
31354
32038
  }
31355
32039
  function teardownFallbackPath(mainRepo, workspace) {
31356
32040
  const candidates = [
@@ -31462,7 +32146,7 @@ function assertFullDownPreflight(mainRepo, target) {
31462
32146
  if (sameWorkspacePath(target.worktreePath, mainRepo)) {
31463
32147
  throw new Error("Refusing to remove the primary Git checkout.");
31464
32148
  }
31465
- if (!target.worktree || target.worktree.prunable || !import_node_fs56.default.existsSync(target.worktreePath)) return;
32149
+ if (!target.worktree || target.worktree.prunable || !import_node_fs57.default.existsSync(target.worktreePath)) return;
31466
32150
  if (target.worktree.locked) {
31467
32151
  throw new Error(
31468
32152
  `Worktree '${target.worktreePath}' is locked; unlock it before workspace down.`
@@ -31494,9 +32178,9 @@ async function workspaceUp(branch, opts = {}) {
31494
32178
  (candidate) => defaultWorktreePath(mainRepo, candidate)
31495
32179
  );
31496
32180
  const generatedPath = candidatePaths.find(
31497
- (candidate) => !import_node_fs56.default.existsSync(candidate) && !worktrees.some((worktree) => sameWorkspacePath(worktree.path, candidate))
32181
+ (candidate) => !import_node_fs57.default.existsSync(candidate) && !worktrees.some((worktree) => sameWorkspacePath(worktree.path, candidate))
31498
32182
  );
31499
- const selectedPath = opts.path ? import_node_path69.default.resolve(opts.path) : existingBranch?.path ?? generatedPath;
32183
+ const selectedPath = opts.path ? import_node_path70.default.resolve(opts.path) : existingBranch?.path ?? generatedPath;
31500
32184
  if (!selectedPath) {
31501
32185
  throw new Error(`Could not allocate a collision-safe worktree path for branch '${branch}'.`);
31502
32186
  }
@@ -31505,7 +32189,7 @@ async function workspaceUp(branch, opts = {}) {
31505
32189
  `Branch '${branch}' already uses worktree '${existingBranch.path}', not '${selectedPath}'.`
31506
32190
  );
31507
32191
  }
31508
- if (import_node_fs56.default.existsSync(selectedPath)) {
32192
+ if (import_node_fs57.default.existsSync(selectedPath)) {
31509
32193
  const registered = worktrees.find(
31510
32194
  (worktree) => sameWorkspacePath(worktree.path, selectedPath)
31511
32195
  );
@@ -31524,11 +32208,11 @@ async function workspaceUp(branch, opts = {}) {
31524
32208
  return selectedPath;
31525
32209
  }
31526
32210
  if (!opts.path) assertDefaultWorktreeRootIgnored(mainRepo);
31527
- const add = (0, import_node_child_process40.spawnSync)("git", ["-C", mainRepo, "worktree", "add", selectedPath, branch], {
32211
+ const add = (0, import_node_child_process41.spawnSync)("git", ["-C", mainRepo, "worktree", "add", selectedPath, branch], {
31528
32212
  encoding: "utf-8"
31529
32213
  });
31530
32214
  if (add.status !== 0) {
31531
- const addNew = (0, import_node_child_process40.spawnSync)(
32215
+ const addNew = (0, import_node_child_process41.spawnSync)(
31532
32216
  "git",
31533
32217
  ["-C", mainRepo, "worktree", "add", "-b", branch, selectedPath],
31534
32218
  { encoding: "utf-8" }
@@ -31596,43 +32280,43 @@ function workspaceLs(repoPath) {
31596
32280
  };
31597
32281
  });
31598
32282
  }
31599
- async function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = false) {
31600
- const devpods = listDevpodWorkspaces(resolved.worktreePath);
31601
- assertDevpodTargetSafe(resolved, worktrees, devpods);
31602
- const devpodId = resolved.record?.devpodId ?? resolved.workspace;
32283
+ async function mutateWorkspaceRuntime(action2, resolved2, worktrees, quiet = false) {
32284
+ const devpods = listDevpodWorkspaces(resolved2.worktreePath);
32285
+ assertDevpodTargetSafe(resolved2, worktrees, devpods);
32286
+ const devpodId = resolved2.record?.devpodId ?? resolved2.workspace;
31603
32287
  claimLifecycleEffect();
31604
- const mutation = action2 === "stop" ? stopOwnedDevpodWorkspace(devpodId, resolved.worktreePath) : deleteOwnedDevpodWorkspace(devpodId, resolved.worktreePath);
31605
- if (action2 === "stop" && mutation.status === "absent" && readManagedRuntimeState(resolved.worktreePath, resolved.workspace)) {
32288
+ const mutation = action2 === "stop" ? stopOwnedDevpodWorkspace(devpodId, resolved2.worktreePath) : deleteOwnedDevpodWorkspace(devpodId, resolved2.worktreePath);
32289
+ if (action2 === "stop" && mutation.status === "absent" && readManagedRuntimeState(resolved2.worktreePath, resolved2.workspace)) {
31606
32290
  throw new Error(
31607
32291
  "Retained managed runtime has no exact provider registration; routes were preserved."
31608
32292
  );
31609
32293
  }
31610
- const retained = action2 === "stop" ? readManagedRuntimeState(resolved.worktreePath, resolved.workspace) : void 0;
32294
+ const retained = action2 === "stop" ? readManagedRuntimeState(resolved2.worktreePath, resolved2.workspace) : void 0;
31611
32295
  let absent = mutation.status === "proven-absent";
31612
32296
  const remove = () => {
31613
32297
  if (retained?.stopBaseline) {
31614
32298
  absent = proveManagedStop(retained).status === "proven-absent";
31615
- if (absent && listRoutesForWorktreePath(resolved.worktreePath).some(
31616
- (route) => route.workspace !== resolved.workspace || !retained.desired.apps.includes(route.name)
32299
+ if (absent && listRoutesForWorktreePath(resolved2.worktreePath).some(
32300
+ (route) => route.workspace !== resolved2.workspace || !retained.desired.apps.includes(route.name)
31617
32301
  ))
31618
32302
  throw new Error("Absent stop contains routes outside the retained desired set.");
31619
32303
  }
31620
32304
  if (absent && !retained) {
31621
- if (listRoutesForWorktreePath(resolved.worktreePath).length)
32305
+ if (listRoutesForWorktreePath(resolved2.worktreePath).length)
31622
32306
  throw new Error("Initial managed stop observed remaining workspace routes.");
31623
32307
  return [];
31624
32308
  }
31625
32309
  claimLifecycleEffect();
31626
- return removeWorkspaceRoutesForWorktree(resolved.workspace, resolved.worktreePath);
32310
+ return removeWorkspaceRoutesForWorktree(resolved2.workspace, resolved2.worktreePath);
31627
32311
  };
31628
- const routes = retained?.stopBaseline ? withMutationLock("Revalidate stop routes", resolved.worktreePath, remove) : remove();
32312
+ const routes = retained?.stopBaseline ? withMutationLock("Revalidate stop routes", resolved2.worktreePath, remove) : remove();
31629
32313
  await ensureTraefikRoutesRemoved([
31630
32314
  ...routes,
31631
32315
  ...absent && retained ? managedStopRouteReferences(retained) : []
31632
32316
  ]);
31633
32317
  if (!quiet) {
31634
32318
  process.stdout.write(
31635
- `Freed ${routes.length} route(s) for workspace '${resolved.workspace}'.
32319
+ `Freed ${routes.length} route(s) for workspace '${resolved2.workspace}'.
31636
32320
  `
31637
32321
  );
31638
32322
  }
@@ -31641,20 +32325,20 @@ async function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = fals
31641
32325
  freedRoutes: routes.length,
31642
32326
  providerChanged: mutation.status === "changed",
31643
32327
  ...absent ? { runtimeAbsent: true } : {},
31644
- workspace: resolved.workspace
32328
+ workspace: resolved2.workspace
31645
32329
  };
31646
32330
  }
31647
32331
  async function runWorkspaceLifecycle(action2, target, opts = {}) {
31648
32332
  const mainRepo = resolveRepoPath(opts.repoPath);
31649
32333
  const worktrees = identifyGitWorktrees(mainRepo);
31650
32334
  const records = listWorkspaceOwnership(mainRepo);
31651
- const resolved = resolveWorkspaceTarget(mainRepo, target, worktrees, records);
32335
+ const resolved2 = resolveWorkspaceTarget(mainRepo, target, worktrees, records);
31652
32336
  if (action2 === "stop") {
31653
- const stopped = await superviseLifecycle("stop", resolved.worktreePath, {
32337
+ const stopped = await superviseLifecycle("stop", resolved2.worktreePath, {
31654
32338
  quiet: opts.quiet
31655
32339
  });
31656
32340
  return {
31657
- workspace: resolved.workspace,
32341
+ workspace: resolved2.workspace,
31658
32342
  devpodId: stopped.devpodId,
31659
32343
  freedRoutes: stopped.freedRoutes,
31660
32344
  providerChanged: stopped.stopped && !stopped.runtimeAbsent,
@@ -31664,30 +32348,30 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
31664
32348
  const operation = async () => {
31665
32349
  const removeWorktree = action2 === "down" && !opts.keepWorktree;
31666
32350
  if (removeWorktree) {
31667
- assertFullDownPreflight(mainRepo, resolved);
32351
+ assertFullDownPreflight(mainRepo, resolved2);
31668
32352
  }
31669
- const result = await mutateWorkspaceRuntime("delete", resolved, worktrees, opts.quiet);
32353
+ const result = await mutateWorkspaceRuntime("delete", resolved2, worktrees, opts.quiet);
31670
32354
  if (removeWorktree) {
31671
- if (resolved.worktree && !resolved.worktree.prunable && import_node_fs56.default.existsSync(resolved.worktreePath)) {
31672
- const rm = (0, import_node_child_process40.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
32355
+ if (resolved2.worktree && !resolved2.worktree.prunable && import_node_fs57.default.existsSync(resolved2.worktreePath)) {
32356
+ const rm = (0, import_node_child_process41.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved2.worktreePath], {
31673
32357
  encoding: "utf-8"
31674
32358
  });
31675
32359
  if (rm.status !== 0) {
31676
32360
  const detail = [rm.error?.message, rm.stderr].filter(Boolean).join("\n").trim();
31677
32361
  throw new Error(
31678
- `git worktree remove failed for '${resolved.worktreePath}': ${detail || "unknown error"}`
32362
+ `git worktree remove failed for '${resolved2.worktreePath}': ${detail || "unknown error"}`
31679
32363
  );
31680
32364
  }
31681
- process.stdout.write(`Removed worktree ${resolved.worktreePath}.
32365
+ process.stdout.write(`Removed worktree ${resolved2.worktreePath}.
31682
32366
  `);
31683
32367
  }
31684
- if (resolved.record) {
31685
- removeWorkspaceOwnership(mainRepo, resolved.workspace);
32368
+ if (resolved2.record) {
32369
+ removeWorkspaceOwnership(mainRepo, resolved2.workspace);
31686
32370
  }
31687
32371
  }
31688
32372
  return result;
31689
32373
  };
31690
- return resolved.worktree && !resolved.worktree.prunable && import_node_fs56.default.existsSync(resolved.worktreePath) ? withLifecycleOperationLock(resolved.worktreePath, operation) : operation();
32374
+ return resolved2.worktree && !resolved2.worktree.prunable && import_node_fs57.default.existsSync(resolved2.worktreePath) ? withLifecycleOperationLock(resolved2.worktreePath, operation) : operation();
31691
32375
  }
31692
32376
  async function reclaimWorktreeExactPath(mainRepo, worktreePath, verify, afterRemove, steps) {
31693
32377
  await withLifecycleOperationLock(worktreePath, async () => {
@@ -31713,7 +32397,7 @@ async function reclaimWorktreeExactPath(mainRepo, worktreePath, verify, afterRem
31713
32397
  );
31714
32398
  steps.push("runtime-deleted");
31715
32399
  }
31716
- const rm = (0, import_node_child_process40.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", worktreePath], {
32400
+ const rm = (0, import_node_child_process41.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", worktreePath], {
31717
32401
  encoding: "utf-8"
31718
32402
  });
31719
32403
  if (rm.status !== 0) {
@@ -31734,13 +32418,13 @@ async function workspaceStop(target, opts = {}) {
31734
32418
  async function workspaceDown(target, opts = {}) {
31735
32419
  return runWorkspaceLifecycle("down", target, opts);
31736
32420
  }
31737
- var import_node_child_process40, import_node_fs56, import_node_path69, WORKSPACE_ALLOCATION_LOCK_WAIT_MS;
32421
+ var import_node_child_process41, import_node_fs57, import_node_path70, WORKSPACE_ALLOCATION_LOCK_WAIT_MS;
31738
32422
  var init_workspace_lifecycle = __esm({
31739
32423
  "src/core/workspace-lifecycle.ts"() {
31740
32424
  "use strict";
31741
- import_node_child_process40 = require("child_process");
31742
- import_node_fs56 = __toESM(require("fs"));
31743
- import_node_path69 = __toESM(require("path"));
32425
+ import_node_child_process41 = require("child_process");
32426
+ import_node_fs57 = __toESM(require("fs"));
32427
+ import_node_path70 = __toESM(require("path"));
31744
32428
  init_devpod_mutation();
31745
32429
  init_devpod_workspaces();
31746
32430
  init_devsy_mutation();
@@ -31759,7 +32443,7 @@ var init_workspace_lifecycle = __esm({
31759
32443
 
31760
32444
  // src/core/worktree-trim.ts
31761
32445
  function listIgnoredPaths(worktreePath) {
31762
- const result = (0, import_node_child_process41.spawnSync)(
32446
+ const result = (0, import_node_child_process42.spawnSync)(
31763
32447
  "git",
31764
32448
  [
31765
32449
  "-C",
@@ -31843,19 +32527,19 @@ function buildTrimManifest(report, patterns, createdAtEpoch) {
31843
32527
  return canonicalJson(manifest);
31844
32528
  }
31845
32529
  function isInside(parent, child) {
31846
- const relative2 = import_node_path70.default.relative(parent, child);
31847
- return relative2 !== "" && !relative2.startsWith("..") && !import_node_path70.default.isAbsolute(relative2);
32530
+ const relative2 = import_node_path71.default.relative(parent, child);
32531
+ return relative2 !== "" && !relative2.startsWith("..") && !import_node_path71.default.isAbsolute(relative2);
31848
32532
  }
31849
32533
  function exists(file) {
31850
32534
  try {
31851
- import_node_fs57.default.lstatSync(file);
32535
+ import_node_fs58.default.lstatSync(file);
31852
32536
  return true;
31853
32537
  } catch {
31854
32538
  return false;
31855
32539
  }
31856
32540
  }
31857
32541
  function hasTrackedFiles(worktreePath, relative2) {
31858
- const result = (0, import_node_child_process41.spawnSync)(
32542
+ const result = (0, import_node_child_process42.spawnSync)(
31859
32543
  "git",
31860
32544
  ["-C", worktreePath, "--literal-pathspecs", "ls-files", "-z", "--", relative2],
31861
32545
  { encoding: "utf-8", env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" } }
@@ -31863,10 +32547,10 @@ function hasTrackedFiles(worktreePath, relative2) {
31863
32547
  return result.status !== 0 || Boolean(result.error) || result.stdout.length > 0;
31864
32548
  }
31865
32549
  function trimOnePath(candidate, relative2, patterns, steps) {
31866
- const target = import_node_path70.default.join(candidate.path, relative2);
32550
+ const target = import_node_path71.default.join(candidate.path, relative2);
31867
32551
  let stat;
31868
32552
  try {
31869
- stat = import_node_fs57.default.lstatSync(target);
32553
+ stat = import_node_fs58.default.lstatSync(target);
31870
32554
  } catch (error) {
31871
32555
  if (error.code === "ENOENT") {
31872
32556
  steps.push(`gone:${relative2}`);
@@ -31878,7 +32562,7 @@ function trimOnePath(candidate, relative2, patterns, steps) {
31878
32562
  steps.push(`skipped-symlink:${relative2}`);
31879
32563
  return;
31880
32564
  }
31881
- if (!isInside(import_node_fs57.default.realpathSync.native(candidate.path), import_node_fs57.default.realpathSync.native(target))) {
32565
+ if (!isInside(import_node_fs58.default.realpathSync.native(candidate.path), import_node_fs58.default.realpathSync.native(target))) {
31882
32566
  steps.push(`skipped-outside:${relative2}`);
31883
32567
  return;
31884
32568
  }
@@ -31891,7 +32575,7 @@ function trimOnePath(candidate, relative2, patterns, steps) {
31891
32575
  return;
31892
32576
  }
31893
32577
  try {
31894
- import_node_fs57.default.rmSync(target, { recursive: true, force: true });
32578
+ import_node_fs58.default.rmSync(target, { recursive: true, force: true });
31895
32579
  } catch (error) {
31896
32580
  steps.push(`partial:${relative2}`);
31897
32581
  throw error;
@@ -31931,7 +32615,7 @@ async function applyTrimManifest(options, dependencies) {
31931
32615
  continue;
31932
32616
  }
31933
32617
  const listed = candidate.paths ?? [];
31934
- if (listed.every((relative2) => !exists(import_node_path70.default.join(candidate.path, relative2))) && matchingReceipt(candidate, manifestSha256)) {
32618
+ if (listed.every((relative2) => !exists(import_node_path71.default.join(candidate.path, relative2))) && matchingReceipt(candidate, manifestSha256)) {
31935
32619
  outcome.status = "already-done";
31936
32620
  continue;
31937
32621
  }
@@ -31955,7 +32639,7 @@ async function applyTrimManifest(options, dependencies) {
31955
32639
  if (!options.yes) {
31956
32640
  verify();
31957
32641
  outcome.status = "would-trim";
31958
- const present = listed.filter((relative2) => exists(import_node_path70.default.join(candidate.path, relative2)));
32642
+ const present = listed.filter((relative2) => exists(import_node_path71.default.join(candidate.path, relative2)));
31959
32643
  outcome.reason = present.length ? `would remove: ${present.join(", ")}` : "nothing present";
31960
32644
  continue;
31961
32645
  }
@@ -31992,13 +32676,13 @@ async function applyTrimManifest(options, dependencies) {
31992
32676
  }
31993
32677
  return { manifestSha256, dryRun: !options.yes, targets, stoppedEarly };
31994
32678
  }
31995
- var import_node_child_process41, import_node_fs57, import_node_path70;
32679
+ var import_node_child_process42, import_node_fs58, import_node_path71;
31996
32680
  var init_worktree_trim = __esm({
31997
32681
  "src/core/worktree-trim.ts"() {
31998
32682
  "use strict";
31999
- import_node_child_process41 = require("child_process");
32000
- import_node_fs57 = __toESM(require("fs"));
32001
- import_node_path70 = __toESM(require("path"));
32683
+ import_node_child_process42 = require("child_process");
32684
+ import_node_fs58 = __toESM(require("fs"));
32685
+ import_node_path71 = __toESM(require("path"));
32002
32686
  init_reliability_lifecycle();
32003
32687
  init_worktree_reclaim();
32004
32688
  init_worktree_safety();
@@ -32012,6 +32696,7 @@ __export(workspace_exports, {
32012
32696
  runWorkspaceDownCommand: () => runWorkspaceDownCommand,
32013
32697
  runWorkspaceGcCommand: () => runWorkspaceGcCommand,
32014
32698
  runWorkspaceJournalSettleCommand: () => runWorkspaceJournalSettleCommand,
32699
+ runWorkspaceLeftoverCommand: () => runWorkspaceLeftoverCommand,
32015
32700
  runWorkspaceLsCommand: () => runWorkspaceLsCommand,
32016
32701
  runWorkspaceManifestHashCommand: () => runWorkspaceManifestHashCommand,
32017
32702
  runWorkspaceManifestSelectCommand: () => runWorkspaceManifestSelectCommand,
@@ -32021,15 +32706,15 @@ __export(workspace_exports, {
32021
32706
  runWorkspaceUpCommand: () => runWorkspaceUpCommand
32022
32707
  });
32023
32708
  function resolveGitWorkspaceRepo(repoPath) {
32024
- const resolved = resolveRepoPath(repoPath);
32709
+ const resolved2 = resolveRepoPath(repoPath);
32025
32710
  try {
32026
- resolveGitCommonDir(resolved);
32711
+ resolveGitCommonDir(resolved2);
32027
32712
  } catch (error) {
32028
- throw new Error(`Workspace commands require a Git repository: '${resolved}'.`, {
32713
+ throw new Error(`Workspace commands require a Git repository: '${resolved2}'.`, {
32029
32714
  cause: error
32030
32715
  });
32031
32716
  }
32032
- return resolved;
32717
+ return resolved2;
32033
32718
  }
32034
32719
  async function runWorkspaceUpCommand(branch, options) {
32035
32720
  const repoPath = resolveGitWorkspaceRepo(options.repo);
@@ -32061,13 +32746,30 @@ function runWorkspaceLsCommand(options) {
32061
32746
  );
32062
32747
  }
32063
32748
  }
32749
+ function writeManifestOutput(manifest, output2) {
32750
+ if (output2) {
32751
+ import_node_fs59.default.writeFileSync(output2, manifest, { flag: "wx" });
32752
+ process.stdout.write(`${sha256Hex(manifest)} ${output2}
32753
+ `);
32754
+ } else {
32755
+ process.stdout.write(manifest);
32756
+ }
32757
+ }
32064
32758
  function runWorkspaceCleanupCommand(options) {
32065
32759
  const repoPath = resolveGitWorkspaceRepo(options.repo);
32760
+ if (options.manifest && options.action === "leftover") {
32761
+ const leftovers = findDockerLeftovers(readDockerInventory(), readLeftoverContext(repoPath));
32762
+ writeManifestOutput(
32763
+ buildLeftoverManifest(repoPath, leftovers, Math.floor(Date.now() / 1e3)),
32764
+ options.output
32765
+ );
32766
+ return;
32767
+ }
32066
32768
  const report = buildWorkspaceCleanupReport({ ...options, repo: repoPath });
32067
32769
  if (options.manifest) {
32068
32770
  const action2 = options.action ?? "reclaim";
32069
32771
  if (action2 !== "reclaim" && action2 !== "trim") {
32070
- throw new Error("--action must be reclaim or trim.");
32772
+ throw new Error("--action must be reclaim, trim or leftover.");
32071
32773
  }
32072
32774
  if (action2 === "trim") {
32073
32775
  if (!options.allWorktrees)
@@ -32076,19 +32778,13 @@ function runWorkspaceCleanupCommand(options) {
32076
32778
  throw new Error("--manifest requires --all-worktrees and --check-merged.");
32077
32779
  }
32078
32780
  const createdAtEpoch = Math.floor(Date.now() / 1e3);
32079
- const worktrees = import_node_fs58.default.existsSync(import_node_path71.default.join(repoPath, ".devrouter.yml")) ? loadRepoConfig(repoPath).worktrees : void 0;
32781
+ const worktrees = import_node_fs59.default.existsSync(import_node_path72.default.join(repoPath, ".devrouter.yml")) ? loadRepoConfig(repoPath).worktrees : void 0;
32080
32782
  const manifest = action2 === "trim" ? buildTrimManifest(
32081
32783
  report,
32082
32784
  { trim: worktrees?.trim ?? [], disposable: worktrees?.disposable },
32083
32785
  createdAtEpoch
32084
32786
  ) : buildReclaimManifest(report, createdAtEpoch);
32085
- if (options.output) {
32086
- import_node_fs58.default.writeFileSync(options.output, manifest, { flag: "wx" });
32087
- process.stdout.write(`${sha256Hex(manifest)} ${options.output}
32088
- `);
32089
- } else {
32090
- process.stdout.write(manifest);
32091
- }
32787
+ writeManifestOutput(manifest, options.output);
32092
32788
  return;
32093
32789
  }
32094
32790
  if (options.json) {
@@ -32153,10 +32849,36 @@ async function runWorkspaceJournalSettleCommand(options) {
32153
32849
  );
32154
32850
  }
32155
32851
  function runWorkspaceManifestHashCommand(file) {
32156
- const { raw } = readManifest(file);
32852
+ let leftover = false;
32853
+ try {
32854
+ leftover = isLeftoverManifest(JSON.parse(import_node_fs59.default.readFileSync(file, "utf-8")));
32855
+ } catch {
32856
+ }
32857
+ const { raw } = leftover ? readLeftoverManifest(file) : readManifest(file);
32157
32858
  process.stdout.write(`${sha256Hex(raw)}
32158
32859
  `);
32159
32860
  }
32861
+ function runWorkspaceLeftoverCommand(options) {
32862
+ const report = applyLeftoverManifest({
32863
+ manifestFile: options.manifest,
32864
+ sha256: options.sha256,
32865
+ maxAgeSeconds: parseInactiveFor(options.maxAge ?? "24h").seconds,
32866
+ yes: Boolean(options.yes)
32867
+ });
32868
+ if (options.json) {
32869
+ printJSON(report);
32870
+ } else {
32871
+ printReclaimReport(
32872
+ {
32873
+ dryRun: report.dryRun,
32874
+ stoppedEarly: false,
32875
+ targets: report.targets.map((target) => ({ ...target, path: target.project }))
32876
+ },
32877
+ "remove the leftovers"
32878
+ );
32879
+ }
32880
+ if (report.targets.some((target) => target.status === "failed")) process.exitCode = 1;
32881
+ }
32160
32882
  function runWorkspaceManifestSelectCommand(file, options) {
32161
32883
  process.stdout.write(selectManifest(file, options.expectedSha256, options.id));
32162
32884
  }
@@ -32205,12 +32927,13 @@ async function runWorkspaceTrimCommand(options) {
32205
32927
  process.exitCode = 1;
32206
32928
  }
32207
32929
  }
32208
- var import_node_fs58, import_node_path71;
32930
+ var import_node_fs59, import_node_path72;
32209
32931
  var init_workspace2 = __esm({
32210
32932
  "src/commands/workspace.ts"() {
32211
32933
  "use strict";
32212
- import_node_fs58 = __toESM(require("fs"));
32213
- import_node_path71 = __toESM(require("path"));
32934
+ import_node_fs59 = __toESM(require("fs"));
32935
+ import_node_path72 = __toESM(require("path"));
32936
+ init_docker_leftovers();
32214
32937
  init_output();
32215
32938
  init_repo_config();
32216
32939
  init_workspace_cleanup();
@@ -32262,7 +32985,7 @@ var init_version = __esm({
32262
32985
 
32263
32986
  // src/cli.ts
32264
32987
  var import_commander = require("commander");
32265
- var CLI_VERSION = true ? "0.2.0" : "0.0.0-dev";
32988
+ var CLI_VERSION = true ? "0.3.0" : "0.0.0-dev";
32266
32989
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
32267
32990
  function withErrorHandling(action2) {
32268
32991
  return async (...args) => {
@@ -32622,7 +33345,7 @@ workspaceCommand2.command("cleanup").description("Report managed workspace activ
32622
33345
  "Emit a canonical reclaim manifest of quiet RECLAIM trees (needs --all-worktrees --check-merged)"
32623
33346
  ).option("--output <file>", "Write the manifest to a new file and print its SHA-256").option(
32624
33347
  "--action <action>",
32625
- "Manifest action: reclaim (needs --check-merged) or trim (declared worktrees.trim paths)",
33348
+ "Manifest action: reclaim (needs --check-merged), trim (declared worktrees.trim paths) or leftover (Docker stacks of deleted checkouts)",
32626
33349
  "reclaim"
32627
33350
  ).action(
32628
33351
  withErrorHandling(async (_options, command) => {
@@ -32691,6 +33414,15 @@ workspaceCommand2.command("trim").description(
32691
33414
  await runWorkspaceTrimCommand2(options);
32692
33415
  })
32693
33416
  );
33417
+ workspaceCommand2.command("leftover").description(
33418
+ "Remove the Docker containers, volumes and networks of an approved leftover manifest, re-verifying each stack inside the provider locks"
33419
+ ).requiredOption("--manifest <file>", "Approved leftover manifest").requiredOption("--sha256 <hash>", "Approved SHA-256 of the manifest").option("--max-age <duration>", "Refuse a manifest older than this (Ns, Nm, Nh, Nd, Nw)", "24h").option("--yes", "Remove; without it the command is a dry run").option("--json", "Output JSON").action(
33420
+ withErrorHandling(async (_options, command) => {
33421
+ const options = command.opts();
33422
+ const { runWorkspaceLeftoverCommand: runWorkspaceLeftoverCommand2 } = await Promise.resolve().then(() => (init_workspace2(), workspace_exports));
33423
+ runWorkspaceLeftoverCommand2(options);
33424
+ })
33425
+ );
32694
33426
  workspaceCommand2.command("gc").description("Report or clean missing ledger-owned workspace resources").option("--repo <path>", "Main repository path (defaults to current directory)").option("--json", "Output JSON").option("--yes", "Delete eligible workspace runtime, route, and ownership resources").action(
32695
33427
  withErrorHandling(async (_options, command) => {
32696
33428
  const options = command.opts();
@@ -32715,10 +33447,10 @@ workspaceCommand2.command("down").description("Delete a workspace's runtime and
32715
33447
  workspaceCommand2.command("journal").description("Inspect and recover workspace lifecycle journals").command("settle").description(
32716
33448
  "Settle an interrupted lifecycle operation as unobservable so ensure and stop can proceed"
32717
33449
  ).argument("[path]", "Checkout path (defaults to current directory)").option("--json", "Output JSON").action(
32718
- withErrorHandling(async (path71, _options, command) => {
33450
+ withErrorHandling(async (path72, _options, command) => {
32719
33451
  const options = command.opts();
32720
33452
  const { runWorkspaceJournalSettleCommand: runWorkspaceJournalSettleCommand2 } = await Promise.resolve().then(() => (init_workspace2(), workspace_exports));
32721
- await runWorkspaceJournalSettleCommand2({ ...path71 ? { path: path71 } : {}, ...options });
33453
+ await runWorkspaceJournalSettleCommand2({ ...path72 ? { path: path72 } : {}, ...options });
32722
33454
  })
32723
33455
  );
32724
33456
  function parseVersionRequest(argv) {