@devrouter/cli 0.0.52 → 0.0.53

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
@@ -249,6 +249,8 @@ profiles:
249
249
  - Validation is strict at config load: unknown keys, non-routed \`apps\`, non-dependency \`dependencies\`, \`readiness\` outside the profile's apps, and multiple defaults are rejected.
250
250
  - Selection: \`devrouter ensure <path> --profile <name>\`. Comma-separated selections (\`--profile manage,pwa\`) merge with deduplication; the canonical name is sorted-unique so order never affects identity or fingerprints. A wildcard member collapses to everything.
251
251
  - Managed adapters receive \`DEVROUTER_PROFILE\` (canonical resolved name) in the post-start env; profile switches replace the owned process group via the fingerprint.
252
+ - Adapters may pass \`--prepare-command <command>\` to \`devrouter-process ensure\` for synchronous dependency preparation under the process lock before application launch. Unchanged owned processes skip preparation; preparation participates in default fingerprints and must not daemonize or detach.
253
+ - Exact managed Devsy stop proves the complete retained container population, ownership and configuration before stopping residual services after a stopped primary. Unknown evidence blocks cleanup, and an original provider failure remains an error even if residual cleanup succeeds.
252
254
 
253
255
  ### Managed devcontainer resources
254
256
 
@@ -2691,7 +2693,7 @@ function loadRepoConfig(repoPath) {
2691
2693
  const config = parseConfig(parsed ?? {}, configPath);
2692
2694
  const requiredVersion = config.devrouter?.version;
2693
2695
  if (requiredVersion && !hasWarnedVersionMismatch) {
2694
- const cliVersion = true ? "0.0.52" : "0.0.0-dev";
2696
+ const cliVersion = true ? "0.0.53" : "0.0.0-dev";
2695
2697
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
2696
2698
  hasWarnedVersionMismatch = true;
2697
2699
  process.stderr.write(
@@ -3227,6 +3229,8 @@ function buildOnboardingPrompt(options = {}) {
3227
3229
  "- Devsy agent readiness: run `devrouter setup --yes --workspace-runtime devsy` once so Devrouter acquires and verifies the pinned agent in its own machine cache. `doctor` reports `ready`, `missing`, `stale`, or `invalid` without network access, and `ensure` fails before the provider queue when readiness is not `ready`. An explicit `DEVSY_AGENT_BINARY` remains authoritative and must match a pinned official asset.",
3228
3230
  "- A managed adapter plus `postCreateCommand` requires `waitFor` exactly `postCreateCommand` or `postStartCommand`; managed selective config preserves lifecycle fields and changes only `runServices`.",
3229
3231
  "- Managed process reuse fingerprints command argv, workspace identity, and the exact adapter snapshot. If a non-secret runtime value also affects reuse, set `DEVROUTER_PROCESS_FINGERPRINT_ENV` to its comma-separated environment names; secret-like names are rejected and raw values are never persisted.",
3232
+ "- Managed adapters may use devrouter-process ensure --prepare-command <command> for synchronous dependency preparation under the process lock before app launch. Unchanged owned processes skip preparation; it participates in default fingerprints and must not daemonize or detach.",
3233
+ "- Exact managed Devsy stop validates complete retained container membership, ownership and configuration before stopping residual services after a stopped primary. Unknown evidence blocks cleanup; original provider failures remain errors even after successful residual cleanup.",
3230
3234
  "- Owner status is `present`, `missing`, `locked`, or `conflict`. Dirty or locked full down fails before side effects. `workspace gc` is a dry run; only `--yes` deletes exact eligible missing resources and records, never Git worktrees, branches, or prune state.",
3231
3235
  "- `devrouter workspace cleanup --repo . --inactive-for 30d --check-merged --json` is report-only and never mutates the workspace runtime, routes, ownership, Git, Docker, applications, worktrees, or branches. It reports ownership, workspace runtime registration, runtime state (`running|stopped|busy|not-found|absent|unknown`), checkout, advisory activity, route, and integration evidence. Local DevPod/Devsy list/status checks always run; `--check-merged` alone enables read-only origin and matching GitHub/GitLab checks. Treat `not-found` as stale runtime after Docker pruning; busy, unavailable, or conflicting evidence suppresses destructive suggestions. Explicit `gc`/`down` can remove exact stale registration only after expected-ID `NotFound` proof and ownership revalidation. Runtime `lastUsed` remains advisory.",
3232
3236
  '- `--measure-size` adds per-workspace storage consumption and stays read-only, but walks each worktree and runs `docker ps` / `docker inspect --size`, so leave it off for a quick report. Each row reports `worktree` and `containerWritable` bytes, which are reclaimable; `imageShared` bytes, which are not, because those image layers are shared with other containers and overlap across rows; and `reclaimable`, the sum of the reclaimable fields. Never add `imageShared` across rows. Any figure that cannot be trusted reports `{"status": "unknown", "reason": ...}` rather than zero, and a workspace with no container reports a measured `0`.',
@@ -5814,13 +5818,14 @@ function startExactManagedServices(options) {
5814
5818
  );
5815
5819
  }
5816
5820
  }
5817
- function stopExactManagedService(containerId, service) {
5821
+ function stopExactManagedService(containerId, service, options) {
5818
5822
  assertSafeContainerId(containerId);
5819
5823
  const result = (0, import_node_child_process7.spawnSync)("docker", ["stop", containerId], {
5820
5824
  encoding: "utf-8",
5821
- stdio: "inherit"
5825
+ stdio: "inherit",
5826
+ ...options ? { timeout: options.timeoutMs } : {}
5822
5827
  });
5823
- if (result.status !== 0) {
5828
+ if (result.status !== 0 || result.error) {
5824
5829
  throw new Error(`Could not stop exact managed service '${service}' (${containerId}).`);
5825
5830
  }
5826
5831
  }
@@ -5844,6 +5849,198 @@ var init_devcontainer_profile = __esm({
5844
5849
  });
5845
5850
 
5846
5851
  // src/core/devpod-environment.ts
5852
+ function isRecord2(value) {
5853
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5854
+ }
5855
+ function assertFullContainerId(value) {
5856
+ if (typeof value !== "string" || !FULL_CONTAINER_ID_PATTERN.test(value)) {
5857
+ throw new Error("Managed stop Docker inspection returned an invalid container id.");
5858
+ }
5859
+ }
5860
+ function assertUniqueContainerIds(ids) {
5861
+ if (new Set(ids).size !== ids.length) {
5862
+ throw new Error("Managed stop Docker inspection returned duplicate container ids.");
5863
+ }
5864
+ }
5865
+ function parseDockerLines(stdout) {
5866
+ const lines = stdout.split(/\r?\n/);
5867
+ while (lines.at(-1) === "") lines.pop();
5868
+ if (lines.some((line) => line === "")) {
5869
+ throw new Error("Managed stop Docker inspection returned an empty record.");
5870
+ }
5871
+ return lines;
5872
+ }
5873
+ function runManagedStopDocker(args) {
5874
+ const result = (0, import_node_child_process8.spawnSync)("docker", args, {
5875
+ encoding: "utf-8",
5876
+ timeout: MANAGED_STOP_DOCKER_TIMEOUT_MS,
5877
+ maxBuffer: MANAGED_STOP_DOCKER_MAX_BUFFER
5878
+ });
5879
+ if (result.error || result.status !== 0) {
5880
+ throw new Error("Managed stop Docker inspection failed.");
5881
+ }
5882
+ if (typeof result.stdout !== "string") {
5883
+ throw new Error("Managed stop Docker inspection returned invalid output.");
5884
+ }
5885
+ return result.stdout;
5886
+ }
5887
+ function listManagedStopContainerIds(composeProject) {
5888
+ const stdout = runManagedStopDocker([
5889
+ "ps",
5890
+ "-a",
5891
+ "--no-trunc",
5892
+ "--filter",
5893
+ `label=${MANAGED_STOP_COMPOSE_LABEL}=${composeProject}`,
5894
+ "--format",
5895
+ "{{.ID}}"
5896
+ ]);
5897
+ const ids = parseDockerLines(stdout);
5898
+ ids.forEach(assertFullContainerId);
5899
+ assertUniqueContainerIds(ids);
5900
+ return ids;
5901
+ }
5902
+ function assertString(value) {
5903
+ if (typeof value !== "string") {
5904
+ throw new Error("Managed stop Docker inspection returned an invalid field.");
5905
+ }
5906
+ }
5907
+ function validateManagedStopSnapshot(value, composeProject) {
5908
+ if (!isRecord2(value)) {
5909
+ throw new Error("Managed stop Docker inspection returned an invalid record.");
5910
+ }
5911
+ assertFullContainerId(value.id);
5912
+ if (!isRecord2(value.state)) {
5913
+ throw new Error("Managed stop Docker inspection returned an invalid state.");
5914
+ }
5915
+ const status = value.state.Status;
5916
+ if (status !== "running" && status !== "exited" && status !== "created") {
5917
+ throw new Error("Managed stop Docker inspection returned an invalid state status.");
5918
+ }
5919
+ if (typeof value.state.Running !== "boolean") {
5920
+ throw new Error("Managed stop Docker inspection returned an invalid running flag.");
5921
+ }
5922
+ if (typeof value.state.Paused !== "boolean" || value.state.Paused) {
5923
+ throw new Error("Managed stop Docker inspection returned an invalid paused flag.");
5924
+ }
5925
+ if (typeof value.state.Restarting !== "boolean" || value.state.Restarting) {
5926
+ throw new Error("Managed stop Docker inspection returned an invalid restarting flag.");
5927
+ }
5928
+ if (typeof value.state.Dead !== "boolean" || value.state.Dead) {
5929
+ throw new Error("Managed stop Docker inspection returned an invalid dead flag.");
5930
+ }
5931
+ if (status === "running" !== value.state.Running) {
5932
+ throw new Error("Managed stop Docker inspection returned contradictory state.");
5933
+ }
5934
+ let health;
5935
+ if (value.state.Health !== void 0 && value.state.Health !== null) {
5936
+ if (!isRecord2(value.state.Health)) {
5937
+ throw new Error("Managed stop Docker inspection returned an invalid health field.");
5938
+ }
5939
+ assertString(value.state.Health.Status);
5940
+ health = { Status: value.state.Health.Status };
5941
+ }
5942
+ if (!isRecord2(value.labels)) {
5943
+ throw new Error("Managed stop Docker inspection returned invalid labels.");
5944
+ }
5945
+ for (const label of MANAGED_STOP_IDENTITY_LABELS) {
5946
+ assertString(value.labels[label]);
5947
+ }
5948
+ if (value.labels[MANAGED_STOP_COMPOSE_LABEL] !== composeProject) {
5949
+ throw new Error("Managed stop Docker inspection returned a foreign Compose project.");
5950
+ }
5951
+ if (!Array.isArray(value.mounts)) {
5952
+ throw new Error("Managed stop Docker inspection returned invalid mounts.");
5953
+ }
5954
+ const mounts = value.mounts.map((mount) => {
5955
+ if (!isRecord2(mount)) {
5956
+ throw new Error("Managed stop Docker inspection returned an invalid mount.");
5957
+ }
5958
+ assertString(mount.Type);
5959
+ assertString(mount.Source);
5960
+ assertString(mount.Destination);
5961
+ return {
5962
+ Type: mount.Type,
5963
+ Source: mount.Source,
5964
+ Destination: mount.Destination
5965
+ };
5966
+ });
5967
+ if (!isRecord2(value.networks)) {
5968
+ throw new Error("Managed stop Docker inspection returned invalid networks.");
5969
+ }
5970
+ return {
5971
+ id: value.id,
5972
+ state: {
5973
+ ...health ? { Health: health } : {},
5974
+ Status: status,
5975
+ Running: value.state.Running,
5976
+ Paused: false,
5977
+ Restarting: false,
5978
+ Dead: false
5979
+ },
5980
+ labels: {
5981
+ "com.docker.compose.project": value.labels["com.docker.compose.project"],
5982
+ "com.docker.compose.service": value.labels["com.docker.compose.service"],
5983
+ "com.docker.compose.project.working_dir": value.labels["com.docker.compose.project.working_dir"],
5984
+ "com.docker.compose.project.config_files": value.labels["com.docker.compose.project.config_files"],
5985
+ "com.docker.compose.config-hash": value.labels["com.docker.compose.config-hash"]
5986
+ },
5987
+ mounts,
5988
+ networks: {}
5989
+ };
5990
+ }
5991
+ function inspectManagedStopPopulation(ids, composeProject) {
5992
+ const stdout = runManagedStopDocker([
5993
+ "inspect",
5994
+ "--format",
5995
+ MANAGED_STOP_INSPECT_TEMPLATE,
5996
+ ...ids
5997
+ ]);
5998
+ const lines = parseDockerLines(stdout);
5999
+ if (lines.length !== ids.length) {
6000
+ throw new Error("Managed stop Docker inspection returned an incomplete population.");
6001
+ }
6002
+ const expected = new Set(ids);
6003
+ const seen = /* @__PURE__ */ new Set();
6004
+ const snapshots = lines.map((line) => {
6005
+ let parsed;
6006
+ try {
6007
+ parsed = JSON.parse(line);
6008
+ } catch {
6009
+ throw new Error("Managed stop Docker inspection returned malformed JSON.");
6010
+ }
6011
+ const snapshot = validateManagedStopSnapshot(parsed, composeProject);
6012
+ if (!expected.has(snapshot.id) || seen.has(snapshot.id)) {
6013
+ throw new Error("Managed stop Docker inspection returned an unexpected population.");
6014
+ }
6015
+ seen.add(snapshot.id);
6016
+ return snapshot;
6017
+ });
6018
+ return snapshots;
6019
+ }
6020
+ function requireSameContainerPopulation(expected, actual) {
6021
+ if (expected.length !== actual.length) {
6022
+ throw new Error("Managed stop Docker inspection observed a changed population.");
6023
+ }
6024
+ const expectedSet = new Set(expected);
6025
+ if (actual.some((id) => !expectedSet.has(id))) {
6026
+ throw new Error("Managed stop Docker inspection observed a changed population.");
6027
+ }
6028
+ }
6029
+ function inspectManagedStopContainers(composeProject) {
6030
+ if (typeof composeProject !== "string" || composeProject.length === 0 || composeProject !== composeProject.trim() || !MANAGED_STOP_PROJECT_PATTERN.test(composeProject)) {
6031
+ throw new Error("Managed stop Docker inspection requires a safe Compose project.");
6032
+ }
6033
+ const listedIds = listManagedStopContainerIds(composeProject);
6034
+ if (listedIds.length === 0) {
6035
+ const confirmedIds2 = listManagedStopContainerIds(composeProject);
6036
+ requireSameContainerPopulation(listedIds, confirmedIds2);
6037
+ return [];
6038
+ }
6039
+ const snapshots = inspectManagedStopPopulation(listedIds, composeProject);
6040
+ const confirmedIds = listManagedStopContainerIds(composeProject);
6041
+ requireSameContainerPopulation(listedIds, confirmedIds);
6042
+ return snapshots;
6043
+ }
5847
6044
  function inspectWorkspaceContainers(options) {
5848
6045
  let ids = options?.ids;
5849
6046
  if (!ids) {
@@ -5927,7 +6124,7 @@ function resolveRunningWorkspaceContainer(repoPath) {
5927
6124
  }
5928
6125
  return { id: container.id, workspacePath: repoMount.Destination };
5929
6126
  }
5930
- var import_node_child_process8, import_node_path16, SAFE_INSPECT_TEMPLATE, SIZE_INSPECT_TEMPLATE;
6127
+ var import_node_child_process8, import_node_path16, SAFE_INSPECT_TEMPLATE, SIZE_INSPECT_TEMPLATE, MANAGED_STOP_INSPECT_TEMPLATE, MANAGED_STOP_PROJECT_PATTERN, FULL_CONTAINER_ID_PATTERN, MANAGED_STOP_DOCKER_TIMEOUT_MS, MANAGED_STOP_DOCKER_MAX_BUFFER, MANAGED_STOP_COMPOSE_LABEL, MANAGED_STOP_IDENTITY_LABELS;
5931
6128
  var init_devpod_environment = __esm({
5932
6129
  "src/core/devpod-environment.ts"() {
5933
6130
  "use strict";
@@ -5939,6 +6136,19 @@ var init_devpod_environment = __esm({
5939
6136
  /}$/,
5940
6137
  ',"sizeRw":{{json (index . "SizeRw")}},"sizeRootFs":{{json (index . "SizeRootFs")}}}'
5941
6138
  );
6139
+ MANAGED_STOP_INSPECT_TEMPLATE = '{"id":{{json .Id}},"state":{"Status":{{json .State.Status}},"Running":{{json .State.Running}},"Paused":{{json .State.Paused}},"Restarting":{{json .State.Restarting}},"Dead":{{json .State.Dead}},"Health":{{with (index .State "Health")}}{"Status":{{json .Status}}}{{else}}null{{end}}},"labels":{"com.docker.compose.project":{{json (index .Config.Labels "com.docker.compose.project")}},"com.docker.compose.service":{{json (index .Config.Labels "com.docker.compose.service")}},"com.docker.compose.project.working_dir":{{json (index .Config.Labels "com.docker.compose.project.working_dir")}},"com.docker.compose.project.config_files":{{json (index .Config.Labels "com.docker.compose.project.config_files")}},"com.docker.compose.config-hash":{{json (index .Config.Labels "com.docker.compose.config-hash")}}},"mounts":[{{range $index, $mount := .Mounts}}{{if $index}},{{end}}{"Type":{{json $mount.Type}},"Source":{{json $mount.Source}},"Destination":{{json $mount.Destination}}}{{end}}],"networks":{}}';
6140
+ MANAGED_STOP_PROJECT_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
6141
+ FULL_CONTAINER_ID_PATTERN = /^[0-9a-f]{64}$/;
6142
+ MANAGED_STOP_DOCKER_TIMEOUT_MS = 5e3;
6143
+ MANAGED_STOP_DOCKER_MAX_BUFFER = 1024 * 1024;
6144
+ MANAGED_STOP_COMPOSE_LABEL = "com.docker.compose.project";
6145
+ MANAGED_STOP_IDENTITY_LABELS = [
6146
+ "com.docker.compose.project",
6147
+ "com.docker.compose.service",
6148
+ "com.docker.compose.project.working_dir",
6149
+ "com.docker.compose.project.config_files",
6150
+ "com.docker.compose.config-hash"
6151
+ ];
5942
6152
  }
5943
6153
  });
5944
6154
 
@@ -6907,7 +7117,8 @@ function listDevsyWorkspaces() {
6907
7117
  }
6908
7118
  const workspace = {
6909
7119
  id: candidate.id,
6910
- source: { localFolder: source.localFolder }
7120
+ source: { localFolder: source.localFolder },
7121
+ ...typeof candidate.context === "string" ? { context: candidate.context } : {}
6911
7122
  };
6912
7123
  if ("lastUsed" in candidate) {
6913
7124
  if (typeof candidate.lastUsed === "string") {
@@ -7586,372 +7797,898 @@ var init_devpod_workspaces = __esm({
7586
7797
  }
7587
7798
  });
7588
7799
 
7589
- // src/core/devsy-mutation.ts
7590
- function failedStartMayHaveAttached(devsyId, repoPath) {
7591
- try {
7592
- const attached = listDevsyWorkspaces();
7593
- const attachedId = devsyId ?? selectDevsyWorkspace(attached, repoPath)?.id;
7594
- if (!attachedId) return false;
7595
- return inspectDevsyWorkspaceOwnership(attached, attachedId, repoPath).status !== "absent";
7596
- } catch {
7597
- return true;
7598
- }
7599
- }
7600
- function withMutationLock(activity, target, operation) {
7601
- import_node_fs20.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
7602
- return withFileLockSync(
7603
- DEVSY_MUTATION_LOCK_FILE,
7604
- {
7605
- activity,
7606
- target: `'${target}'`,
7607
- waitMs: DEVSY_MUTATION_WAIT_MS,
7608
- fair: true,
7609
- onWait: createStderrWaitReporter(activity, `'${target}'`)
7610
- },
7611
- operation
7612
- );
7800
+ // src/core/route-publication.ts
7801
+ function routedAppsFromConfig(config) {
7802
+ return config.apps.filter((app) => app.kind !== "dependency");
7613
7803
  }
7614
- function withMutationLockAsync(activity, target, operation) {
7615
- import_node_fs20.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
7616
- return withFileLock(
7617
- DEVSY_MUTATION_LOCK_FILE,
7618
- {
7619
- activity,
7620
- target: `'${target}'`,
7621
- waitMs: DEVSY_MUTATION_WAIT_MS,
7622
- fair: true,
7623
- onWait: createStderrWaitReporter(activity, `'${target}'`)
7624
- },
7625
- operation
7804
+ function configuredProxyAppsFromConfig(config) {
7805
+ return routedAppsFromConfig(config).filter(
7806
+ (app) => app.runtime === "proxy"
7626
7807
  );
7627
7808
  }
7628
- function commandFailure2(result) {
7629
- return [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
7630
- }
7631
- function runDevsyUp(args, env, quiet) {
7632
- return new Promise((resolve) => {
7633
- const child = (0, import_node_child_process15.spawn)("devsy", args, {
7634
- stdio: ["inherit", quiet ? 2 : "inherit", "pipe"],
7635
- env
7636
- });
7637
- const stderr = child.stderr;
7638
- if (!stderr) throw new Error("Devsy startup stderr pipe was not created.");
7639
- let stderrTail = Buffer.alloc(0);
7640
- let spawnError;
7641
- stderr.on("data", (chunk) => {
7642
- const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
7643
- const writable = process.stderr.write(value);
7644
- if (!writable) {
7645
- stderr.pause();
7646
- process.stderr.once("drain", () => stderr.resume());
7647
- }
7648
- if (value.length >= DEVSY_STDERR_TAIL_BYTES) {
7649
- stderrTail = Buffer.from(value.subarray(value.length - DEVSY_STDERR_TAIL_BYTES));
7650
- } else {
7651
- const combined = Buffer.concat([stderrTail, value]);
7652
- stderrTail = combined.length > DEVSY_STDERR_TAIL_BYTES ? combined.subarray(combined.length - DEVSY_STDERR_TAIL_BYTES) : combined;
7653
- }
7654
- });
7655
- child.once("error", (error) => {
7656
- spawnError = error;
7657
- });
7658
- child.once("close", (status) => {
7659
- resolve({ status, error: spawnError, stderrTail: stderrTail.toString("utf-8") });
7660
- });
7661
- });
7662
- }
7663
- function runDevsyAction(action2, devsyId, force = false) {
7664
- const args = action2 === "delete" ? ["delete", devsyId, ...force ? ["--force"] : [], "--ignore-not-found"] : ["stop", devsyId];
7665
- const result = (0, import_node_child_process15.spawnSync)("devsy", ["workspace", ...args], { encoding: "utf-8" });
7666
- if (result.status !== 0) {
7809
+ function proxyAppsFromConfig(config) {
7810
+ const routedApps = routedAppsFromConfig(config);
7811
+ const proxyApps = configuredProxyAppsFromConfig(config);
7812
+ const unsupported = routedApps.filter((app) => app.runtime !== "proxy");
7813
+ if (unsupported.length > 0) {
7667
7814
  throw new Error(
7668
- `devsy workspace ${action2}${force ? " --force" : ""} failed for '${devsyId}': ${commandFailure2(result) || "unknown error"}`
7815
+ `Environment reconciliation supports proxy runtime apps only; unsupported: ${unsupported.map((app) => `${app.name} (${app.runtime})`).join(", ")}`
7669
7816
  );
7670
7817
  }
7818
+ return proxyApps;
7671
7819
  }
7672
- function inspectExactOwnership(devsyId, worktreePath) {
7673
- const ownership = inspectDevsyWorkspaceOwnership(listDevsyWorkspaces(), devsyId, worktreePath);
7674
- if (ownership.status === "conflict") throw new Error(ownership.reason);
7675
- return ownership;
7676
- }
7677
- function mutateOwnedDevsyWorkspace(action2, devsyId, worktreePath) {
7678
- return withMutationLock(`Devsy ${action2}`, worktreePath, () => {
7679
- const before = inspectExactOwnership(devsyId, worktreePath);
7680
- if (before.status === "absent") return { status: "absent" };
7681
- runDevsyAction(action2, devsyId);
7682
- let after = inspectExactOwnership(devsyId, worktreePath);
7683
- if (action2 === "stop" && after.status !== "owned") {
7684
- throw new Error(
7685
- `Devsy workspace '${devsyId}' no longer owns '${worktreePath}' after provider stop.`
7686
- );
7687
- }
7688
- if (action2 === "delete" && after.status === "owned") {
7689
- const runtime = inspectDevsyRuntimeStatus(devsyId);
7690
- if (runtime !== "not-found") {
7691
- throw new Error(
7692
- `Devsy workspace '${devsyId}' still owns '${worktreePath}' after provider delete (runtime=${runtime}).`
7693
- );
7694
- }
7695
- after = inspectExactOwnership(devsyId, worktreePath);
7696
- if (after.status === "owned") {
7697
- runDevsyAction("delete", devsyId, true);
7698
- after = inspectExactOwnership(devsyId, worktreePath);
7699
- }
7700
- if (after.status !== "absent") {
7820
+ async function ensureRouteInfrastructure(apps, options = {}) {
7821
+ ensureRouterFiles();
7822
+ await ensureNetwork(DEVNET_NAME);
7823
+ const tlsCoverage = await ensureTLSHostsCovered(
7824
+ apps.map((app) => app.host),
7825
+ options
7826
+ );
7827
+ for (const app of apps) {
7828
+ if (app.protocol === "tcp") {
7829
+ if (!isTLSEnabled()) {
7701
7830
  throw new Error(
7702
- `Devsy workspace '${devsyId}' still owns '${worktreePath}' after forced provider delete.`
7831
+ `TCP route '${app.name}' requires TLS. Run: ${tlsSetupCommand(options.repoPath)}`
7703
7832
  );
7704
7833
  }
7834
+ activateTcpProtocol(app.tcpProtocol);
7705
7835
  }
7706
- return { status: "changed" };
7707
- });
7836
+ }
7837
+ startRouterStack();
7838
+ return tlsCoverage;
7708
7839
  }
7709
- function stopOwnedDevsyWorkspace(devsyId, worktreePath) {
7710
- return mutateOwnedDevsyWorkspace("stop", devsyId, worktreePath);
7840
+ async function replacePublishedProxyRoutes(repoPath, config, workspace, options = {}) {
7841
+ const apps = proxyAppsFromConfig(config);
7842
+ const tlsCoverage = options.prepareInfrastructure === false ? { refreshed: false } : await ensureRouteInfrastructure(apps, { repoPath });
7843
+ const routes = apps.map((app) => {
7844
+ const upstream = parseUpstream(app.upstream);
7845
+ return {
7846
+ name: app.name,
7847
+ host: app.host,
7848
+ protocol: app.protocol,
7849
+ tcpProtocol: app.protocol === "tcp" ? app.tcpProtocol : void 0,
7850
+ repoPath,
7851
+ port: upstream.port,
7852
+ mode: "proxy",
7853
+ upstreamHost: upstream.upstreamHost,
7854
+ workspace
7855
+ };
7856
+ });
7857
+ replaceHostRoutesForRepo(repoPath, routes);
7858
+ return { routes, tlsRefreshed: tlsCoverage.refreshed };
7711
7859
  }
7712
- function deleteOwnedDevsyWorkspace(devsyId, worktreePath) {
7713
- return mutateOwnedDevsyWorkspace("delete", devsyId, worktreePath);
7860
+ var init_route_publication = __esm({
7861
+ "src/core/route-publication.ts"() {
7862
+ "use strict";
7863
+ init_docker();
7864
+ init_host_routes();
7865
+ init_router();
7866
+ init_tls();
7867
+ }
7868
+ });
7869
+
7870
+ // src/core/workspace-ownership.ts
7871
+ function commandError(command, repoPath, stderr) {
7872
+ return new Error(
7873
+ `${command} failed for '${repoPath}': ${stderr?.trim() || "not a Git repository"}`
7874
+ );
7714
7875
  }
7715
- function assertDevsyTarget(devsyId, repoPath) {
7716
- const workspaces = listDevsyWorkspaces();
7717
- const existing = selectDevsyWorkspace(workspaces, repoPath);
7718
- const id = devsyId ?? existing?.id;
7719
- if (id) {
7720
- const before = inspectDevsyWorkspaceOwnership(workspaces, id, repoPath);
7721
- if (before.status === "conflict") throw new Error(before.reason);
7876
+ function resolveGitCommonDir(repoPath) {
7877
+ const result = (0, import_node_child_process15.spawnSync)("git", ["-C", repoPath, "rev-parse", "--git-common-dir"], {
7878
+ encoding: "utf-8",
7879
+ env: READ_ONLY_GIT_ENV
7880
+ });
7881
+ const output2 = result.stdout.trim();
7882
+ if (result.status !== 0 || !output2) {
7883
+ throw commandError("Could not resolve the Git common directory", repoPath, result.stderr);
7722
7884
  }
7723
- return id;
7885
+ return comparableWorkspacePath(import_node_path20.default.isAbsolute(output2) ? output2 : import_node_path20.default.resolve(repoPath, output2));
7724
7886
  }
7725
- async function startDevsyWorkspace(options) {
7726
- const activity = options.recreate ? "Devsy recreate" : "Devsy start";
7727
- let agent;
7887
+ function resolveGitTopLevel(repoPath) {
7888
+ const result = (0, import_node_child_process15.spawnSync)("git", ["-C", repoPath, "rev-parse", "--show-toplevel"], {
7889
+ encoding: "utf-8",
7890
+ env: READ_ONLY_GIT_ENV
7891
+ });
7892
+ const output2 = result.stdout.trim();
7893
+ if (result.status !== 0 || !output2) {
7894
+ throw commandError("Could not resolve the Git checkout root", repoPath, result.stderr);
7895
+ }
7896
+ return comparableWorkspacePath(output2);
7897
+ }
7898
+ function listGitWorktrees(repoPath) {
7899
+ const result = (0, import_node_child_process15.spawnSync)("git", ["-C", repoPath, "worktree", "list", "--porcelain"], {
7900
+ encoding: "utf-8",
7901
+ env: READ_ONLY_GIT_ENV
7902
+ });
7903
+ if (result.status !== 0) {
7904
+ throw commandError("git worktree list", repoPath, result.stderr);
7905
+ }
7906
+ const worktrees = [];
7907
+ let current = {};
7908
+ const finish = () => {
7909
+ if (!current.path) return;
7910
+ worktrees.push({
7911
+ path: comparableWorkspacePath(current.path),
7912
+ branch: current.branch,
7913
+ locked: current.locked ?? false,
7914
+ prunable: current.prunable ?? false
7915
+ });
7916
+ current = {};
7917
+ };
7918
+ for (const line of `${result.stdout}
7919
+ `.split("\n")) {
7920
+ if (line.startsWith("worktree ")) {
7921
+ finish();
7922
+ current.path = line.slice("worktree ".length).trim();
7923
+ } else if (line.startsWith("branch ")) {
7924
+ current.branch = line.slice("branch ".length).trim().replace(/^refs\/heads\//, "");
7925
+ } else if (line === "locked" || line.startsWith("locked ")) {
7926
+ current.locked = true;
7927
+ } else if (line === "prunable" || line.startsWith("prunable ")) {
7928
+ current.prunable = true;
7929
+ } else if (line === "") {
7930
+ finish();
7931
+ }
7932
+ }
7933
+ return worktrees;
7934
+ }
7935
+ function ownershipDirectory(repoPath) {
7936
+ return import_node_path20.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
7937
+ }
7938
+ function validateWorkspace(value, label) {
7939
+ if (typeof value !== "string" || wsFromBranch(value) !== value) {
7940
+ throw new Error(`invalid workspace ownership ${label}`);
7941
+ }
7942
+ return value;
7943
+ }
7944
+ function validateTimestamp(value, label) {
7945
+ if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
7946
+ throw new Error(`invalid workspace ownership ${label}`);
7947
+ }
7948
+ return value;
7949
+ }
7950
+ function validateRecord(value, expectedWorkspace) {
7951
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
7952
+ throw new Error("invalid workspace ownership record");
7953
+ }
7954
+ const candidate = value;
7955
+ if (candidate.version !== OWNERSHIP_VERSION) {
7956
+ throw new Error(`unsupported workspace ownership version '${String(candidate.version)}'`);
7957
+ }
7958
+ const workspace = validateWorkspace(candidate.workspace, "workspace");
7959
+ if (expectedWorkspace && workspace !== expectedWorkspace) {
7960
+ throw new Error(
7961
+ `workspace ownership file '${expectedWorkspace}' contains identity '${workspace}'`
7962
+ );
7963
+ }
7964
+ if (typeof candidate.worktreePath !== "string" || !import_node_path20.default.isAbsolute(candidate.worktreePath)) {
7965
+ throw new Error("invalid workspace ownership worktreePath");
7966
+ }
7967
+ if (candidate.branch !== null && typeof candidate.branch !== "string") {
7968
+ throw new Error("invalid workspace ownership branch");
7969
+ }
7970
+ const devpodId = validateWorkspace(candidate.devpodId, "devpodId");
7971
+ return {
7972
+ version: OWNERSHIP_VERSION,
7973
+ workspace,
7974
+ worktreePath: comparableWorkspacePath(candidate.worktreePath),
7975
+ branch: candidate.branch,
7976
+ devpodId,
7977
+ createdAt: validateTimestamp(candidate.createdAt, "createdAt"),
7978
+ updatedAt: validateTimestamp(candidate.updatedAt, "updatedAt")
7979
+ };
7980
+ }
7981
+ function recordPath(repoPath, workspace) {
7982
+ return import_node_path20.default.join(
7983
+ ownershipDirectory(repoPath),
7984
+ `${validateWorkspace(workspace, "workspace")}.json`
7985
+ );
7986
+ }
7987
+ function readRecordFile(filePath, expectedWorkspace) {
7988
+ let parsed;
7728
7989
  try {
7729
- agent = requireReadyDevsyAgent();
7990
+ parsed = JSON.parse(import_node_fs20.default.readFileSync(filePath, "utf-8"));
7730
7991
  } catch (error) {
7731
- if (!(error instanceof DevsyAgentReadinessError)) throw error;
7732
- const repair = devsyAgentRepairSuggestion(error.inspection);
7733
- throw new Error(`${error.message}. ${repair}`);
7992
+ if (error instanceof SyntaxError) {
7993
+ throw new Error(`invalid workspace ownership JSON at '${filePath}'`);
7994
+ }
7995
+ throw error;
7734
7996
  }
7735
- return withMutationLockAsync(activity, options.repoPath, async () => {
7736
- let devsyId = assertDevsyTarget(options.devsyId, options.repoPath);
7737
- if (devsyId && options.recreate) {
7738
- const attached = listDevsyWorkspaces();
7739
- const ownership = inspectDevsyWorkspaceOwnership(attached, devsyId, options.repoPath);
7740
- if (ownership.status !== "owned") {
7741
- throw new Error(`Cannot recreate Devsy workspace '${devsyId}' without one exact owner.`);
7997
+ return validateRecord(parsed, expectedWorkspace);
7998
+ }
7999
+ function readWorkspaceOwnership(repoPath, workspace) {
8000
+ const filePath = recordPath(repoPath, workspace);
8001
+ try {
8002
+ return readRecordFile(filePath, workspace);
8003
+ } catch (error) {
8004
+ if (error.code === "ENOENT") return void 0;
8005
+ throw error;
8006
+ }
8007
+ }
8008
+ function listWorkspaceOwnership(repoPath) {
8009
+ const directory = ownershipDirectory(repoPath);
8010
+ return listWorkspaceOwnershipInDirectory(directory);
8011
+ }
8012
+ function listWorkspaceOwnershipInDirectory(directory) {
8013
+ let entries;
8014
+ try {
8015
+ entries = import_node_fs20.default.readdirSync(directory, { withFileTypes: true });
8016
+ } catch (error) {
8017
+ if (error.code === "ENOENT") return [];
8018
+ throw error;
8019
+ }
8020
+ return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
8021
+ const workspace = entry.name.slice(0, -".json".length);
8022
+ validateWorkspace(workspace, "filename");
8023
+ return readRecordFile(import_node_path20.default.join(directory, entry.name), workspace);
8024
+ });
8025
+ }
8026
+ function writeWorkspaceOwnershipInDirectory(directory, input2) {
8027
+ const workspace = validateWorkspace(input2.workspace, "workspace");
8028
+ const devpodId = validateWorkspace(input2.devpodId, "devpodId");
8029
+ const worktreePath = comparableWorkspacePath(input2.worktreePath);
8030
+ const filePath = import_node_path20.default.join(directory, `${workspace}.json`);
8031
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8032
+ const records = listWorkspaceOwnershipInDirectory(directory);
8033
+ const existing = records.find((record2) => record2.workspace === workspace);
8034
+ if (existing && !sameWorkspacePath(existing.worktreePath, worktreePath)) {
8035
+ throw new Error(
8036
+ `Workspace '${workspace}' already belongs to '${existing.worktreePath}', refusing '${worktreePath}'.`
8037
+ );
8038
+ }
8039
+ const pathOwner = records.find(
8040
+ (record2) => record2.workspace !== workspace && sameWorkspacePath(record2.worktreePath, worktreePath)
8041
+ );
8042
+ if (pathOwner) {
8043
+ throw new Error(
8044
+ `Worktree '${worktreePath}' is already owned by workspace '${pathOwner.workspace}'.`
8045
+ );
8046
+ }
8047
+ if (existing && existing.devpodId !== devpodId) {
8048
+ throw new Error(
8049
+ `Workspace '${workspace}' already owns DevPod '${existing.devpodId}', refusing '${devpodId}'.`
8050
+ );
8051
+ }
8052
+ const record = {
8053
+ version: OWNERSHIP_VERSION,
8054
+ workspace,
8055
+ worktreePath,
8056
+ branch: input2.branch ?? null,
8057
+ devpodId,
8058
+ createdAt: existing?.createdAt ?? validateTimestamp(now, "createdAt"),
8059
+ updatedAt: validateTimestamp(now, "updatedAt")
8060
+ };
8061
+ writeFileAtomically(filePath, `${JSON.stringify(record, null, 2)}
8062
+ `);
8063
+ return record;
8064
+ }
8065
+ function removeWorkspaceOwnershipInDirectory(directory, workspace) {
8066
+ const filePath = import_node_path20.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
8067
+ try {
8068
+ import_node_fs20.default.rmSync(filePath);
8069
+ return true;
8070
+ } catch (error) {
8071
+ if (error.code === "ENOENT") return false;
8072
+ throw error;
8073
+ }
8074
+ }
8075
+ function sameOwnershipRecord(left, right) {
8076
+ return left.version === right.version && left.workspace === right.workspace && sameWorkspacePath(left.worktreePath, right.worktreePath) && left.branch === right.branch && left.devpodId === right.devpodId && left.createdAt === right.createdAt && left.updatedAt === right.updatedAt;
8077
+ }
8078
+ function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
8079
+ const filePath = import_node_path20.default.join(
8080
+ directory,
8081
+ `${validateWorkspace(expected.workspace, "workspace")}.json`
8082
+ );
8083
+ let current;
8084
+ try {
8085
+ current = readRecordFile(filePath, expected.workspace);
8086
+ } catch (error) {
8087
+ if (error.code === "ENOENT") return "absent";
8088
+ throw error;
8089
+ }
8090
+ if (!sameOwnershipRecord(current, expected)) return "changed";
8091
+ import_node_fs20.default.rmSync(filePath);
8092
+ return "removed";
8093
+ }
8094
+ function withWorkspaceOwnershipTransaction(repoPath, operation, options = {}) {
8095
+ const directory = ownershipDirectory(repoPath);
8096
+ import_node_fs20.default.mkdirSync(directory, { recursive: true });
8097
+ return withFileLockSync(
8098
+ import_node_path20.default.join(directory, ".lock"),
8099
+ {
8100
+ activity: "workspace ownership transaction",
8101
+ target: `'${repoPath}'`,
8102
+ waitMs: options.waitMs ?? 5e3
8103
+ },
8104
+ () => operation({
8105
+ list: () => listWorkspaceOwnershipInDirectory(directory),
8106
+ write: (input2) => writeWorkspaceOwnershipInDirectory(directory, input2),
8107
+ remove: (workspace) => removeWorkspaceOwnershipInDirectory(directory, workspace),
8108
+ removeIfMatches: (expected) => removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected)
8109
+ })
8110
+ );
8111
+ }
8112
+ function providerPathOwner(providerWorkspaces, worktreePath) {
8113
+ const owners = providerWorkspaces.filter(
8114
+ (workspace) => sameWorkspacePath(workspace.source.localFolder, worktreePath)
8115
+ );
8116
+ if (owners.length > 1) {
8117
+ throw new Error(
8118
+ `Worktree '${worktreePath}' is registered by multiple workspace runtimes; no identity was claimed.`
8119
+ );
8120
+ }
8121
+ return owners[0];
8122
+ }
8123
+ function persistedWorkspaceOwners(repoPath, worktreePath) {
8124
+ const owners = /* @__PURE__ */ new Map();
8125
+ for (const worktree of listGitWorktrees(repoPath)) {
8126
+ if (sameWorkspacePath(worktree.path, worktreePath)) continue;
8127
+ let workspace;
8128
+ try {
8129
+ workspace = readPersistedWorkspace(worktree.path);
8130
+ } catch (error) {
8131
+ if (!import_node_fs20.default.existsSync(worktree.path)) continue;
8132
+ throw error;
8133
+ }
8134
+ if (!workspace) continue;
8135
+ const existing = owners.get(workspace);
8136
+ if (existing && !sameWorkspacePath(existing, worktree.path)) {
8137
+ throw new Error(`Persisted workspace identity '${workspace}' belongs to multiple worktrees.`);
8138
+ }
8139
+ owners.set(workspace, worktree.path);
8140
+ }
8141
+ return owners;
8142
+ }
8143
+ function claimConflict(workspace, devpodId, worktreePath, records, providerWorkspaces, persistedOwners, exactRecord) {
8144
+ const recordOwner = records.find(
8145
+ (record) => record !== exactRecord && (record.workspace === workspace || record.devpodId === devpodId)
8146
+ );
8147
+ if (recordOwner) {
8148
+ return `workspace owner record '${recordOwner.workspace}' for '${recordOwner.worktreePath}'`;
8149
+ }
8150
+ const providerOwner = providerWorkspaces.find(
8151
+ (providerWorkspace) => providerWorkspace.id === devpodId && !sameWorkspacePath(providerWorkspace.source.localFolder, worktreePath)
8152
+ );
8153
+ if (providerOwner) {
8154
+ return `workspace runtime identity '${providerOwner.id}' already belongs to '${providerOwner.source.localFolder}'`;
8155
+ }
8156
+ const persistedOwner = persistedOwners.get(workspace);
8157
+ if (persistedOwner) {
8158
+ return `persisted checkout metadata for '${persistedOwner}'`;
8159
+ }
8160
+ return void 0;
8161
+ }
8162
+ function claimWorkspaceIdentity(repoPath, input2) {
8163
+ const worktreePath = comparableWorkspacePath(repoPath);
8164
+ const exactProvider = providerPathOwner(input2.providerWorkspaces, worktreePath);
8165
+ return withWorkspaceOwnershipTransaction(repoPath, (transaction) => {
8166
+ const records = transaction.list();
8167
+ const exactRecords = records.filter(
8168
+ (record) => sameWorkspacePath(record.worktreePath, worktreePath)
8169
+ );
8170
+ if (exactRecords.length > 1) {
8171
+ throw new Error(
8172
+ `Worktree '${worktreePath}' has multiple workspace owner records; no identity was claimed.`
8173
+ );
8174
+ }
8175
+ const exactRecord = exactRecords[0];
8176
+ const persisted = readPersistedWorkspace(worktreePath);
8177
+ const persistedOwners = persistedWorkspaceOwners(repoPath, worktreePath);
8178
+ if (exactRecord) {
8179
+ if (persisted && persisted !== exactRecord.workspace) {
8180
+ throw new Error(
8181
+ `Persisted workspace identity '${persisted}' disagrees with owner record '${exactRecord.workspace}'.`
8182
+ );
7742
8183
  }
8184
+ if (exactProvider && exactProvider.id !== exactRecord.devpodId) {
8185
+ throw new Error(
8186
+ `Workspace runtime '${exactProvider.id}' disagrees with owner record '${exactRecord.devpodId}'.`
8187
+ );
8188
+ }
8189
+ const conflict2 = claimConflict(
8190
+ exactRecord.workspace,
8191
+ exactRecord.devpodId,
8192
+ worktreePath,
8193
+ records,
8194
+ input2.providerWorkspaces,
8195
+ persistedOwners,
8196
+ exactRecord
8197
+ );
8198
+ if (conflict2) {
8199
+ throw new Error(
8200
+ `Workspace '${exactRecord.workspace}' conflicts with ${conflict2}; no identity was claimed.`
8201
+ );
8202
+ }
8203
+ if (!persisted) persistWorkspace(worktreePath, exactRecord.workspace);
8204
+ return transaction.write({
8205
+ workspace: exactRecord.workspace,
8206
+ worktreePath,
8207
+ branch: input2.branch ?? null,
8208
+ devpodId: exactRecord.devpodId
8209
+ });
7743
8210
  }
7744
- if (!devsyId && options.recreate) {
7745
- throw new Error("Cannot recreate a Devsy workspace before its exact id is known.");
8211
+ if (persisted && exactProvider && persisted !== exactProvider.id) {
8212
+ throw new Error(
8213
+ `Persisted workspace identity '${persisted}' disagrees with workspace runtime '${exactProvider.id}'.`
8214
+ );
7746
8215
  }
7747
- const args = ["workspace", "up", options.repoPath];
7748
- if (devsyId) args.push("--id", devsyId);
7749
- if (options.devcontainerPath) args.push("--devcontainer", options.devcontainerPath);
7750
- args.push("--ide-launch", "skip");
7751
- if (options.workspace) {
7752
- args.push(
7753
- "--workspace-env",
7754
- `WORKSPACE=${options.workspace.token}`,
7755
- "--workspace-env",
7756
- `DEVROUTER_WORKSPACE=${options.workspace.token}`
8216
+ let workspace = persisted ?? exactProvider?.id;
8217
+ let devpodId = exactProvider?.id ?? persisted;
8218
+ if (!workspace || !devpodId) {
8219
+ if (input2.unavailableRuntimes.length > 0) {
8220
+ throw new Error(
8221
+ `Cannot claim a new workspace identity because these runtime registries are unavailable: ${input2.unavailableRuntimes.join(", ")}.`
8222
+ );
8223
+ }
8224
+ const candidate = workspaceIdentityCandidates(input2.source).find(
8225
+ (next) => !claimConflict(
8226
+ next,
8227
+ next,
8228
+ worktreePath,
8229
+ records,
8230
+ input2.providerWorkspaces,
8231
+ persistedOwners
8232
+ )
7757
8233
  );
8234
+ if (!candidate) {
8235
+ throw new Error(
8236
+ `Could not allocate a collision-safe workspace identity for '${worktreePath}'.`
8237
+ );
8238
+ }
8239
+ workspace = candidate;
8240
+ devpodId = candidate;
7758
8241
  }
7759
- if (options.inactivityTimeout) {
7760
- args.push("--provider-option", `INACTIVITY_TIMEOUT=${options.inactivityTimeout}`);
8242
+ const conflict = claimConflict(
8243
+ workspace,
8244
+ devpodId,
8245
+ worktreePath,
8246
+ records,
8247
+ input2.providerWorkspaces,
8248
+ persistedOwners
8249
+ );
8250
+ if (conflict) {
8251
+ throw new Error(
8252
+ `Workspace '${workspace}' conflicts with ${conflict}; no identity was claimed.`
8253
+ );
7761
8254
  }
7762
- if (options.recreate) args.push("--recreate");
7763
- const env = { ...process.env };
7764
- env.DEVSY_AGENT_BINARY = agent.binaryPath;
7765
- if (options.workspace) {
7766
- env.WORKSPACE = options.workspace.token;
7767
- env.DEVROUTER_WORKSPACE = options.workspace.token;
7768
- env.DEVROUTER_GIT_COMMON_DIR = options.workspace.gitCommonDir;
7769
- env.DEVCONTAINER_COMPOSE_OVERLAY = "docker-compose.devrouter.yml";
7770
- } else {
7771
- delete env.WORKSPACE;
7772
- delete env.DEVROUTER_WORKSPACE;
7773
- delete env.DEVROUTER_GIT_COMMON_DIR;
7774
- delete env.DEVCONTAINER_COMPOSE_OVERLAY;
8255
+ const written = transaction.write({
8256
+ workspace,
8257
+ worktreePath,
8258
+ branch: input2.branch ?? null,
8259
+ devpodId
8260
+ });
8261
+ try {
8262
+ persistWorkspace(worktreePath, workspace);
8263
+ } catch (error) {
8264
+ const cleanup = transaction.removeIfMatches(written);
8265
+ if (cleanup !== "removed") {
8266
+ const detail = error instanceof Error ? error.message : String(error);
8267
+ throw new Error(
8268
+ `Could not persist workspace identity and owner-record rollback was '${cleanup}': ${detail}`
8269
+ );
8270
+ }
8271
+ throw error;
7775
8272
  }
7776
- const result = await runDevsyUp(args, env, options.quiet ?? false);
7777
- if (result.status !== 0) {
7778
- let message = `devsy workspace up failed for '${devsyId ?? options.repoPath}'.`;
7779
- if (result.error?.message) message += ` ${result.error.message}`;
7780
- if (AGENT_ACQUISITION_RE.test(result.stderrTail)) {
7781
- message += ` Devsy rejected the verified agent source. Run: ${DEVSY_AGENT_SETUP_COMMAND}`;
8273
+ return written;
8274
+ });
8275
+ }
8276
+ function removeWorkspaceOwnership(repoPath, workspace) {
8277
+ return withWorkspaceOwnershipTransaction(
8278
+ repoPath,
8279
+ (transaction) => transaction.remove(workspace)
8280
+ );
8281
+ }
8282
+ function inspectWorkspaceOwnership(record, worktrees, devpods) {
8283
+ const worktree = worktrees.find(
8284
+ (candidate) => sameWorkspacePath(candidate.path, record.worktreePath)
8285
+ );
8286
+ const devpodOwnership = devpods ? inspectDevpodWorkspaceOwnership(devpods, record.devpodId, record.worktreePath) : void 0;
8287
+ const devpodStatus = devpodOwnership?.status ?? "unknown";
8288
+ if (devpodStatus === "conflict") {
8289
+ return { ownerStatus: "conflict", devpodStatus, worktree };
8290
+ }
8291
+ if (worktree?.locked) {
8292
+ return { ownerStatus: "locked", devpodStatus, worktree };
8293
+ }
8294
+ if ((!worktree || worktree.prunable) && import_node_fs20.default.existsSync(record.worktreePath)) {
8295
+ return { ownerStatus: "conflict", devpodStatus, worktree };
8296
+ }
8297
+ if (!worktree || worktree.prunable) {
8298
+ return { ownerStatus: "missing", devpodStatus, worktree };
8299
+ }
8300
+ let persisted;
8301
+ try {
8302
+ persisted = readPersistedWorkspace(worktree.path);
8303
+ } catch {
8304
+ return { ownerStatus: "conflict", devpodStatus, worktree };
8305
+ }
8306
+ return {
8307
+ ownerStatus: persisted === record.workspace ? "present" : "conflict",
8308
+ devpodStatus,
8309
+ worktree
8310
+ };
8311
+ }
8312
+ function listMissingWorkspaceOwnership(repoPath) {
8313
+ const worktrees = listGitWorktrees(repoPath);
8314
+ return listWorkspaceOwnership(repoPath).filter(
8315
+ (record) => inspectWorkspaceOwnership(record, worktrees, void 0).ownerStatus === "missing"
8316
+ );
8317
+ }
8318
+ var import_node_child_process15, import_node_fs20, import_node_path20, READ_ONLY_GIT_ENV, OWNERSHIP_VERSION, OWNERSHIP_DIR;
8319
+ var init_workspace_ownership = __esm({
8320
+ "src/core/workspace-ownership.ts"() {
8321
+ "use strict";
8322
+ import_node_child_process15 = require("child_process");
8323
+ import_node_fs20 = __toESM(require("fs"));
8324
+ import_node_path20 = __toESM(require("path"));
8325
+ init_atomic_file();
8326
+ init_devpod_workspaces();
8327
+ init_file_lock();
8328
+ init_workspace();
8329
+ READ_ONLY_GIT_ENV = { ...process.env, GIT_OPTIONAL_LOCKS: "0" };
8330
+ OWNERSHIP_VERSION = 1;
8331
+ OWNERSHIP_DIR = import_node_path20.default.join("devrouter", "workspaces");
8332
+ }
8333
+ });
8334
+
8335
+ // src/core/managed-devsy-stop.ts
8336
+ function sameSet(left, right) {
8337
+ return JSON.stringify([...left].sort()) === JSON.stringify([...right].sort());
8338
+ }
8339
+ function containerIdentity(container) {
8340
+ return JSON.stringify({ id: container.id, labels: container.labels, mounts: container.mounts });
8341
+ }
8342
+ function stopRetainedManagedDevsyWorkspace(options) {
8343
+ const { repoPath, devsyId } = options;
8344
+ const linked = isLinkedWorktree(repoPath);
8345
+ const workspace = linked ? resolveWorktreeWorkspace(repoPath) : void 0;
8346
+ const retainedState = readManagedRuntimeState(repoPath, workspace);
8347
+ if (!retainedState) return false;
8348
+ const state = retainedState;
8349
+ if (state.devpodId !== devsyId || linked && !workspace) {
8350
+ throw new Error("Managed stop requires the exact retained workspace identity.");
8351
+ }
8352
+ const workspaceEnv = workspace ? { token: workspace, gitCommonDir: resolveGitCommonDir(repoPath) } : void 0;
8353
+ function registration() {
8354
+ resetWorkspaceRuntimeCaches();
8355
+ if (resolveWorkspaceRuntimeOrDefault(repoPath) !== "devsy") {
8356
+ throw new Error("Managed stop provider selection changed.");
8357
+ }
8358
+ const owner = inspectDevsyWorkspaceOwnership(listDevsyWorkspaces(), devsyId, repoPath);
8359
+ if (owner.status !== "owned") {
8360
+ throw new Error("Managed stop requires one exact retained Devsy registration.");
8361
+ }
8362
+ if (workspace) {
8363
+ const record = readWorkspaceOwnership(repoPath, workspace);
8364
+ if (!record || record.devpodId !== devsyId || !sameWorkspacePath(record.worktreePath, repoPath) || resolveWorktreeWorkspace(repoPath) !== workspace || resolveGitCommonDir(repoPath) !== workspaceEnv?.gitCommonDir) {
8365
+ throw new Error("Managed stop workspace ownership changed.");
8366
+ }
8367
+ }
8368
+ return owner.workspace;
8369
+ }
8370
+ const context = registration().context;
8371
+ if (!context || !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(context) || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(devsyId)) {
8372
+ throw new Error("Managed stop requires a valid exact Devsy context and workspace ID.");
8373
+ }
8374
+ const devsyRoot = import_node_path21.default.resolve(process.env.DEVSY_HOME || import_node_path21.default.join(import_node_os2.default.homedir(), ".devsy"));
8375
+ const featureDirectory = import_node_path21.default.join(
8376
+ devsyRoot,
8377
+ "contexts",
8378
+ context,
8379
+ "workspaces",
8380
+ devsyId,
8381
+ "agent",
8382
+ ".docker-compose"
8383
+ );
8384
+ function prove(previous) {
8385
+ if (registration().context !== context || JSON.stringify(readManagedRuntimeState(repoPath, workspace)) !== JSON.stringify(state)) {
8386
+ throw new Error("Managed stop retained context or runtime record changed.");
8387
+ }
8388
+ const runtime = loadRuntimeConfig(repoPath, workspace ?? "", state.profile);
8389
+ const managed = runtime.config.managedRuntime;
8390
+ if (!managed || runtime.profile !== state.profile || runtime.workspace !== workspace) {
8391
+ throw new Error("Managed stop requires the recorded managed profile.");
8392
+ }
8393
+ const plan = inspectManagedDevcontainerConfig({
8394
+ repoPath,
8395
+ config: runtime.config,
8396
+ profile: runtime.resolvedProfile,
8397
+ linked
8398
+ });
8399
+ const selectedProcesses = runtime.resolvedProfile?.processes;
8400
+ const processes = !runtime.resolvedProfile || selectedProcesses?.length === 1 && selectedProcesses[0] === "*" ? managed.processes : selectedProcesses ?? [];
8401
+ if (state.sourceConfigSha256 !== plan.sourceConfigSha256 || state.effectiveConfigSha256 !== plan.effectiveConfigSha256 || !sameSet(
8402
+ state.desired.apps,
8403
+ proxyAppsFromConfig(runtime.config).map((app) => app.name)
8404
+ ) || !sameSet(state.desired.services, plan.desiredProfileServices) || !sameSet(state.desired.processes, processes) || inspectManagedDevcontainerGeneratedConfig(plan).status !== "valid") {
8405
+ throw new Error("Managed stop requires unchanged recorded resources and configuration.");
8406
+ }
8407
+ const containers = inspectManagedStopContainers(state.composeProject);
8408
+ const services = /* @__PURE__ */ new Set();
8409
+ for (const container of containers) {
8410
+ const service = container.labels["com.docker.compose.service"] ?? "";
8411
+ if (!plan.nativeRunServices.includes(service) || services.has(service) || container.labels["com.docker.compose.project"] !== state.composeProject || !sameWorkspacePath(
8412
+ container.labels["com.docker.compose.project.working_dir"] ?? "",
8413
+ plan.composeDirectory
8414
+ )) {
8415
+ throw new Error("Managed stop found unexpected or duplicate project membership.");
7782
8416
  }
7783
- if (failedStartMayHaveAttached(devsyId, options.repoPath)) {
7784
- throw new DevsyStartPostconditionError(message);
8417
+ services.add(service);
8418
+ const files = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").map((file) => file.trim());
8419
+ if (files.some((file) => !file || !import_node_path21.default.isAbsolute(file)) || new Set(files.map((file) => import_node_path21.default.resolve(file))).size !== files.length || files.length < plan.composeFiles.length || plan.composeFiles.some((file, index) => !sameWorkspacePath(file, files[index]))) {
8420
+ throw new Error("Managed stop Compose file identity changed.");
7785
8421
  }
7786
- throw new Error(message);
8422
+ for (const file of files.slice(plan.composeFiles.length)) {
8423
+ if (import_node_path21.default.dirname(file) !== featureDirectory || !/^docker-compose\.devcontainer\.containerFeatures-[a-zA-Z0-9_-]+\.yml$/.test(
8424
+ import_node_path21.default.basename(file)
8425
+ ) || import_node_fs21.default.realpathSync(file) !== import_node_path21.default.join(import_node_fs21.default.realpathSync(devsyRoot), import_node_path21.default.relative(devsyRoot, file))) {
8426
+ throw new Error("Managed stop refuses a foreign or escaped provider Compose file.");
8427
+ }
8428
+ }
8429
+ if (previous) {
8430
+ const retained = previous.find((entry) => entry.id === container.id);
8431
+ if (!retained || containerIdentity(retained) !== containerIdentity(container) || !retained.state.Running && container.state.Running) {
8432
+ throw new Error("Managed stop container identity or quiescent state changed.");
8433
+ }
8434
+ }
8435
+ }
8436
+ if (plan.desiredServices.some((service) => !services.has(service)) || previous && !sameSet(
8437
+ previous.map((c) => c.id),
8438
+ containers.map((c) => c.id)
8439
+ )) {
8440
+ throw new Error("Managed stop cannot prove the complete retained service population.");
8441
+ }
8442
+ const primary = containers.find(
8443
+ (c) => c.labels["com.docker.compose.service"] === plan.primaryService
8444
+ );
8445
+ if (primary?.mounts.filter(
8446
+ (mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath)
8447
+ ).length !== 1) {
8448
+ throw new Error("Managed stop cannot prove the exact primary workspace mount.");
8449
+ }
8450
+ assertManagedContainerConfigUnchanged({ plan, containers, workspace: workspaceEnv });
8451
+ if (registration().context !== context) {
8452
+ throw new Error("Managed stop provider context changed during inspection.");
7787
8453
  }
8454
+ const status = inspectDevsyRuntimeStatus(devsyId);
8455
+ if (status !== "running" && status !== "stopped" || status === "running" !== primary.state.Running) {
8456
+ throw new Error("Managed stop requires consistent provider and primary container state.");
8457
+ }
8458
+ return { containers, primary, status };
8459
+ }
8460
+ const initial = prove();
8461
+ let providerError;
8462
+ let providerFailed = false;
8463
+ if (initial.status === "running") {
7788
8464
  try {
7789
- const attached = listDevsyWorkspaces();
7790
- devsyId ??= selectDevsyWorkspace(attached, options.repoPath)?.id;
7791
- if (!devsyId) {
7792
- throw new Error(`Devsy did not attach '${options.repoPath}' after startup.`);
8465
+ options.stopProvider();
8466
+ } catch (error) {
8467
+ providerFailed = true;
8468
+ providerError = error;
8469
+ }
8470
+ }
8471
+ try {
8472
+ const stopped = () => {
8473
+ const current = prove(initial.containers);
8474
+ if (current.status !== "stopped" || current.primary.state.Running) {
8475
+ throw new Error("Managed stop has not stopped the exact primary container.");
7793
8476
  }
7794
- const ownership = inspectDevsyWorkspaceOwnership(attached, devsyId, options.repoPath);
7795
- if (ownership.status === "conflict") throw new Error(ownership.reason);
7796
- if (ownership.status !== "owned") {
7797
- throw new Error(
7798
- `Devsy did not attach '${options.repoPath}' as '${devsyId}' after startup.`
7799
- );
8477
+ return current.containers;
8478
+ };
8479
+ stopped();
8480
+ for (const retained of initial.containers) {
8481
+ if (!retained.state.Running || retained.id === initial.primary.id) continue;
8482
+ const current = stopped().find((entry) => entry.id === retained.id);
8483
+ if (current?.state.Running) {
8484
+ stopExactManagedService(current.id, current.labels["com.docker.compose.service"] ?? "", {
8485
+ timeoutMs: 3e4
8486
+ });
7800
8487
  }
7801
- return devsyId;
7802
- } catch (error) {
7803
- const message = error instanceof Error ? error.message : String(error);
7804
- throw new DevsyStartPostconditionError(message);
7805
8488
  }
7806
- });
8489
+ if (stopped().some((container) => container.state.Running)) {
8490
+ throw new Error("Managed stop left a retained service running.");
8491
+ }
8492
+ } catch (error) {
8493
+ if (providerFailed) {
8494
+ throw new AggregateError(
8495
+ [providerError, error],
8496
+ "Devsy provider stop failed; complete retained shutdown could not be verified.",
8497
+ { cause: providerError }
8498
+ );
8499
+ }
8500
+ throw error;
8501
+ }
8502
+ if (providerFailed) throw providerError;
8503
+ return true;
7807
8504
  }
7808
- var import_node_child_process15, import_node_fs20, import_node_path20, DEVSY_MUTATION_LOCK_FILE, DEVSY_MUTATION_WAIT_MS, DevsyStartPostconditionError, AGENT_ACQUISITION_RE, DEVSY_STDERR_TAIL_BYTES;
7809
- var init_devsy_mutation = __esm({
7810
- "src/core/devsy-mutation.ts"() {
8505
+ var import_node_fs21, import_node_os2, import_node_path21;
8506
+ var init_managed_devsy_stop = __esm({
8507
+ "src/core/managed-devsy-stop.ts"() {
7811
8508
  "use strict";
7812
- import_node_child_process15 = require("child_process");
7813
- import_node_fs20 = __toESM(require("fs"));
7814
- import_node_path20 = __toESM(require("path"));
7815
- init_devsy_agent();
8509
+ import_node_fs21 = __toESM(require("fs"));
8510
+ import_node_os2 = __toESM(require("os"));
8511
+ import_node_path21 = __toESM(require("path"));
8512
+ init_devcontainer_profile();
8513
+ init_devpod_environment();
7816
8514
  init_devsy_workspaces();
7817
- init_file_lock();
7818
- init_router();
7819
- DEVSY_MUTATION_LOCK_FILE = import_node_path20.default.join(DEVROUTER_HOME, "devsy-mutation.lock");
7820
- DEVSY_MUTATION_WAIT_MS = 18e5;
7821
- DevsyStartPostconditionError = class extends Error {
7822
- };
7823
- AGENT_ACQUISITION_RE = /inject agent.*agent binary not found/i;
7824
- DEVSY_STDERR_TAIL_BYTES = 8192;
8515
+ init_managed_runtime_state();
8516
+ init_repo_config();
8517
+ init_route_publication();
8518
+ init_workspace();
8519
+ init_workspace_ownership();
8520
+ init_workspace_runtime();
7825
8521
  }
7826
8522
  });
7827
8523
 
7828
- // src/core/devpod-mutation.ts
7829
- function withMutationLock2(activity, target, operation) {
7830
- import_node_fs21.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
8524
+ // src/core/devsy-mutation.ts
8525
+ function failedStartMayHaveAttached(devsyId, repoPath) {
8526
+ try {
8527
+ const attached = listDevsyWorkspaces();
8528
+ const attachedId = devsyId ?? selectDevsyWorkspace(attached, repoPath)?.id;
8529
+ if (!attachedId) return false;
8530
+ return inspectDevsyWorkspaceOwnership(attached, attachedId, repoPath).status !== "absent";
8531
+ } catch {
8532
+ return true;
8533
+ }
8534
+ }
8535
+ function withMutationLock(activity, target, operation) {
8536
+ import_node_fs22.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
7831
8537
  return withFileLockSync(
7832
- DEVPOD_MUTATION_LOCK_FILE,
8538
+ DEVSY_MUTATION_LOCK_FILE,
7833
8539
  {
7834
8540
  activity,
7835
8541
  target: `'${target}'`,
7836
- waitMs: DEVPOD_MUTATION_WAIT_MS,
8542
+ waitMs: DEVSY_MUTATION_WAIT_MS,
7837
8543
  fair: true,
7838
8544
  onWait: createStderrWaitReporter(activity, `'${target}'`)
7839
8545
  },
7840
8546
  operation
7841
8547
  );
7842
8548
  }
7843
- function commandFailure3(result) {
8549
+ function withMutationLockAsync(activity, target, operation) {
8550
+ import_node_fs22.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
8551
+ return withFileLock(
8552
+ DEVSY_MUTATION_LOCK_FILE,
8553
+ {
8554
+ activity,
8555
+ target: `'${target}'`,
8556
+ waitMs: DEVSY_MUTATION_WAIT_MS,
8557
+ fair: true,
8558
+ onWait: createStderrWaitReporter(activity, `'${target}'`)
8559
+ },
8560
+ operation
8561
+ );
8562
+ }
8563
+ function commandFailure2(result) {
7844
8564
  return [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
7845
8565
  }
7846
- function runDevpodAction(action2, devpodId, force = false) {
7847
- const args = action2 === "delete" ? [action2, devpodId, ...force ? ["--force"] : [], "--ignore-not-found"] : [action2, devpodId];
7848
- const result = (0, import_node_child_process16.spawnSync)("devpod", args, { encoding: "utf-8" });
8566
+ function runDevsyUp(args, env, quiet) {
8567
+ return new Promise((resolve) => {
8568
+ const child = (0, import_node_child_process16.spawn)("devsy", args, {
8569
+ stdio: ["inherit", quiet ? 2 : "inherit", "pipe"],
8570
+ env
8571
+ });
8572
+ const stderr = child.stderr;
8573
+ if (!stderr) throw new Error("Devsy startup stderr pipe was not created.");
8574
+ let stderrTail = Buffer.alloc(0);
8575
+ let spawnError;
8576
+ stderr.on("data", (chunk) => {
8577
+ const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
8578
+ const writable = process.stderr.write(value);
8579
+ if (!writable) {
8580
+ stderr.pause();
8581
+ process.stderr.once("drain", () => stderr.resume());
8582
+ }
8583
+ if (value.length >= DEVSY_STDERR_TAIL_BYTES) {
8584
+ stderrTail = Buffer.from(value.subarray(value.length - DEVSY_STDERR_TAIL_BYTES));
8585
+ } else {
8586
+ const combined = Buffer.concat([stderrTail, value]);
8587
+ stderrTail = combined.length > DEVSY_STDERR_TAIL_BYTES ? combined.subarray(combined.length - DEVSY_STDERR_TAIL_BYTES) : combined;
8588
+ }
8589
+ });
8590
+ child.once("error", (error) => {
8591
+ spawnError = error;
8592
+ });
8593
+ child.once("close", (status) => {
8594
+ resolve({ status, error: spawnError, stderrTail: stderrTail.toString("utf-8") });
8595
+ });
8596
+ });
8597
+ }
8598
+ function runDevsyAction(action2, devsyId, force = false) {
8599
+ const args = action2 === "delete" ? ["delete", devsyId, ...force ? ["--force"] : [], "--ignore-not-found"] : ["stop", devsyId];
8600
+ const result = (0, import_node_child_process16.spawnSync)("devsy", ["workspace", ...args], { encoding: "utf-8" });
7849
8601
  if (result.status !== 0) {
7850
8602
  throw new Error(
7851
- `devpod ${action2}${force ? " --force" : ""} failed for '${devpodId}': ${commandFailure3(result) || "unknown error"}`
8603
+ `devsy workspace ${action2}${force ? " --force" : ""} failed for '${devsyId}': ${commandFailure2(result) || "unknown error"}`
7852
8604
  );
7853
8605
  }
7854
8606
  }
7855
- function inspectExactOwnership2(devpodId, worktreePath) {
7856
- const ownership = inspectDevpodWorkspaceOwnership(
7857
- listDevpodWorkspaces(worktreePath),
7858
- devpodId,
7859
- worktreePath
7860
- );
8607
+ function inspectExactOwnership(devsyId, worktreePath) {
8608
+ const ownership = inspectDevsyWorkspaceOwnership(listDevsyWorkspaces(), devsyId, worktreePath);
7861
8609
  if (ownership.status === "conflict") throw new Error(ownership.reason);
7862
8610
  return ownership;
7863
8611
  }
7864
- function mutateOwnedDevpodWorkspace(action2, devpodId, worktreePath) {
7865
- return withMutationLock2(`DevPod ${action2}`, worktreePath, () => {
7866
- const before = inspectExactOwnership2(devpodId, worktreePath);
8612
+ function mutateOwnedDevsyWorkspace(action2, devsyId, worktreePath) {
8613
+ return withMutationLock(`Devsy ${action2}`, worktreePath, () => {
8614
+ if (action2 === "stop" && stopRetainedManagedDevsyWorkspace({
8615
+ repoPath: worktreePath,
8616
+ devsyId,
8617
+ stopProvider: () => runDevsyAction("stop", devsyId)
8618
+ }))
8619
+ return { status: "changed" };
8620
+ const before = inspectExactOwnership(devsyId, worktreePath);
7867
8621
  if (before.status === "absent") return { status: "absent" };
7868
- runDevpodAction(action2, devpodId);
7869
- let after = inspectExactOwnership2(devpodId, worktreePath);
8622
+ runDevsyAction(action2, devsyId);
8623
+ let after = inspectExactOwnership(devsyId, worktreePath);
7870
8624
  if (action2 === "stop" && after.status !== "owned") {
7871
- throw new Error(`DevPod '${devpodId}' no longer owns '${worktreePath}' after provider stop.`);
8625
+ throw new Error(
8626
+ `Devsy workspace '${devsyId}' no longer owns '${worktreePath}' after provider stop.`
8627
+ );
7872
8628
  }
7873
8629
  if (action2 === "delete" && after.status === "owned") {
7874
- const runtime = inspectDevpodRuntimeStatus(devpodId, worktreePath);
8630
+ const runtime = inspectDevsyRuntimeStatus(devsyId);
7875
8631
  if (runtime !== "not-found") {
7876
8632
  throw new Error(
7877
- `DevPod '${devpodId}' still owns '${worktreePath}' after provider delete (runtime=${runtime}).`
8633
+ `Devsy workspace '${devsyId}' still owns '${worktreePath}' after provider delete (runtime=${runtime}).`
7878
8634
  );
7879
8635
  }
7880
- after = inspectExactOwnership2(devpodId, worktreePath);
8636
+ after = inspectExactOwnership(devsyId, worktreePath);
7881
8637
  if (after.status === "owned") {
7882
- runDevpodAction("delete", devpodId, true);
7883
- after = inspectExactOwnership2(devpodId, worktreePath);
8638
+ runDevsyAction("delete", devsyId, true);
8639
+ after = inspectExactOwnership(devsyId, worktreePath);
7884
8640
  }
7885
8641
  if (after.status !== "absent") {
7886
8642
  throw new Error(
7887
- `DevPod '${devpodId}' still owns '${worktreePath}' after forced provider delete.`
8643
+ `Devsy workspace '${devsyId}' still owns '${worktreePath}' after forced provider delete.`
7888
8644
  );
7889
8645
  }
7890
8646
  }
7891
8647
  return { status: "changed" };
7892
8648
  });
7893
8649
  }
7894
- function stopOwnedDevpodWorkspace(devpodId, worktreePath) {
7895
- if (resolveWorkspaceRuntimeOrDefault(worktreePath) === "devsy") {
7896
- const result2 = stopOwnedDevsyWorkspace(devpodId, worktreePath);
7897
- resetWorkspaceRuntimeCaches();
7898
- return result2;
7899
- }
7900
- const result = mutateOwnedDevpodWorkspace("stop", devpodId, worktreePath);
7901
- resetWorkspaceRuntimeCaches();
7902
- return result;
8650
+ function stopOwnedDevsyWorkspace(devsyId, worktreePath) {
8651
+ return mutateOwnedDevsyWorkspace("stop", devsyId, worktreePath);
7903
8652
  }
7904
- function deleteOwnedDevpodWorkspace(devpodId, worktreePath) {
7905
- if (resolveWorkspaceRuntimeOrDefault(worktreePath) === "devsy") {
7906
- const result2 = deleteOwnedDevsyWorkspace(devpodId, worktreePath);
7907
- resetWorkspaceRuntimeCaches();
7908
- return result2;
8653
+ function deleteOwnedDevsyWorkspace(devsyId, worktreePath) {
8654
+ return mutateOwnedDevsyWorkspace("delete", devsyId, worktreePath);
8655
+ }
8656
+ function assertDevsyTarget(devsyId, repoPath) {
8657
+ const workspaces = listDevsyWorkspaces();
8658
+ const existing = selectDevsyWorkspace(workspaces, repoPath);
8659
+ const id = devsyId ?? existing?.id;
8660
+ if (id) {
8661
+ const before = inspectDevsyWorkspaceOwnership(workspaces, id, repoPath);
8662
+ if (before.status === "conflict") throw new Error(before.reason);
7909
8663
  }
7910
- const result = mutateOwnedDevpodWorkspace("delete", devpodId, worktreePath);
7911
- resetWorkspaceRuntimeCaches();
7912
- return result;
8664
+ return id;
7913
8665
  }
7914
- async function startDevpodWorkspace(options) {
7915
- if (resolveWorkspaceRuntimeOrDefault(options.repoPath) === "devsy") {
7916
- try {
7917
- const result = await startDevsyWorkspace({
7918
- repoPath: options.repoPath,
7919
- devsyId: options.devpodId,
7920
- devcontainerPath: options.devcontainerPath,
7921
- recreate: options.recreate,
7922
- quiet: options.quiet,
7923
- workspace: options.workspace,
7924
- inactivityTimeout: readWorkspaceRuntimeConfig().devsyInactivityTimeout
7925
- });
7926
- resetWorkspaceRuntimeCaches();
7927
- return result;
7928
- } catch (error) {
7929
- if (error instanceof DevsyStartPostconditionError) {
7930
- resetWorkspaceRuntimeCaches();
7931
- throw new DevpodStartPostconditionError(error.message);
7932
- }
7933
- throw error;
7934
- }
8666
+ async function startDevsyWorkspace(options) {
8667
+ const activity = options.recreate ? "Devsy recreate" : "Devsy start";
8668
+ let agent;
8669
+ try {
8670
+ agent = requireReadyDevsyAgent();
8671
+ } catch (error) {
8672
+ if (!(error instanceof DevsyAgentReadinessError)) throw error;
8673
+ const repair = devsyAgentRepairSuggestion(error.inspection);
8674
+ throw new Error(`${error.message}. ${repair}`);
7935
8675
  }
7936
- const activity = options.recreate ? "DevPod recreate" : "DevPod start";
7937
- return withMutationLock2(activity, options.repoPath, () => {
7938
- const workspaces = listDevpodWorkspaces(options.repoPath);
7939
- let devpodId = options.devpodId ?? selectDevpodWorkspace(workspaces, options.repoPath)?.id;
7940
- if (devpodId) {
7941
- const before = inspectDevpodWorkspaceOwnership(workspaces, devpodId, options.repoPath);
7942
- if (before.status === "conflict") throw new Error(before.reason);
7943
- if (options.recreate && before.status !== "owned") {
7944
- throw new Error(`Cannot recreate DevPod '${devpodId}' without one exact owner.`);
8676
+ return withMutationLockAsync(activity, options.repoPath, async () => {
8677
+ let devsyId = assertDevsyTarget(options.devsyId, options.repoPath);
8678
+ if (devsyId && options.recreate) {
8679
+ const attached = listDevsyWorkspaces();
8680
+ const ownership = inspectDevsyWorkspaceOwnership(attached, devsyId, options.repoPath);
8681
+ if (ownership.status !== "owned") {
8682
+ throw new Error(`Cannot recreate Devsy workspace '${devsyId}' without one exact owner.`);
7945
8683
  }
7946
- } else if (options.recreate) {
7947
- throw new Error("Cannot recreate a DevPod before its exact id is known.");
7948
8684
  }
7949
- const args = ["up", options.repoPath];
7950
- if (devpodId) args.push("--id", devpodId);
7951
- if (options.devcontainerPath) {
7952
- args.push("--devcontainer-path", options.devcontainerPath);
8685
+ if (!devsyId && options.recreate) {
8686
+ throw new Error("Cannot recreate a Devsy workspace before its exact id is known.");
7953
8687
  }
7954
- args.push("--open-ide=false");
8688
+ const args = ["workspace", "up", options.repoPath];
8689
+ if (devsyId) args.push("--id", devsyId);
8690
+ if (options.devcontainerPath) args.push("--devcontainer", options.devcontainerPath);
8691
+ args.push("--ide-launch", "skip");
7955
8692
  if (options.workspace) {
7956
8693
  args.push(
7957
8694
  "--workspace-env",
@@ -7960,8 +8697,12 @@ async function startDevpodWorkspace(options) {
7960
8697
  `DEVROUTER_WORKSPACE=${options.workspace.token}`
7961
8698
  );
7962
8699
  }
8700
+ if (options.inactivityTimeout) {
8701
+ args.push("--provider-option", `INACTIVITY_TIMEOUT=${options.inactivityTimeout}`);
8702
+ }
7963
8703
  if (options.recreate) args.push("--recreate");
7964
8704
  const env = { ...process.env };
8705
+ env.DEVSY_AGENT_BINARY = agent.binaryPath;
7965
8706
  if (options.workspace) {
7966
8707
  env.WORKSPACE = options.workspace.token;
7967
8708
  env.DEVROUTER_WORKSPACE = options.workspace.token;
@@ -7973,515 +8714,252 @@ async function startDevpodWorkspace(options) {
7973
8714
  delete env.DEVROUTER_GIT_COMMON_DIR;
7974
8715
  delete env.DEVCONTAINER_COMPOSE_OVERLAY;
7975
8716
  }
7976
- const result = (0, import_node_child_process16.spawnSync)("devpod", args, {
7977
- stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
7978
- env
7979
- });
8717
+ const result = await runDevsyUp(args, env, options.quiet ?? false);
7980
8718
  if (result.status !== 0) {
7981
- throw new Error(`devpod up failed for '${devpodId ?? options.repoPath}'.`);
7982
- }
7983
- try {
7984
- const attached = listDevpodWorkspaces(options.repoPath);
7985
- devpodId ??= selectDevpodWorkspace(attached, options.repoPath)?.id;
7986
- if (!devpodId) {
7987
- throw new Error(`DevPod did not attach '${options.repoPath}' after startup.`);
8719
+ let message = `devsy workspace up failed for '${devsyId ?? options.repoPath}'.`;
8720
+ if (result.error?.message) message += ` ${result.error.message}`;
8721
+ if (AGENT_ACQUISITION_RE.test(result.stderrTail)) {
8722
+ message += ` Devsy rejected the verified agent source. Run: ${DEVSY_AGENT_SETUP_COMMAND}`;
7988
8723
  }
7989
- const ownership = inspectDevpodWorkspaceOwnership(attached, devpodId, options.repoPath);
7990
- if (ownership.status === "conflict") throw new Error(ownership.reason);
7991
- if (ownership.status !== "owned") {
7992
- throw new Error(
7993
- `DevPod did not attach '${options.repoPath}' as '${devpodId}' after startup.`
7994
- );
8724
+ if (failedStartMayHaveAttached(devsyId, options.repoPath)) {
8725
+ throw new DevsyStartPostconditionError(message);
7995
8726
  }
7996
- resetWorkspaceRuntimeCaches();
7997
- return devpodId;
7998
- } catch (error) {
7999
- const message = error instanceof Error ? error.message : String(error);
8000
- throw new DevpodStartPostconditionError(message);
8001
- }
8002
- });
8003
- }
8004
- var import_node_child_process16, import_node_fs21, import_node_path21, DEVPOD_MUTATION_LOCK_FILE, DEVPOD_MUTATION_WAIT_MS, DevpodStartPostconditionError;
8005
- var init_devpod_mutation = __esm({
8006
- "src/core/devpod-mutation.ts"() {
8007
- "use strict";
8008
- import_node_child_process16 = require("child_process");
8009
- import_node_fs21 = __toESM(require("fs"));
8010
- import_node_path21 = __toESM(require("path"));
8011
- init_devpod_workspaces();
8012
- init_devsy_mutation();
8013
- init_file_lock();
8014
- init_router();
8015
- init_workspace_runtime();
8016
- DEVPOD_MUTATION_LOCK_FILE = import_node_path21.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
8017
- DEVPOD_MUTATION_WAIT_MS = 18e5;
8018
- DevpodStartPostconditionError = class extends Error {
8019
- };
8020
- }
8021
- });
8022
-
8023
- // src/core/workspace-ownership.ts
8024
- function commandError(command, repoPath, stderr) {
8025
- return new Error(
8026
- `${command} failed for '${repoPath}': ${stderr?.trim() || "not a Git repository"}`
8027
- );
8028
- }
8029
- function resolveGitCommonDir(repoPath) {
8030
- const result = (0, import_node_child_process17.spawnSync)("git", ["-C", repoPath, "rev-parse", "--git-common-dir"], {
8031
- encoding: "utf-8",
8032
- env: READ_ONLY_GIT_ENV
8033
- });
8034
- const output2 = result.stdout.trim();
8035
- if (result.status !== 0 || !output2) {
8036
- throw commandError("Could not resolve the Git common directory", repoPath, result.stderr);
8037
- }
8038
- return comparableWorkspacePath(import_node_path22.default.isAbsolute(output2) ? output2 : import_node_path22.default.resolve(repoPath, output2));
8039
- }
8040
- function resolveGitTopLevel(repoPath) {
8041
- const result = (0, import_node_child_process17.spawnSync)("git", ["-C", repoPath, "rev-parse", "--show-toplevel"], {
8042
- encoding: "utf-8",
8043
- env: READ_ONLY_GIT_ENV
8044
- });
8045
- const output2 = result.stdout.trim();
8046
- if (result.status !== 0 || !output2) {
8047
- throw commandError("Could not resolve the Git checkout root", repoPath, result.stderr);
8048
- }
8049
- return comparableWorkspacePath(output2);
8050
- }
8051
- function listGitWorktrees(repoPath) {
8052
- const result = (0, import_node_child_process17.spawnSync)("git", ["-C", repoPath, "worktree", "list", "--porcelain"], {
8053
- encoding: "utf-8",
8054
- env: READ_ONLY_GIT_ENV
8055
- });
8056
- if (result.status !== 0) {
8057
- throw commandError("git worktree list", repoPath, result.stderr);
8058
- }
8059
- const worktrees = [];
8060
- let current = {};
8061
- const finish = () => {
8062
- if (!current.path) return;
8063
- worktrees.push({
8064
- path: comparableWorkspacePath(current.path),
8065
- branch: current.branch,
8066
- locked: current.locked ?? false,
8067
- prunable: current.prunable ?? false
8068
- });
8069
- current = {};
8070
- };
8071
- for (const line of `${result.stdout}
8072
- `.split("\n")) {
8073
- if (line.startsWith("worktree ")) {
8074
- finish();
8075
- current.path = line.slice("worktree ".length).trim();
8076
- } else if (line.startsWith("branch ")) {
8077
- current.branch = line.slice("branch ".length).trim().replace(/^refs\/heads\//, "");
8078
- } else if (line === "locked" || line.startsWith("locked ")) {
8079
- current.locked = true;
8080
- } else if (line === "prunable" || line.startsWith("prunable ")) {
8081
- current.prunable = true;
8082
- } else if (line === "") {
8083
- finish();
8084
- }
8085
- }
8086
- return worktrees;
8087
- }
8088
- function ownershipDirectory(repoPath) {
8089
- return import_node_path22.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
8090
- }
8091
- function validateWorkspace(value, label) {
8092
- if (typeof value !== "string" || wsFromBranch(value) !== value) {
8093
- throw new Error(`invalid workspace ownership ${label}`);
8094
- }
8095
- return value;
8096
- }
8097
- function validateTimestamp(value, label) {
8098
- if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
8099
- throw new Error(`invalid workspace ownership ${label}`);
8100
- }
8101
- return value;
8102
- }
8103
- function validateRecord(value, expectedWorkspace) {
8104
- if (!value || typeof value !== "object" || Array.isArray(value)) {
8105
- throw new Error("invalid workspace ownership record");
8106
- }
8107
- const candidate = value;
8108
- if (candidate.version !== OWNERSHIP_VERSION) {
8109
- throw new Error(`unsupported workspace ownership version '${String(candidate.version)}'`);
8110
- }
8111
- const workspace = validateWorkspace(candidate.workspace, "workspace");
8112
- if (expectedWorkspace && workspace !== expectedWorkspace) {
8113
- throw new Error(
8114
- `workspace ownership file '${expectedWorkspace}' contains identity '${workspace}'`
8115
- );
8116
- }
8117
- if (typeof candidate.worktreePath !== "string" || !import_node_path22.default.isAbsolute(candidate.worktreePath)) {
8118
- throw new Error("invalid workspace ownership worktreePath");
8119
- }
8120
- if (candidate.branch !== null && typeof candidate.branch !== "string") {
8121
- throw new Error("invalid workspace ownership branch");
8122
- }
8123
- const devpodId = validateWorkspace(candidate.devpodId, "devpodId");
8124
- return {
8125
- version: OWNERSHIP_VERSION,
8126
- workspace,
8127
- worktreePath: comparableWorkspacePath(candidate.worktreePath),
8128
- branch: candidate.branch,
8129
- devpodId,
8130
- createdAt: validateTimestamp(candidate.createdAt, "createdAt"),
8131
- updatedAt: validateTimestamp(candidate.updatedAt, "updatedAt")
8132
- };
8133
- }
8134
- function recordPath(repoPath, workspace) {
8135
- return import_node_path22.default.join(
8136
- ownershipDirectory(repoPath),
8137
- `${validateWorkspace(workspace, "workspace")}.json`
8138
- );
8139
- }
8140
- function readRecordFile(filePath, expectedWorkspace) {
8141
- let parsed;
8142
- try {
8143
- parsed = JSON.parse(import_node_fs22.default.readFileSync(filePath, "utf-8"));
8144
- } catch (error) {
8145
- if (error instanceof SyntaxError) {
8146
- throw new Error(`invalid workspace ownership JSON at '${filePath}'`);
8727
+ throw new Error(message);
8147
8728
  }
8148
- throw error;
8149
- }
8150
- return validateRecord(parsed, expectedWorkspace);
8151
- }
8152
- function readWorkspaceOwnership(repoPath, workspace) {
8153
- const filePath = recordPath(repoPath, workspace);
8154
- try {
8155
- return readRecordFile(filePath, workspace);
8156
- } catch (error) {
8157
- if (error.code === "ENOENT") return void 0;
8158
- throw error;
8159
- }
8160
- }
8161
- function listWorkspaceOwnership(repoPath) {
8162
- const directory = ownershipDirectory(repoPath);
8163
- return listWorkspaceOwnershipInDirectory(directory);
8164
- }
8165
- function listWorkspaceOwnershipInDirectory(directory) {
8166
- let entries;
8167
- try {
8168
- entries = import_node_fs22.default.readdirSync(directory, { withFileTypes: true });
8169
- } catch (error) {
8170
- if (error.code === "ENOENT") return [];
8171
- throw error;
8172
- }
8173
- return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
8174
- const workspace = entry.name.slice(0, -".json".length);
8175
- validateWorkspace(workspace, "filename");
8176
- return readRecordFile(import_node_path22.default.join(directory, entry.name), workspace);
8177
- });
8178
- }
8179
- function writeWorkspaceOwnershipInDirectory(directory, input2) {
8180
- const workspace = validateWorkspace(input2.workspace, "workspace");
8181
- const devpodId = validateWorkspace(input2.devpodId, "devpodId");
8182
- const worktreePath = comparableWorkspacePath(input2.worktreePath);
8183
- const filePath = import_node_path22.default.join(directory, `${workspace}.json`);
8184
- const now = (/* @__PURE__ */ new Date()).toISOString();
8185
- const records = listWorkspaceOwnershipInDirectory(directory);
8186
- const existing = records.find((record2) => record2.workspace === workspace);
8187
- if (existing && !sameWorkspacePath(existing.worktreePath, worktreePath)) {
8188
- throw new Error(
8189
- `Workspace '${workspace}' already belongs to '${existing.worktreePath}', refusing '${worktreePath}'.`
8190
- );
8191
- }
8192
- const pathOwner = records.find(
8193
- (record2) => record2.workspace !== workspace && sameWorkspacePath(record2.worktreePath, worktreePath)
8194
- );
8195
- if (pathOwner) {
8196
- throw new Error(
8197
- `Worktree '${worktreePath}' is already owned by workspace '${pathOwner.workspace}'.`
8198
- );
8199
- }
8200
- if (existing && existing.devpodId !== devpodId) {
8201
- throw new Error(
8202
- `Workspace '${workspace}' already owns DevPod '${existing.devpodId}', refusing '${devpodId}'.`
8203
- );
8204
- }
8205
- const record = {
8206
- version: OWNERSHIP_VERSION,
8207
- workspace,
8208
- worktreePath,
8209
- branch: input2.branch ?? null,
8210
- devpodId,
8211
- createdAt: existing?.createdAt ?? validateTimestamp(now, "createdAt"),
8212
- updatedAt: validateTimestamp(now, "updatedAt")
8213
- };
8214
- writeFileAtomically(filePath, `${JSON.stringify(record, null, 2)}
8215
- `);
8216
- return record;
8217
- }
8218
- function removeWorkspaceOwnershipInDirectory(directory, workspace) {
8219
- const filePath = import_node_path22.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
8220
- try {
8221
- import_node_fs22.default.rmSync(filePath);
8222
- return true;
8223
- } catch (error) {
8224
- if (error.code === "ENOENT") return false;
8225
- throw error;
8226
- }
8227
- }
8228
- function sameOwnershipRecord(left, right) {
8229
- return left.version === right.version && left.workspace === right.workspace && sameWorkspacePath(left.worktreePath, right.worktreePath) && left.branch === right.branch && left.devpodId === right.devpodId && left.createdAt === right.createdAt && left.updatedAt === right.updatedAt;
8729
+ try {
8730
+ const attached = listDevsyWorkspaces();
8731
+ devsyId ??= selectDevsyWorkspace(attached, options.repoPath)?.id;
8732
+ if (!devsyId) {
8733
+ throw new Error(`Devsy did not attach '${options.repoPath}' after startup.`);
8734
+ }
8735
+ const ownership = inspectDevsyWorkspaceOwnership(attached, devsyId, options.repoPath);
8736
+ if (ownership.status === "conflict") throw new Error(ownership.reason);
8737
+ if (ownership.status !== "owned") {
8738
+ throw new Error(
8739
+ `Devsy did not attach '${options.repoPath}' as '${devsyId}' after startup.`
8740
+ );
8741
+ }
8742
+ return devsyId;
8743
+ } catch (error) {
8744
+ const message = error instanceof Error ? error.message : String(error);
8745
+ throw new DevsyStartPostconditionError(message);
8746
+ }
8747
+ });
8230
8748
  }
8231
- function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
8232
- const filePath = import_node_path22.default.join(
8233
- directory,
8234
- `${validateWorkspace(expected.workspace, "workspace")}.json`
8235
- );
8236
- let current;
8237
- try {
8238
- current = readRecordFile(filePath, expected.workspace);
8239
- } catch (error) {
8240
- if (error.code === "ENOENT") return "absent";
8241
- throw error;
8749
+ var import_node_child_process16, import_node_fs22, import_node_path22, DEVSY_MUTATION_LOCK_FILE, DEVSY_MUTATION_WAIT_MS, DevsyStartPostconditionError, AGENT_ACQUISITION_RE, DEVSY_STDERR_TAIL_BYTES;
8750
+ var init_devsy_mutation = __esm({
8751
+ "src/core/devsy-mutation.ts"() {
8752
+ "use strict";
8753
+ import_node_child_process16 = require("child_process");
8754
+ import_node_fs22 = __toESM(require("fs"));
8755
+ import_node_path22 = __toESM(require("path"));
8756
+ init_devsy_agent();
8757
+ init_devsy_workspaces();
8758
+ init_file_lock();
8759
+ init_managed_devsy_stop();
8760
+ init_router();
8761
+ DEVSY_MUTATION_LOCK_FILE = import_node_path22.default.join(DEVROUTER_HOME, "devsy-mutation.lock");
8762
+ DEVSY_MUTATION_WAIT_MS = 18e5;
8763
+ DevsyStartPostconditionError = class extends Error {
8764
+ };
8765
+ AGENT_ACQUISITION_RE = /inject agent.*agent binary not found/i;
8766
+ DEVSY_STDERR_TAIL_BYTES = 8192;
8242
8767
  }
8243
- if (!sameOwnershipRecord(current, expected)) return "changed";
8244
- import_node_fs22.default.rmSync(filePath);
8245
- return "removed";
8246
- }
8247
- function withWorkspaceOwnershipTransaction(repoPath, operation, options = {}) {
8248
- const directory = ownershipDirectory(repoPath);
8249
- import_node_fs22.default.mkdirSync(directory, { recursive: true });
8768
+ });
8769
+
8770
+ // src/core/devpod-mutation.ts
8771
+ function withMutationLock2(activity, target, operation) {
8772
+ import_node_fs23.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
8250
8773
  return withFileLockSync(
8251
- import_node_path22.default.join(directory, ".lock"),
8774
+ DEVPOD_MUTATION_LOCK_FILE,
8252
8775
  {
8253
- activity: "workspace ownership transaction",
8254
- target: `'${repoPath}'`,
8255
- waitMs: options.waitMs ?? 5e3
8776
+ activity,
8777
+ target: `'${target}'`,
8778
+ waitMs: DEVPOD_MUTATION_WAIT_MS,
8779
+ fair: true,
8780
+ onWait: createStderrWaitReporter(activity, `'${target}'`)
8256
8781
  },
8257
- () => operation({
8258
- list: () => listWorkspaceOwnershipInDirectory(directory),
8259
- write: (input2) => writeWorkspaceOwnershipInDirectory(directory, input2),
8260
- remove: (workspace) => removeWorkspaceOwnershipInDirectory(directory, workspace),
8261
- removeIfMatches: (expected) => removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected)
8262
- })
8782
+ operation
8263
8783
  );
8264
8784
  }
8265
- function providerPathOwner(providerWorkspaces, worktreePath) {
8266
- const owners = providerWorkspaces.filter(
8267
- (workspace) => sameWorkspacePath(workspace.source.localFolder, worktreePath)
8268
- );
8269
- if (owners.length > 1) {
8785
+ function commandFailure3(result) {
8786
+ return [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
8787
+ }
8788
+ function runDevpodAction(action2, devpodId, force = false) {
8789
+ const args = action2 === "delete" ? [action2, devpodId, ...force ? ["--force"] : [], "--ignore-not-found"] : [action2, devpodId];
8790
+ const result = (0, import_node_child_process17.spawnSync)("devpod", args, { encoding: "utf-8" });
8791
+ if (result.status !== 0) {
8270
8792
  throw new Error(
8271
- `Worktree '${worktreePath}' is registered by multiple workspace runtimes; no identity was claimed.`
8793
+ `devpod ${action2}${force ? " --force" : ""} failed for '${devpodId}': ${commandFailure3(result) || "unknown error"}`
8272
8794
  );
8273
8795
  }
8274
- return owners[0];
8275
- }
8276
- function persistedWorkspaceOwners(repoPath, worktreePath) {
8277
- const owners = /* @__PURE__ */ new Map();
8278
- for (const worktree of listGitWorktrees(repoPath)) {
8279
- if (sameWorkspacePath(worktree.path, worktreePath)) continue;
8280
- let workspace;
8281
- try {
8282
- workspace = readPersistedWorkspace(worktree.path);
8283
- } catch (error) {
8284
- if (!import_node_fs22.default.existsSync(worktree.path)) continue;
8285
- throw error;
8286
- }
8287
- if (!workspace) continue;
8288
- const existing = owners.get(workspace);
8289
- if (existing && !sameWorkspacePath(existing, worktree.path)) {
8290
- throw new Error(`Persisted workspace identity '${workspace}' belongs to multiple worktrees.`);
8291
- }
8292
- owners.set(workspace, worktree.path);
8293
- }
8294
- return owners;
8295
8796
  }
8296
- function claimConflict(workspace, devpodId, worktreePath, records, providerWorkspaces, persistedOwners, exactRecord) {
8297
- const recordOwner = records.find(
8298
- (record) => record !== exactRecord && (record.workspace === workspace || record.devpodId === devpodId)
8299
- );
8300
- if (recordOwner) {
8301
- return `workspace owner record '${recordOwner.workspace}' for '${recordOwner.worktreePath}'`;
8302
- }
8303
- const providerOwner = providerWorkspaces.find(
8304
- (providerWorkspace) => providerWorkspace.id === devpodId && !sameWorkspacePath(providerWorkspace.source.localFolder, worktreePath)
8797
+ function inspectExactOwnership2(devpodId, worktreePath) {
8798
+ const ownership = inspectDevpodWorkspaceOwnership(
8799
+ listDevpodWorkspaces(worktreePath),
8800
+ devpodId,
8801
+ worktreePath
8305
8802
  );
8306
- if (providerOwner) {
8307
- return `workspace runtime identity '${providerOwner.id}' already belongs to '${providerOwner.source.localFolder}'`;
8308
- }
8309
- const persistedOwner = persistedOwners.get(workspace);
8310
- if (persistedOwner) {
8311
- return `persisted checkout metadata for '${persistedOwner}'`;
8312
- }
8313
- return void 0;
8803
+ if (ownership.status === "conflict") throw new Error(ownership.reason);
8804
+ return ownership;
8314
8805
  }
8315
- function claimWorkspaceIdentity(repoPath, input2) {
8316
- const worktreePath = comparableWorkspacePath(repoPath);
8317
- const exactProvider = providerPathOwner(input2.providerWorkspaces, worktreePath);
8318
- return withWorkspaceOwnershipTransaction(repoPath, (transaction) => {
8319
- const records = transaction.list();
8320
- const exactRecords = records.filter(
8321
- (record) => sameWorkspacePath(record.worktreePath, worktreePath)
8322
- );
8323
- if (exactRecords.length > 1) {
8324
- throw new Error(
8325
- `Worktree '${worktreePath}' has multiple workspace owner records; no identity was claimed.`
8326
- );
8806
+ function mutateOwnedDevpodWorkspace(action2, devpodId, worktreePath) {
8807
+ return withMutationLock2(`DevPod ${action2}`, worktreePath, () => {
8808
+ const before = inspectExactOwnership2(devpodId, worktreePath);
8809
+ if (before.status === "absent") return { status: "absent" };
8810
+ runDevpodAction(action2, devpodId);
8811
+ let after = inspectExactOwnership2(devpodId, worktreePath);
8812
+ if (action2 === "stop" && after.status !== "owned") {
8813
+ throw new Error(`DevPod '${devpodId}' no longer owns '${worktreePath}' after provider stop.`);
8327
8814
  }
8328
- const exactRecord = exactRecords[0];
8329
- const persisted = readPersistedWorkspace(worktreePath);
8330
- const persistedOwners = persistedWorkspaceOwners(repoPath, worktreePath);
8331
- if (exactRecord) {
8332
- if (persisted && persisted !== exactRecord.workspace) {
8815
+ if (action2 === "delete" && after.status === "owned") {
8816
+ const runtime = inspectDevpodRuntimeStatus(devpodId, worktreePath);
8817
+ if (runtime !== "not-found") {
8333
8818
  throw new Error(
8334
- `Persisted workspace identity '${persisted}' disagrees with owner record '${exactRecord.workspace}'.`
8819
+ `DevPod '${devpodId}' still owns '${worktreePath}' after provider delete (runtime=${runtime}).`
8335
8820
  );
8336
8821
  }
8337
- if (exactProvider && exactProvider.id !== exactRecord.devpodId) {
8338
- throw new Error(
8339
- `Workspace runtime '${exactProvider.id}' disagrees with owner record '${exactRecord.devpodId}'.`
8340
- );
8822
+ after = inspectExactOwnership2(devpodId, worktreePath);
8823
+ if (after.status === "owned") {
8824
+ runDevpodAction("delete", devpodId, true);
8825
+ after = inspectExactOwnership2(devpodId, worktreePath);
8341
8826
  }
8342
- const conflict2 = claimConflict(
8343
- exactRecord.workspace,
8344
- exactRecord.devpodId,
8345
- worktreePath,
8346
- records,
8347
- input2.providerWorkspaces,
8348
- persistedOwners,
8349
- exactRecord
8350
- );
8351
- if (conflict2) {
8827
+ if (after.status !== "absent") {
8352
8828
  throw new Error(
8353
- `Workspace '${exactRecord.workspace}' conflicts with ${conflict2}; no identity was claimed.`
8829
+ `DevPod '${devpodId}' still owns '${worktreePath}' after forced provider delete.`
8354
8830
  );
8355
8831
  }
8356
- if (!persisted) persistWorkspace(worktreePath, exactRecord.workspace);
8357
- return transaction.write({
8358
- workspace: exactRecord.workspace,
8359
- worktreePath,
8360
- branch: input2.branch ?? null,
8361
- devpodId: exactRecord.devpodId
8362
- });
8363
8832
  }
8364
- if (persisted && exactProvider && persisted !== exactProvider.id) {
8365
- throw new Error(
8366
- `Persisted workspace identity '${persisted}' disagrees with workspace runtime '${exactProvider.id}'.`
8367
- );
8833
+ return { status: "changed" };
8834
+ });
8835
+ }
8836
+ function stopOwnedDevpodWorkspace(devpodId, worktreePath) {
8837
+ try {
8838
+ resetWorkspaceRuntimeCaches();
8839
+ if (resolveWorkspaceRuntimeOrDefault(worktreePath) === "devsy") {
8840
+ return stopOwnedDevsyWorkspace(devpodId, worktreePath);
8368
8841
  }
8369
- let workspace = persisted ?? exactProvider?.id;
8370
- let devpodId = exactProvider?.id ?? persisted;
8371
- if (!workspace || !devpodId) {
8372
- if (input2.unavailableRuntimes.length > 0) {
8373
- throw new Error(
8374
- `Cannot claim a new workspace identity because these runtime registries are unavailable: ${input2.unavailableRuntimes.join(", ")}.`
8375
- );
8842
+ return mutateOwnedDevpodWorkspace("stop", devpodId, worktreePath);
8843
+ } finally {
8844
+ resetWorkspaceRuntimeCaches();
8845
+ }
8846
+ }
8847
+ function deleteOwnedDevpodWorkspace(devpodId, worktreePath) {
8848
+ if (resolveWorkspaceRuntimeOrDefault(worktreePath) === "devsy") {
8849
+ const result2 = deleteOwnedDevsyWorkspace(devpodId, worktreePath);
8850
+ resetWorkspaceRuntimeCaches();
8851
+ return result2;
8852
+ }
8853
+ const result = mutateOwnedDevpodWorkspace("delete", devpodId, worktreePath);
8854
+ resetWorkspaceRuntimeCaches();
8855
+ return result;
8856
+ }
8857
+ async function startDevpodWorkspace(options) {
8858
+ if (resolveWorkspaceRuntimeOrDefault(options.repoPath) === "devsy") {
8859
+ try {
8860
+ const result = await startDevsyWorkspace({
8861
+ repoPath: options.repoPath,
8862
+ devsyId: options.devpodId,
8863
+ devcontainerPath: options.devcontainerPath,
8864
+ recreate: options.recreate,
8865
+ quiet: options.quiet,
8866
+ workspace: options.workspace,
8867
+ inactivityTimeout: readWorkspaceRuntimeConfig().devsyInactivityTimeout
8868
+ });
8869
+ resetWorkspaceRuntimeCaches();
8870
+ return result;
8871
+ } catch (error) {
8872
+ if (error instanceof DevsyStartPostconditionError) {
8873
+ resetWorkspaceRuntimeCaches();
8874
+ throw new DevpodStartPostconditionError(error.message);
8376
8875
  }
8377
- const candidate = workspaceIdentityCandidates(input2.source).find(
8378
- (next) => !claimConflict(
8379
- next,
8380
- next,
8381
- worktreePath,
8382
- records,
8383
- input2.providerWorkspaces,
8384
- persistedOwners
8385
- )
8386
- );
8387
- if (!candidate) {
8388
- throw new Error(
8389
- `Could not allocate a collision-safe workspace identity for '${worktreePath}'.`
8390
- );
8876
+ throw error;
8877
+ }
8878
+ }
8879
+ const activity = options.recreate ? "DevPod recreate" : "DevPod start";
8880
+ return withMutationLock2(activity, options.repoPath, () => {
8881
+ const workspaces = listDevpodWorkspaces(options.repoPath);
8882
+ let devpodId = options.devpodId ?? selectDevpodWorkspace(workspaces, options.repoPath)?.id;
8883
+ if (devpodId) {
8884
+ const before = inspectDevpodWorkspaceOwnership(workspaces, devpodId, options.repoPath);
8885
+ if (before.status === "conflict") throw new Error(before.reason);
8886
+ if (options.recreate && before.status !== "owned") {
8887
+ throw new Error(`Cannot recreate DevPod '${devpodId}' without one exact owner.`);
8391
8888
  }
8392
- workspace = candidate;
8393
- devpodId = candidate;
8889
+ } else if (options.recreate) {
8890
+ throw new Error("Cannot recreate a DevPod before its exact id is known.");
8394
8891
  }
8395
- const conflict = claimConflict(
8396
- workspace,
8397
- devpodId,
8398
- worktreePath,
8399
- records,
8400
- input2.providerWorkspaces,
8401
- persistedOwners
8402
- );
8403
- if (conflict) {
8404
- throw new Error(
8405
- `Workspace '${workspace}' conflicts with ${conflict}; no identity was claimed.`
8892
+ const args = ["up", options.repoPath];
8893
+ if (devpodId) args.push("--id", devpodId);
8894
+ if (options.devcontainerPath) {
8895
+ args.push("--devcontainer-path", options.devcontainerPath);
8896
+ }
8897
+ args.push("--open-ide=false");
8898
+ if (options.workspace) {
8899
+ args.push(
8900
+ "--workspace-env",
8901
+ `WORKSPACE=${options.workspace.token}`,
8902
+ "--workspace-env",
8903
+ `DEVROUTER_WORKSPACE=${options.workspace.token}`
8406
8904
  );
8407
8905
  }
8408
- const written = transaction.write({
8409
- workspace,
8410
- worktreePath,
8411
- branch: input2.branch ?? null,
8412
- devpodId
8906
+ if (options.recreate) args.push("--recreate");
8907
+ const env = { ...process.env };
8908
+ if (options.workspace) {
8909
+ env.WORKSPACE = options.workspace.token;
8910
+ env.DEVROUTER_WORKSPACE = options.workspace.token;
8911
+ env.DEVROUTER_GIT_COMMON_DIR = options.workspace.gitCommonDir;
8912
+ env.DEVCONTAINER_COMPOSE_OVERLAY = "docker-compose.devrouter.yml";
8913
+ } else {
8914
+ delete env.WORKSPACE;
8915
+ delete env.DEVROUTER_WORKSPACE;
8916
+ delete env.DEVROUTER_GIT_COMMON_DIR;
8917
+ delete env.DEVCONTAINER_COMPOSE_OVERLAY;
8918
+ }
8919
+ const result = (0, import_node_child_process17.spawnSync)("devpod", args, {
8920
+ stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
8921
+ env
8413
8922
  });
8923
+ if (result.status !== 0) {
8924
+ throw new Error(`devpod up failed for '${devpodId ?? options.repoPath}'.`);
8925
+ }
8414
8926
  try {
8415
- persistWorkspace(worktreePath, workspace);
8416
- } catch (error) {
8417
- const cleanup = transaction.removeIfMatches(written);
8418
- if (cleanup !== "removed") {
8419
- const detail = error instanceof Error ? error.message : String(error);
8927
+ const attached = listDevpodWorkspaces(options.repoPath);
8928
+ devpodId ??= selectDevpodWorkspace(attached, options.repoPath)?.id;
8929
+ if (!devpodId) {
8930
+ throw new Error(`DevPod did not attach '${options.repoPath}' after startup.`);
8931
+ }
8932
+ const ownership = inspectDevpodWorkspaceOwnership(attached, devpodId, options.repoPath);
8933
+ if (ownership.status === "conflict") throw new Error(ownership.reason);
8934
+ if (ownership.status !== "owned") {
8420
8935
  throw new Error(
8421
- `Could not persist workspace identity and owner-record rollback was '${cleanup}': ${detail}`
8936
+ `DevPod did not attach '${options.repoPath}' as '${devpodId}' after startup.`
8422
8937
  );
8423
8938
  }
8424
- throw error;
8939
+ resetWorkspaceRuntimeCaches();
8940
+ return devpodId;
8941
+ } catch (error) {
8942
+ const message = error instanceof Error ? error.message : String(error);
8943
+ throw new DevpodStartPostconditionError(message);
8425
8944
  }
8426
- return written;
8427
8945
  });
8428
8946
  }
8429
- function removeWorkspaceOwnership(repoPath, workspace) {
8430
- return withWorkspaceOwnershipTransaction(
8431
- repoPath,
8432
- (transaction) => transaction.remove(workspace)
8433
- );
8434
- }
8435
- function inspectWorkspaceOwnership(record, worktrees, devpods) {
8436
- const worktree = worktrees.find(
8437
- (candidate) => sameWorkspacePath(candidate.path, record.worktreePath)
8438
- );
8439
- const devpodOwnership = devpods ? inspectDevpodWorkspaceOwnership(devpods, record.devpodId, record.worktreePath) : void 0;
8440
- const devpodStatus = devpodOwnership?.status ?? "unknown";
8441
- if (devpodStatus === "conflict") {
8442
- return { ownerStatus: "conflict", devpodStatus, worktree };
8443
- }
8444
- if (worktree?.locked) {
8445
- return { ownerStatus: "locked", devpodStatus, worktree };
8446
- }
8447
- if ((!worktree || worktree.prunable) && import_node_fs22.default.existsSync(record.worktreePath)) {
8448
- return { ownerStatus: "conflict", devpodStatus, worktree };
8449
- }
8450
- if (!worktree || worktree.prunable) {
8451
- return { ownerStatus: "missing", devpodStatus, worktree };
8452
- }
8453
- let persisted;
8454
- try {
8455
- persisted = readPersistedWorkspace(worktree.path);
8456
- } catch {
8457
- return { ownerStatus: "conflict", devpodStatus, worktree };
8458
- }
8459
- return {
8460
- ownerStatus: persisted === record.workspace ? "present" : "conflict",
8461
- devpodStatus,
8462
- worktree
8463
- };
8464
- }
8465
- function listMissingWorkspaceOwnership(repoPath) {
8466
- const worktrees = listGitWorktrees(repoPath);
8467
- return listWorkspaceOwnership(repoPath).filter(
8468
- (record) => inspectWorkspaceOwnership(record, worktrees, void 0).ownerStatus === "missing"
8469
- );
8470
- }
8471
- var import_node_child_process17, import_node_fs22, import_node_path22, READ_ONLY_GIT_ENV, OWNERSHIP_VERSION, OWNERSHIP_DIR;
8472
- var init_workspace_ownership = __esm({
8473
- "src/core/workspace-ownership.ts"() {
8947
+ var import_node_child_process17, import_node_fs23, import_node_path23, DEVPOD_MUTATION_LOCK_FILE, DEVPOD_MUTATION_WAIT_MS, DevpodStartPostconditionError;
8948
+ var init_devpod_mutation = __esm({
8949
+ "src/core/devpod-mutation.ts"() {
8474
8950
  "use strict";
8475
8951
  import_node_child_process17 = require("child_process");
8476
- import_node_fs22 = __toESM(require("fs"));
8477
- import_node_path22 = __toESM(require("path"));
8478
- init_atomic_file();
8952
+ import_node_fs23 = __toESM(require("fs"));
8953
+ import_node_path23 = __toESM(require("path"));
8479
8954
  init_devpod_workspaces();
8955
+ init_devsy_mutation();
8480
8956
  init_file_lock();
8481
- init_workspace();
8482
- READ_ONLY_GIT_ENV = { ...process.env, GIT_OPTIONAL_LOCKS: "0" };
8483
- OWNERSHIP_VERSION = 1;
8484
- OWNERSHIP_DIR = import_node_path22.default.join("devrouter", "workspaces");
8957
+ init_router();
8958
+ init_workspace_runtime();
8959
+ DEVPOD_MUTATION_LOCK_FILE = import_node_path23.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
8960
+ DEVPOD_MUTATION_WAIT_MS = 18e5;
8961
+ DevpodStartPostconditionError = class extends Error {
8962
+ };
8485
8963
  }
8486
8964
  });
8487
8965
 
@@ -8495,10 +8973,10 @@ function inRepositoryWorkspaceScope(repoPath, worktreePath, livePaths) {
8495
8973
  if (livePaths.some((candidate) => sameWorkspacePath(candidate, worktreePath))) return true;
8496
8974
  const comparableRepo = comparableWorkspacePath(repoPath);
8497
8975
  const comparableWorktree = comparableWorkspacePath(worktreePath);
8498
- const localRoot = import_node_path23.default.join(comparableRepo, "trees") + import_node_path23.default.sep;
8976
+ const localRoot = import_node_path24.default.join(comparableRepo, "trees") + import_node_path24.default.sep;
8499
8977
  if (comparableWorktree.startsWith(localRoot)) return true;
8500
- const legacyPrefix = `${import_node_path23.default.basename(comparableRepo)}-`;
8501
- return import_node_path23.default.dirname(comparableWorktree) === import_node_path23.default.dirname(comparableRepo) && import_node_path23.default.basename(comparableWorktree).startsWith(legacyPrefix);
8978
+ const legacyPrefix = `${import_node_path24.default.basename(comparableRepo)}-`;
8979
+ return import_node_path24.default.dirname(comparableWorktree) === import_node_path24.default.dirname(comparableRepo) && import_node_path24.default.basename(comparableWorktree).startsWith(legacyPrefix);
8502
8980
  }
8503
8981
  function previewActions(devpodStatus, routeCount, includeRecord) {
8504
8982
  const actions = [
@@ -8750,11 +9228,11 @@ function applyWorkspaceGc(plan) {
8750
9228
  candidates
8751
9229
  };
8752
9230
  }
8753
- var import_node_path23;
9231
+ var import_node_path24;
8754
9232
  var init_workspace_gc = __esm({
8755
9233
  "src/core/workspace-gc.ts"() {
8756
9234
  "use strict";
8757
- import_node_path23 = __toESM(require("path"));
9235
+ import_node_path24 = __toESM(require("path"));
8758
9236
  init_devpod_mutation();
8759
9237
  init_devpod_workspaces();
8760
9238
  init_host_routes();
@@ -8824,11 +9302,11 @@ function inspectPostgresCredentials(repoPath, config) {
8824
9302
  parseErrors.push(`${app.name}: ${composeFile} (${message})`);
8825
9303
  continue;
8826
9304
  }
8827
- if (!import_node_fs23.default.existsSync(absolutePath)) {
9305
+ if (!import_node_fs24.default.existsSync(absolutePath)) {
8828
9306
  continue;
8829
9307
  }
8830
9308
  try {
8831
- const raw = import_node_fs23.default.readFileSync(absolutePath, "utf-8");
9309
+ const raw = import_node_fs24.default.readFileSync(absolutePath, "utf-8");
8832
9310
  const parsed = import_yaml5.default.parse(raw);
8833
9311
  const root = asRecord2(parsed);
8834
9312
  const services = asRecord2(root?.services);
@@ -9107,7 +9585,7 @@ async function buildDoctorReport(options = {}) {
9107
9585
  const config = runtimeConfig.config;
9108
9586
  loadedConfig = config;
9109
9587
  loadedWorkspace = runtimeConfig.workspace;
9110
- const cliVersion = true ? "0.0.52" : "0.0.0-dev";
9588
+ const cliVersion = true ? "0.0.53" : "0.0.0-dev";
9111
9589
  const configVersion = config.devrouter?.version;
9112
9590
  if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
9113
9591
  addCheck(checks, {
@@ -9137,9 +9615,9 @@ async function buildDoctorReport(options = {}) {
9137
9615
  (app) => app.docker.composeFiles.map((filePath) => ({
9138
9616
  app: app.name,
9139
9617
  filePath,
9140
- absolutePath: import_node_path24.default.resolve(repo.path, filePath)
9618
+ absolutePath: import_node_path25.default.resolve(repo.path, filePath)
9141
9619
  }))
9142
- ).filter((entry) => !import_node_fs23.default.existsSync(entry.absolutePath));
9620
+ ).filter((entry) => !import_node_fs24.default.existsSync(entry.absolutePath));
9143
9621
  addCheck(checks, {
9144
9622
  id: "repo.compose-files",
9145
9623
  level: missingComposeFiles.length === 0 ? "ok" : "error",
@@ -9182,8 +9660,8 @@ async function buildDoctorReport(options = {}) {
9182
9660
  const missingHostCwds = config.apps.filter((app) => app.runtime === "host").map((app) => ({
9183
9661
  app: app.name,
9184
9662
  cwd: app.hostRun.cwd,
9185
- absolutePath: import_node_path24.default.resolve(repo.path, app.hostRun.cwd)
9186
- })).filter((entry) => !import_node_fs23.default.existsSync(entry.absolutePath));
9663
+ absolutePath: import_node_path25.default.resolve(repo.path, app.hostRun.cwd)
9664
+ })).filter((entry) => !import_node_fs24.default.existsSync(entry.absolutePath));
9187
9665
  addCheck(checks, {
9188
9666
  id: "repo.host-cwd",
9189
9667
  level: missingHostCwds.length === 0 ? "ok" : "error",
@@ -9327,12 +9805,12 @@ async function buildDoctorReport(options = {}) {
9327
9805
  nextSteps
9328
9806
  };
9329
9807
  }
9330
- var import_node_fs23, import_node_path24, import_yaml5, POSTGRES_DEFAULTS;
9808
+ var import_node_fs24, import_node_path25, import_yaml5, POSTGRES_DEFAULTS;
9331
9809
  var init_doctor = __esm({
9332
9810
  "src/core/doctor.ts"() {
9333
9811
  "use strict";
9334
- import_node_fs23 = __toESM(require("fs"));
9335
- import_node_path24 = __toESM(require("path"));
9812
+ import_node_fs24 = __toESM(require("fs"));
9813
+ import_node_path25 = __toESM(require("path"));
9336
9814
  import_yaml5 = __toESM(require("yaml"));
9337
9815
  init_docker();
9338
9816
  init_host_routes();
@@ -9779,76 +10257,6 @@ var init_http_route_probe = __esm({
9779
10257
  }
9780
10258
  });
9781
10259
 
9782
- // src/core/route-publication.ts
9783
- function routedAppsFromConfig(config) {
9784
- return config.apps.filter((app) => app.kind !== "dependency");
9785
- }
9786
- function configuredProxyAppsFromConfig(config) {
9787
- return routedAppsFromConfig(config).filter(
9788
- (app) => app.runtime === "proxy"
9789
- );
9790
- }
9791
- function proxyAppsFromConfig(config) {
9792
- const routedApps = routedAppsFromConfig(config);
9793
- const proxyApps = configuredProxyAppsFromConfig(config);
9794
- const unsupported = routedApps.filter((app) => app.runtime !== "proxy");
9795
- if (unsupported.length > 0) {
9796
- throw new Error(
9797
- `Environment reconciliation supports proxy runtime apps only; unsupported: ${unsupported.map((app) => `${app.name} (${app.runtime})`).join(", ")}`
9798
- );
9799
- }
9800
- return proxyApps;
9801
- }
9802
- async function ensureRouteInfrastructure(apps, options = {}) {
9803
- ensureRouterFiles();
9804
- await ensureNetwork(DEVNET_NAME);
9805
- const tlsCoverage = await ensureTLSHostsCovered(
9806
- apps.map((app) => app.host),
9807
- options
9808
- );
9809
- for (const app of apps) {
9810
- if (app.protocol === "tcp") {
9811
- if (!isTLSEnabled()) {
9812
- throw new Error(
9813
- `TCP route '${app.name}' requires TLS. Run: ${tlsSetupCommand(options.repoPath)}`
9814
- );
9815
- }
9816
- activateTcpProtocol(app.tcpProtocol);
9817
- }
9818
- }
9819
- startRouterStack();
9820
- return tlsCoverage;
9821
- }
9822
- async function replacePublishedProxyRoutes(repoPath, config, workspace, options = {}) {
9823
- const apps = proxyAppsFromConfig(config);
9824
- const tlsCoverage = options.prepareInfrastructure === false ? { refreshed: false } : await ensureRouteInfrastructure(apps, { repoPath });
9825
- const routes = apps.map((app) => {
9826
- const upstream = parseUpstream(app.upstream);
9827
- return {
9828
- name: app.name,
9829
- host: app.host,
9830
- protocol: app.protocol,
9831
- tcpProtocol: app.protocol === "tcp" ? app.tcpProtocol : void 0,
9832
- repoPath,
9833
- port: upstream.port,
9834
- mode: "proxy",
9835
- upstreamHost: upstream.upstreamHost,
9836
- workspace
9837
- };
9838
- });
9839
- replaceHostRoutesForRepo(repoPath, routes);
9840
- return { routes, tlsRefreshed: tlsCoverage.refreshed };
9841
- }
9842
- var init_route_publication = __esm({
9843
- "src/core/route-publication.ts"() {
9844
- "use strict";
9845
- init_docker();
9846
- init_host_routes();
9847
- init_router();
9848
- init_tls();
9849
- }
9850
- });
9851
-
9852
10260
  // src/core/traefik-route-health.ts
9853
10261
  function sleep(ms) {
9854
10262
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -9893,7 +10301,7 @@ function inspectRouterApi(protocol) {
9893
10301
  )
9894
10302
  };
9895
10303
  }
9896
- function isRecord2(value) {
10304
+ function isRecord3(value) {
9897
10305
  return value !== null && typeof value === "object" && !Array.isArray(value);
9898
10306
  }
9899
10307
  function inspectNamedRouterApi(protocol, resource, name) {
@@ -9924,7 +10332,7 @@ function inspectNamedRouterApi(protocol, resource, name) {
9924
10332
  details: `${protocol.toUpperCase()} ${resource.slice(0, -1)} API returned invalid JSON`
9925
10333
  };
9926
10334
  }
9927
- if (!isRecord2(value)) {
10335
+ if (!isRecord3(value)) {
9928
10336
  return {
9929
10337
  ok: false,
9930
10338
  details: `${protocol.toUpperCase()} ${resource.slice(0, -1)} API returned a non-object`
@@ -9953,19 +10361,19 @@ function renderExpectedRoute(route) {
9953
10361
  };
9954
10362
  const document = buildHostRoutesDocument([routeState], false);
9955
10363
  const protocolDocument = document[protocol];
9956
- if (!isRecord2(protocolDocument)) return void 0;
10364
+ if (!isRecord3(protocolDocument)) return void 0;
9957
10365
  const routers = protocolDocument.routers;
9958
10366
  const services = protocolDocument.services;
9959
- if (!isRecord2(routers) || !isRecord2(services)) return void 0;
10367
+ if (!isRecord3(routers) || !isRecord3(services)) return void 0;
9960
10368
  const routerKey = routerName.slice(0, routerName.lastIndexOf("@"));
9961
10369
  const router = routers[routerKey];
9962
10370
  const service = services[routerKey];
9963
- if (!isRecord2(router) || !isRecord2(service)) return void 0;
10371
+ if (!isRecord3(router) || !isRecord3(service)) return void 0;
9964
10372
  const rule = router.rule;
9965
10373
  const serviceReference = router.service;
9966
10374
  const loadBalancer = service.loadBalancer;
9967
- const servers = isRecord2(loadBalancer) ? loadBalancer.servers : void 0;
9968
- if (typeof rule !== "string" || serviceReference !== routerKey || !isRecord2(loadBalancer) || !Array.isArray(servers) || servers.length !== 1 || !isRecord2(servers[0])) {
10375
+ const servers = isRecord3(loadBalancer) ? loadBalancer.servers : void 0;
10376
+ if (typeof rule !== "string" || serviceReference !== routerKey || !isRecord3(loadBalancer) || !Array.isArray(servers) || servers.length !== 1 || !isRecord3(servers[0])) {
9969
10377
  return void 0;
9970
10378
  }
9971
10379
  const serverField = protocol === "http" ? "url" : "address";
@@ -10009,9 +10417,9 @@ function inspectExpectedRouteMatch(route) {
10009
10417
  return { ok: false, routes: [routeName], details: serviceResult.details };
10010
10418
  }
10011
10419
  const loadBalancer = serviceResult.value.loadBalancer;
10012
- const servers = isRecord2(loadBalancer) ? loadBalancer.servers : void 0;
10420
+ const servers = isRecord3(loadBalancer) ? loadBalancer.servers : void 0;
10013
10421
  const server = Array.isArray(servers) && servers.length === 1 ? servers[0] : void 0;
10014
- if (serviceResult.value.status !== "enabled" || serviceResult.value.name !== expected.serviceName || !isRecord2(loadBalancer) || !Array.isArray(servers) || servers.length !== 1 || !isRecord2(server) || server[expected.serverField] !== expected.server) {
10422
+ if (serviceResult.value.status !== "enabled" || serviceResult.value.name !== expected.serviceName || !isRecord3(loadBalancer) || !Array.isArray(servers) || servers.length !== 1 || !isRecord3(server) || server[expected.serverField] !== expected.server) {
10015
10423
  return {
10016
10424
  ok: false,
10017
10425
  routes: [routeName],
@@ -10108,7 +10516,7 @@ async function ensureTraefikRouteExpectation(routes, expectation, options) {
10108
10516
  throw new Error(
10109
10517
  "Traefik routes did not reload; shared router restart is disabled for this operation."
10110
10518
  );
10111
- import_node_fs24.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
10519
+ import_node_fs25.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
10112
10520
  return withFileLock(
10113
10521
  ROUTER_RELOAD_LOCK_FILE,
10114
10522
  {
@@ -10152,18 +10560,18 @@ async function ensureTraefikRoutesMatch(routes, options = {}) {
10152
10560
  async function ensureTraefikRoutesRemoved(routes, options = {}) {
10153
10561
  return ensureTraefikRouteExpectation(routes, "removed", options);
10154
10562
  }
10155
- var import_node_child_process20, import_node_fs24, import_node_path25, ROUTER_API_BASE, ROUTER_RELOAD_LOCK_FILE, ROUTER_API_REQUEST_TIMEOUT_SECONDS, DEFAULT_INITIAL_TIMEOUT_MS, DEFAULT_RECOVERY_TIMEOUT_MS, DEFAULT_POLL_INTERVAL_MS, ROUTER_API_PAGE_SIZE;
10563
+ var import_node_child_process20, import_node_fs25, import_node_path26, ROUTER_API_BASE, ROUTER_RELOAD_LOCK_FILE, ROUTER_API_REQUEST_TIMEOUT_SECONDS, DEFAULT_INITIAL_TIMEOUT_MS, DEFAULT_RECOVERY_TIMEOUT_MS, DEFAULT_POLL_INTERVAL_MS, ROUTER_API_PAGE_SIZE;
10156
10564
  var init_traefik_route_health = __esm({
10157
10565
  "src/core/traefik-route-health.ts"() {
10158
10566
  "use strict";
10159
10567
  import_node_child_process20 = require("child_process");
10160
- import_node_fs24 = __toESM(require("fs"));
10161
- import_node_path25 = __toESM(require("path"));
10568
+ import_node_fs25 = __toESM(require("fs"));
10569
+ import_node_path26 = __toESM(require("path"));
10162
10570
  init_file_lock();
10163
10571
  init_host_routes();
10164
10572
  init_router();
10165
10573
  ROUTER_API_BASE = "http://127.0.0.1:8080/api";
10166
- ROUTER_RELOAD_LOCK_FILE = import_node_path25.default.join(DEVROUTER_HOME, "router-reload.lock");
10574
+ ROUTER_RELOAD_LOCK_FILE = import_node_path26.default.join(DEVROUTER_HOME, "router-reload.lock");
10167
10575
  ROUTER_API_REQUEST_TIMEOUT_SECONDS = "2";
10168
10576
  DEFAULT_INITIAL_TIMEOUT_MS = 3e3;
10169
10577
  DEFAULT_RECOVERY_TIMEOUT_MS = 1e4;
@@ -10175,11 +10583,11 @@ var init_traefik_route_health = __esm({
10175
10583
  // src/core/workspace-ensure.ts
10176
10584
  function assertOverlay(container, repoPath) {
10177
10585
  const workingDir = container.labels["com.docker.compose.project.working_dir"];
10178
- if (!workingDir || !sameWorkspacePath(workingDir, import_node_path26.default.join(repoPath, ".devcontainer"))) {
10586
+ if (!workingDir || !sameWorkspacePath(workingDir, import_node_path27.default.join(repoPath, ".devcontainer"))) {
10179
10587
  throw new Error(`Container '${container.id}' does not belong to the exact worktree.`);
10180
10588
  }
10181
10589
  const configFiles = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").filter(Boolean);
10182
- const expectedOverlay = import_node_path26.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
10590
+ const expectedOverlay = import_node_path27.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
10183
10591
  if (!configFiles.some((configFile) => sameWorkspacePath(configFile, expectedOverlay))) {
10184
10592
  throw new Error(`Container '${container.id}' was not started with ${DEVCONTAINER_OVERLAY}.`);
10185
10593
  }
@@ -10369,7 +10777,7 @@ function hasExactManagedComposeProject(repoPath, state) {
10369
10777
  }).some((container) => {
10370
10778
  const workingDir = container.labels["com.docker.compose.project.working_dir"];
10371
10779
  return Boolean(
10372
- workingDir && sameWorkspacePath(workingDir, import_node_path26.default.join(repoPath, ".devcontainer"))
10780
+ workingDir && sameWorkspacePath(workingDir, import_node_path27.default.join(repoPath, ".devcontainer"))
10373
10781
  );
10374
10782
  });
10375
10783
  } catch {
@@ -10726,7 +11134,7 @@ function resolvePrimaryTarget(repoPath) {
10726
11134
  }
10727
11135
  function isPrimaryCheckout(repoPath) {
10728
11136
  try {
10729
- return import_node_fs25.default.statSync(import_node_path26.default.join(repoPath, ".git")).isDirectory();
11137
+ return import_node_fs26.default.statSync(import_node_path27.default.join(repoPath, ".git")).isDirectory();
10730
11138
  } catch {
10731
11139
  return false;
10732
11140
  }
@@ -10784,8 +11192,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
10784
11192
  const target = options.repair ? resolveRepairTarget(repoPath, linked) : linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
10785
11193
  let devpodId = target.devpodId;
10786
11194
  if (target.kind === "linked") {
10787
- const overlayPath = import_node_path26.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
10788
- if (!import_node_fs25.default.existsSync(overlayPath)) {
11195
+ const overlayPath = import_node_path27.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
11196
+ if (!import_node_fs26.default.existsSync(overlayPath)) {
10789
11197
  throw new Error(`Missing required DevPod compose overlay: ${overlayPath}`);
10790
11198
  }
10791
11199
  }
@@ -10859,7 +11267,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
10859
11267
  }
10860
11268
  const apps = proxyAppsFromConfig(runtime.config);
10861
11269
  const parsedUpstreams = apps.map((app) => parseUpstream(app.upstream));
10862
- const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path26.default.basename(repoPath)) ?? "app";
11270
+ const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path27.default.basename(repoPath)) ?? "app";
10863
11271
  for (const [index, app] of apps.entries()) {
10864
11272
  if (!parsedUpstreams[index].host.startsWith(`${aliasPrefix}-`)) {
10865
11273
  const owner = target.kind === "linked" ? "workspace" : "checkout";
@@ -11427,9 +11835,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
11427
11835
  return withWorkspaceLifecycleLock(repoPath, async () => {
11428
11836
  if (!options.repair) return ensureLocked();
11429
11837
  const runtime = resolveWorkspaceRuntimeOrDefault(repoPath);
11430
- import_node_fs25.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
11838
+ import_node_fs26.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
11431
11839
  return withFileLock(
11432
- import_node_path26.default.join(DEVROUTER_HOME, `${runtime}-mutation.lock`),
11840
+ import_node_path27.default.join(DEVROUTER_HOME, `${runtime}-mutation.lock`),
11433
11841
  {
11434
11842
  activity: "Managed runtime repair",
11435
11843
  target: repoPath,
@@ -11441,13 +11849,13 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
11441
11849
  );
11442
11850
  });
11443
11851
  }
11444
- var import_node_child_process21, import_node_fs25, import_node_path26, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
11852
+ var import_node_child_process21, import_node_fs26, import_node_path27, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
11445
11853
  var init_workspace_ensure = __esm({
11446
11854
  "src/core/workspace-ensure.ts"() {
11447
11855
  "use strict";
11448
11856
  import_node_child_process21 = require("child_process");
11449
- import_node_fs25 = __toESM(require("fs"));
11450
- import_node_path26 = __toESM(require("path"));
11857
+ import_node_fs26 = __toESM(require("fs"));
11858
+ import_node_path27 = __toESM(require("path"));
11451
11859
  init_devcontainer_profile();
11452
11860
  init_devpod_environment();
11453
11861
  init_devpod_mutation();
@@ -11536,7 +11944,7 @@ function warnMissingWorkspaceOwnership(repoPath) {
11536
11944
  );
11537
11945
  }
11538
11946
  function defaultWorktreePath(mainRepo, ws) {
11539
- return import_node_path27.default.join(mainRepo, "trees", ws);
11947
+ return import_node_path28.default.join(mainRepo, "trees", ws);
11540
11948
  }
11541
11949
  function assertDefaultWorktreeRootIgnored(mainRepo) {
11542
11950
  const ignored = (0, import_node_child_process22.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
@@ -11544,12 +11952,12 @@ function assertDefaultWorktreeRootIgnored(mainRepo) {
11544
11952
  });
11545
11953
  if (ignored.status !== 0) {
11546
11954
  throw new Error(
11547
- `Default worktree root '${import_node_path27.default.join(mainRepo, "trees")}' is not ignored. Add 'trees/' to '${import_node_path27.default.join(mainRepo, ".gitignore")}' or use --path.`
11955
+ `Default worktree root '${import_node_path28.default.join(mainRepo, "trees")}' is not ignored. Add 'trees/' to '${import_node_path28.default.join(mainRepo, ".gitignore")}' or use --path.`
11548
11956
  );
11549
11957
  }
11550
11958
  }
11551
11959
  function legacyDefaultWorktreePath(mainRepo, ws) {
11552
- return import_node_path27.default.join(import_node_path27.default.dirname(mainRepo), `${import_node_path27.default.basename(mainRepo)}-${ws}`);
11960
+ return import_node_path28.default.join(import_node_path28.default.dirname(mainRepo), `${import_node_path28.default.basename(mainRepo)}-${ws}`);
11553
11961
  }
11554
11962
  function teardownFallbackPath(mainRepo, workspace) {
11555
11963
  const candidates = [
@@ -11661,7 +12069,7 @@ function assertFullDownPreflight(mainRepo, target) {
11661
12069
  if (sameWorkspacePath(target.worktreePath, mainRepo)) {
11662
12070
  throw new Error("Refusing to remove the primary Git checkout.");
11663
12071
  }
11664
- if (!target.worktree || target.worktree.prunable || !import_node_fs26.default.existsSync(target.worktreePath)) return;
12072
+ if (!target.worktree || target.worktree.prunable || !import_node_fs27.default.existsSync(target.worktreePath)) return;
11665
12073
  if (target.worktree.locked) {
11666
12074
  throw new Error(
11667
12075
  `Worktree '${target.worktreePath}' is locked; unlock it before workspace down.`
@@ -11701,9 +12109,9 @@ async function workspaceUp(branch, opts = {}) {
11701
12109
  (candidate) => defaultWorktreePath(mainRepo, candidate)
11702
12110
  );
11703
12111
  const generatedPath = candidatePaths.find(
11704
- (candidate) => !import_node_fs26.default.existsSync(candidate) && !worktrees.some((worktree) => sameWorkspacePath(worktree.path, candidate))
12112
+ (candidate) => !import_node_fs27.default.existsSync(candidate) && !worktrees.some((worktree) => sameWorkspacePath(worktree.path, candidate))
11705
12113
  );
11706
- const selectedPath = opts.path ? import_node_path27.default.resolve(opts.path) : existingBranch?.path ?? generatedPath;
12114
+ const selectedPath = opts.path ? import_node_path28.default.resolve(opts.path) : existingBranch?.path ?? generatedPath;
11707
12115
  if (!selectedPath) {
11708
12116
  throw new Error(`Could not allocate a collision-safe worktree path for branch '${branch}'.`);
11709
12117
  }
@@ -11712,7 +12120,7 @@ async function workspaceUp(branch, opts = {}) {
11712
12120
  `Branch '${branch}' already uses worktree '${existingBranch.path}', not '${selectedPath}'.`
11713
12121
  );
11714
12122
  }
11715
- if (import_node_fs26.default.existsSync(selectedPath)) {
12123
+ if (import_node_fs27.default.existsSync(selectedPath)) {
11716
12124
  const registered = worktrees.find(
11717
12125
  (worktree) => sameWorkspacePath(worktree.path, selectedPath)
11718
12126
  );
@@ -11806,6 +12214,11 @@ async function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = fals
11806
12214
  assertDevpodTargetSafe(resolved, worktrees, devpods);
11807
12215
  const devpodId = resolved.record?.devpodId ?? resolved.workspace;
11808
12216
  const mutation = action2 === "stop" ? stopOwnedDevpodWorkspace(devpodId, resolved.worktreePath) : deleteOwnedDevpodWorkspace(devpodId, resolved.worktreePath);
12217
+ if (action2 === "stop" && mutation.status === "absent" && readManagedRuntimeState(resolved.worktreePath, resolved.workspace)) {
12218
+ throw new Error(
12219
+ "Retained managed runtime has no exact provider registration; routes were preserved."
12220
+ );
12221
+ }
11809
12222
  const routes = removeWorkspaceRoutesForWorktree(resolved.workspace, resolved.worktreePath);
11810
12223
  await ensureTraefikRoutesRemoved(routes);
11811
12224
  if (!quiet) {
@@ -11838,7 +12251,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
11838
12251
  opts.quiet
11839
12252
  );
11840
12253
  if (removeWorktree) {
11841
- if (resolved.worktree && !resolved.worktree.prunable && import_node_fs26.default.existsSync(resolved.worktreePath)) {
12254
+ if (resolved.worktree && !resolved.worktree.prunable && import_node_fs27.default.existsSync(resolved.worktreePath)) {
11842
12255
  const rm = (0, import_node_child_process22.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
11843
12256
  encoding: "utf-8"
11844
12257
  });
@@ -11857,7 +12270,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
11857
12270
  }
11858
12271
  return result;
11859
12272
  };
11860
- return resolved.worktree && !resolved.worktree.prunable && import_node_fs26.default.existsSync(resolved.worktreePath) ? withWorkspaceLifecycleLock(resolved.worktreePath, operation) : operation();
12273
+ return resolved.worktree && !resolved.worktree.prunable && import_node_fs27.default.existsSync(resolved.worktreePath) ? withWorkspaceLifecycleLock(resolved.worktreePath, operation) : operation();
11861
12274
  }
11862
12275
  async function mutateWorkspaceOwnedPath(action2, worktreePath, opts = {}) {
11863
12276
  const mainRepo = resolveRepoPath(opts.repoPath);
@@ -11892,15 +12305,16 @@ async function workspaceStop(target, opts = {}) {
11892
12305
  async function workspaceDown(target, opts = {}) {
11893
12306
  return runWorkspaceLifecycle("down", target, opts);
11894
12307
  }
11895
- var import_node_child_process22, import_node_fs26, import_node_path27, WORKSPACE_ALLOCATION_LOCK_WAIT_MS;
12308
+ var import_node_child_process22, import_node_fs27, import_node_path28, WORKSPACE_ALLOCATION_LOCK_WAIT_MS;
11896
12309
  var init_workspace_lifecycle = __esm({
11897
12310
  "src/core/workspace-lifecycle.ts"() {
11898
12311
  "use strict";
11899
12312
  import_node_child_process22 = require("child_process");
11900
- import_node_fs26 = __toESM(require("fs"));
11901
- import_node_path27 = __toESM(require("path"));
12313
+ import_node_fs27 = __toESM(require("fs"));
12314
+ import_node_path28 = __toESM(require("path"));
11902
12315
  init_devpod_mutation();
11903
12316
  init_devpod_workspaces();
12317
+ init_managed_runtime_state();
11904
12318
  init_repo_config();
11905
12319
  init_route_state();
11906
12320
  init_traefik_route_health();
@@ -11952,6 +12366,11 @@ async function environmentStop(repoPath, options = {}) {
11952
12366
  return withWorkspaceLifecycleLock(repoPath, async () => {
11953
12367
  const devpod = selectDevpodWorkspace(listDevpodWorkspaces(repoPath), repoPath);
11954
12368
  const mutation = devpod ? options.delete ? deleteOwnedDevpodWorkspace(devpod.id, repoPath) : stopOwnedDevpodWorkspace(devpod.id, repoPath) : { status: "absent" };
12369
+ if (!options.delete && mutation.status === "absent" && readManagedRuntimeState(repoPath, workspace)) {
12370
+ throw new Error(
12371
+ "Retained managed runtime has no exact provider registration; routes were preserved."
12372
+ );
12373
+ }
11955
12374
  const removedRoutes = removeHostRoutesWhere(
11956
12375
  (route) => sameWorkspacePath(route.repoPath, repoPath)
11957
12376
  );
@@ -11973,6 +12392,7 @@ var init_environment_stop = __esm({
11973
12392
  init_devpod_mutation();
11974
12393
  init_devpod_workspaces();
11975
12394
  init_host_routes();
12395
+ init_managed_runtime_state();
11976
12396
  init_traefik_route_health();
11977
12397
  init_workspace();
11978
12398
  init_workspace_lifecycle();
@@ -12470,17 +12890,17 @@ function asRecord3(value) {
12470
12890
  return value;
12471
12891
  }
12472
12892
  function readJson(filePath) {
12473
- if (!import_node_fs27.default.existsSync(filePath)) {
12893
+ if (!import_node_fs28.default.existsSync(filePath)) {
12474
12894
  return void 0;
12475
12895
  }
12476
12896
  try {
12477
- return JSON.parse(import_node_fs27.default.readFileSync(filePath, "utf-8"));
12897
+ return JSON.parse(import_node_fs28.default.readFileSync(filePath, "utf-8"));
12478
12898
  } catch {
12479
12899
  return void 0;
12480
12900
  }
12481
12901
  }
12482
12902
  function relative(repoPath, filePath) {
12483
- return import_node_path28.default.relative(repoPath, filePath) || ".";
12903
+ return import_node_path29.default.relative(repoPath, filePath) || ".";
12484
12904
  }
12485
12905
  function redactEnvAssignments(value) {
12486
12906
  return value.replace(
@@ -12521,7 +12941,7 @@ function inspectPackageManager(repoPath, pkg) {
12521
12941
  ["bun.lock", "bun"]
12522
12942
  ];
12523
12943
  for (const [fileName, name] of lockfiles) {
12524
- if (import_node_fs27.default.existsSync(import_node_path28.default.join(repoPath, fileName))) {
12944
+ if (import_node_fs28.default.existsSync(import_node_path29.default.join(repoPath, fileName))) {
12525
12945
  return { name, source: fileName };
12526
12946
  }
12527
12947
  }
@@ -12536,9 +12956,9 @@ function inspectNode(repoPath, pkg) {
12536
12956
  if (typeof engines?.node === "string") {
12537
12957
  return { version: engines.node, source: "package.json:engines.node" };
12538
12958
  }
12539
- const nvmrc = import_node_path28.default.join(repoPath, ".nvmrc");
12540
- if (import_node_fs27.default.existsSync(nvmrc)) {
12541
- const version = import_node_fs27.default.readFileSync(nvmrc, "utf-8").trim();
12959
+ const nvmrc = import_node_path29.default.join(repoPath, ".nvmrc");
12960
+ if (import_node_fs28.default.existsSync(nvmrc)) {
12961
+ const version = import_node_fs28.default.readFileSync(nvmrc, "utf-8").trim();
12542
12962
  return { version, source: ".nvmrc" };
12543
12963
  }
12544
12964
  return void 0;
@@ -12599,7 +13019,7 @@ function configuredComposeFiles(repoPath) {
12599
13019
  const files = config.apps.filter(
12600
13020
  (app) => app.runtime === "docker"
12601
13021
  ).flatMap((app) => app.docker.composeFiles).filter(
12602
- (fileName) => !import_node_path28.default.isAbsolute(fileName) && !import_node_path28.default.normalize(fileName).startsWith("..")
13022
+ (fileName) => !import_node_path29.default.isAbsolute(fileName) && !import_node_path29.default.normalize(fileName).startsWith("..")
12603
13023
  );
12604
13024
  return Array.from(new Set(files));
12605
13025
  } catch {
@@ -12617,7 +13037,7 @@ function composeFiles(repoPath) {
12617
13037
  ...configuredComposeFiles(repoPath)
12618
13038
  ];
12619
13039
  return Array.from(new Set(candidates)).filter(
12620
- (fileName) => import_node_fs27.default.existsSync(import_node_path28.default.join(repoPath, fileName))
13040
+ (fileName) => import_node_fs28.default.existsSync(import_node_path29.default.join(repoPath, fileName))
12621
13041
  );
12622
13042
  }
12623
13043
  function stringArray2(value) {
@@ -12655,7 +13075,7 @@ function inspectServices(repoPath) {
12655
13075
  const services = [];
12656
13076
  for (const fileName of composeFiles(repoPath)) {
12657
13077
  try {
12658
- const parsed = import_yaml6.default.parse(import_node_fs27.default.readFileSync(import_node_path28.default.join(repoPath, fileName), "utf-8"));
13078
+ const parsed = import_yaml6.default.parse(import_node_fs28.default.readFileSync(import_node_path29.default.join(repoPath, fileName), "utf-8"));
12659
13079
  const serviceMap = asRecord3(asRecord3(parsed)?.services);
12660
13080
  for (const [name, value] of Object.entries(serviceMap ?? {})) {
12661
13081
  const service = asRecord3(value);
@@ -12690,8 +13110,8 @@ function inspectServices(repoPath) {
12690
13110
  return services;
12691
13111
  }
12692
13112
  function inspectEnvFiles(repoPath) {
12693
- const files = import_node_fs27.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
12694
- const content = import_node_fs27.default.readFileSync(import_node_path28.default.join(repoPath, fileName), "utf-8");
13113
+ const files = import_node_fs28.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
13114
+ const content = import_node_fs28.default.readFileSync(import_node_path29.default.join(repoPath, fileName), "utf-8");
12695
13115
  const names = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#") && line.includes("=")).map((line) => line.split("=")[0]?.trim()).filter((name) => Boolean(name)).sort();
12696
13116
  return { path: fileName, names };
12697
13117
  });
@@ -12705,18 +13125,18 @@ function inspectEnvFiles(repoPath) {
12705
13125
  };
12706
13126
  }
12707
13127
  function inspectDevcontainer(repoPath) {
12708
- const dir = import_node_path28.default.join(repoPath, ".devcontainer");
12709
- if (!import_node_fs27.default.existsSync(dir)) {
13128
+ const dir = import_node_path29.default.join(repoPath, ".devcontainer");
13129
+ if (!import_node_fs28.default.existsSync(dir)) {
12710
13130
  return { exists: false, files: [] };
12711
13131
  }
12712
13132
  return {
12713
13133
  exists: true,
12714
- files: import_node_fs27.default.readdirSync(dir).filter((fileName) => import_node_fs27.default.statSync(import_node_path28.default.join(dir, fileName)).isFile()).sort().map((fileName) => `.devcontainer/${fileName}`)
13134
+ files: import_node_fs28.default.readdirSync(dir).filter((fileName) => import_node_fs28.default.statSync(import_node_path29.default.join(dir, fileName)).isFile()).sort().map((fileName) => `.devcontainer/${fileName}`)
12715
13135
  };
12716
13136
  }
12717
13137
  function inspectDevrouter(repoPath) {
12718
13138
  const configPath = getRepoConfigPath(repoPath);
12719
- if (!import_node_fs27.default.existsSync(configPath)) {
13139
+ if (!import_node_fs28.default.existsSync(configPath)) {
12720
13140
  return {
12721
13141
  exists: false,
12722
13142
  configPath,
@@ -12760,15 +13180,15 @@ function inspectAgentGuidance(repoPath) {
12760
13180
  ["AGENTS.md", "agents"],
12761
13181
  ["CLAUDE.md", "claude"]
12762
13182
  ]) {
12763
- if (import_node_fs27.default.existsSync(import_node_path28.default.join(repoPath, fileName))) {
13183
+ if (import_node_fs28.default.existsSync(import_node_path29.default.join(repoPath, fileName))) {
12764
13184
  results.push({ path: fileName, kind });
12765
13185
  }
12766
13186
  }
12767
- const skillsDir = import_node_path28.default.join(repoPath, ".agents", "skills");
12768
- if (import_node_fs27.default.existsSync(skillsDir)) {
12769
- for (const name of import_node_fs27.default.readdirSync(skillsDir).sort()) {
12770
- const skillPath = import_node_path28.default.join(skillsDir, name, "SKILL.md");
12771
- if (import_node_fs27.default.existsSync(skillPath)) {
13187
+ const skillsDir = import_node_path29.default.join(repoPath, ".agents", "skills");
13188
+ if (import_node_fs28.default.existsSync(skillsDir)) {
13189
+ for (const name of import_node_fs28.default.readdirSync(skillsDir).sort()) {
13190
+ const skillPath = import_node_path29.default.join(skillsDir, name, "SKILL.md");
13191
+ if (import_node_fs28.default.existsSync(skillPath)) {
12772
13192
  results.push({ path: relative(repoPath, skillPath), kind: "skill" });
12773
13193
  }
12774
13194
  }
@@ -12811,7 +13231,7 @@ function buildIssues(report) {
12811
13231
  }
12812
13232
  function inspectRepo(options = {}) {
12813
13233
  const repoPath = resolveRepoPath(options.repo);
12814
- const pkg = readJson(import_node_path28.default.join(repoPath, "package.json"));
13234
+ const pkg = readJson(import_node_path29.default.join(repoPath, "package.json"));
12815
13235
  const scripts = inspectScripts(pkg);
12816
13236
  const reportWithoutIssues = {
12817
13237
  repoPath,
@@ -12830,12 +13250,12 @@ function inspectRepo(options = {}) {
12830
13250
  issues: buildIssues(reportWithoutIssues)
12831
13251
  };
12832
13252
  }
12833
- var import_node_fs27, import_node_path28, import_yaml6;
13253
+ var import_node_fs28, import_node_path29, import_yaml6;
12834
13254
  var init_repo_inspect = __esm({
12835
13255
  "src/core/repo-inspect.ts"() {
12836
13256
  "use strict";
12837
- import_node_fs27 = __toESM(require("fs"));
12838
- import_node_path28 = __toESM(require("path"));
13257
+ import_node_fs28 = __toESM(require("fs"));
13258
+ import_node_path29 = __toESM(require("path"));
12839
13259
  import_yaml6 = __toESM(require("yaml"));
12840
13260
  init_repo_config();
12841
13261
  }
@@ -12940,7 +13360,7 @@ function requiredFileChecks(repoPath) {
12940
13360
  ".devcontainer/docker-compose.yml",
12941
13361
  ".devrouter.yml"
12942
13362
  ];
12943
- const missing = required.filter((fileName) => !import_node_fs28.default.existsSync(import_node_path29.default.join(repoPath, fileName)));
13363
+ const missing = required.filter((fileName) => !import_node_fs29.default.existsSync(import_node_path30.default.join(repoPath, fileName)));
12944
13364
  return {
12945
13365
  id: "repo.devcontainer.verify-files",
12946
13366
  level: missing.length === 0 ? "ok" : "error",
@@ -13159,12 +13579,12 @@ async function verifyDevcontainer(options = {}) {
13159
13579
  nextSteps: collectNextSteps3(checks)
13160
13580
  };
13161
13581
  }
13162
- var import_node_fs28, import_node_path29;
13582
+ var import_node_fs29, import_node_path30;
13163
13583
  var init_devcontainer_verify = __esm({
13164
13584
  "src/core/devcontainer-verify.ts"() {
13165
13585
  "use strict";
13166
- import_node_fs28 = __toESM(require("fs"));
13167
- import_node_path29 = __toESM(require("path"));
13586
+ import_node_fs29 = __toESM(require("fs"));
13587
+ import_node_path30 = __toESM(require("path"));
13168
13588
  init_capabilities();
13169
13589
  init_doctor();
13170
13590
  init_host_routes();
@@ -13461,7 +13881,7 @@ function packageManagerIssues(repo) {
13461
13881
  }
13462
13882
  function plannedFiles(repoPath, version) {
13463
13883
  const repo = inspectRepo({ repo: repoPath });
13464
- const projectName = sanitizeProjectName(import_node_path30.default.basename(repo.repoPath));
13884
+ const projectName = sanitizeProjectName(import_node_path31.default.basename(repo.repoPath));
13465
13885
  const nodeMajor = majorVersion(repo.node?.version, "24");
13466
13886
  const pnpmVersion = repo.packageManager?.name === "pnpm" && repo.packageManager.version ? repo.packageManager.version : DEFAULT_PNPM_VERSION;
13467
13887
  const port = inferPort2(repo);
@@ -13506,8 +13926,8 @@ function plannedFiles(repoPath, version) {
13506
13926
  };
13507
13927
  }
13508
13928
  function classifyFile(repoPath, file) {
13509
- const absolutePath = import_node_path30.default.join(repoPath, file.relativePath);
13510
- if (!import_node_fs29.default.existsSync(absolutePath)) {
13929
+ const absolutePath = import_node_path31.default.join(repoPath, file.relativePath);
13930
+ if (!import_node_fs30.default.existsSync(absolutePath)) {
13511
13931
  return {
13512
13932
  path: file.relativePath,
13513
13933
  action: "create",
@@ -13515,7 +13935,7 @@ function classifyFile(repoPath, file) {
13515
13935
  bytes: Buffer.byteLength(file.content)
13516
13936
  };
13517
13937
  }
13518
- const current = import_node_fs29.default.readFileSync(absolutePath, "utf-8");
13938
+ const current = import_node_fs30.default.readFileSync(absolutePath, "utf-8");
13519
13939
  if (!current.includes(MANAGED_MARKER2)) {
13520
13940
  return {
13521
13941
  path: file.relativePath,
@@ -13572,11 +13992,11 @@ function buildPlan(repoPath, dryRun, version) {
13572
13992
  };
13573
13993
  }
13574
13994
  function writeFile(repoPath, file) {
13575
- const absolutePath = import_node_path30.default.join(repoPath, file.relativePath);
13576
- import_node_fs29.default.mkdirSync(import_node_path30.default.dirname(absolutePath), { recursive: true });
13577
- import_node_fs29.default.writeFileSync(absolutePath, file.content, "utf-8");
13995
+ const absolutePath = import_node_path31.default.join(repoPath, file.relativePath);
13996
+ import_node_fs30.default.mkdirSync(import_node_path31.default.dirname(absolutePath), { recursive: true });
13997
+ import_node_fs30.default.writeFileSync(absolutePath, file.content, "utf-8");
13578
13998
  if (file.executable) {
13579
- import_node_fs29.default.chmodSync(absolutePath, 493);
13999
+ import_node_fs30.default.chmodSync(absolutePath, 493);
13580
14000
  }
13581
14001
  }
13582
14002
  function writeDevcontainer(options = {}) {
@@ -13614,12 +14034,12 @@ function writeDevcontainer(options = {}) {
13614
14034
  nextSteps: postWriteNextSteps(repoPath)
13615
14035
  };
13616
14036
  }
13617
- var import_node_fs29, import_node_path30, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
14037
+ var import_node_fs30, import_node_path31, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
13618
14038
  var init_devcontainer_write = __esm({
13619
14039
  "src/core/devcontainer-write.ts"() {
13620
14040
  "use strict";
13621
- import_node_fs29 = __toESM(require("fs"));
13622
- import_node_path30 = __toESM(require("path"));
14041
+ import_node_fs30 = __toESM(require("fs"));
14042
+ import_node_path31 = __toESM(require("path"));
13623
14043
  init_repo_config();
13624
14044
  init_repo_inspect();
13625
14045
  MANAGED_MARKER2 = "devrouter:managed devcontainer";
@@ -13805,11 +14225,11 @@ var init_profile_resolution = __esm({
13805
14225
  function fail(message) {
13806
14226
  throw new Error(message);
13807
14227
  }
13808
- function isRecord3(value) {
14228
+ function isRecord4(value) {
13809
14229
  return typeof value === "object" && value !== null && !Array.isArray(value);
13810
14230
  }
13811
14231
  function requireRecord(value, label) {
13812
- if (!isRecord3(value)) fail(`${label} must be a mapping.`);
14232
+ if (!isRecord4(value)) fail(`${label} must be a mapping.`);
13813
14233
  return value;
13814
14234
  }
13815
14235
  function requireExactKeys(value, allowed, label) {
@@ -14026,13 +14446,13 @@ function buildProfilePlanReport(options) {
14026
14446
  return { ...report, contractPath, bindings };
14027
14447
  }
14028
14448
  function loadContract(repoPath, contractPath) {
14029
- if (import_node_path31.default.isAbsolute(contractPath)) {
14449
+ if (import_node_path32.default.isAbsolute(contractPath)) {
14030
14450
  fail("profile plan contract path must be relative to the repository.");
14031
14451
  }
14032
14452
  const absolutePath = assertPathWithinRepo(contractPath, repoPath, "Profile plan contract");
14033
14453
  let metadata;
14034
14454
  try {
14035
- metadata = import_node_fs30.default.lstatSync(absolutePath);
14455
+ metadata = import_node_fs31.default.lstatSync(absolutePath);
14036
14456
  } catch (error) {
14037
14457
  fail(`could not inspect profile plan contract '${contractPath}': ${error.message}`);
14038
14458
  }
@@ -14042,16 +14462,16 @@ function loadContract(repoPath, contractPath) {
14042
14462
  if (metadata.size > MAX_CONTRACT_BYTES) {
14043
14463
  fail(`profile plan contract exceeds the ${MAX_CONTRACT_BYTES}-byte limit.`);
14044
14464
  }
14045
- const realRepo = import_node_fs30.default.realpathSync(repoPath);
14046
- const realContract = import_node_fs30.default.realpathSync(absolutePath);
14465
+ const realRepo = import_node_fs31.default.realpathSync(repoPath);
14466
+ const realContract = import_node_fs31.default.realpathSync(absolutePath);
14047
14467
  assertPathWithinRepo(realContract, realRepo, "Profile plan contract");
14048
- const source = import_node_fs30.default.readFileSync(absolutePath, "utf-8");
14468
+ const source = import_node_fs31.default.readFileSync(absolutePath, "utf-8");
14049
14469
  if (Buffer.byteLength(source, "utf-8") > MAX_CONTRACT_BYTES) {
14050
14470
  fail(`profile plan contract exceeds the ${MAX_CONTRACT_BYTES}-byte limit.`);
14051
14471
  }
14052
14472
  return {
14053
14473
  contract: parseProfilePlanContract(source),
14054
- contractPath: import_node_path31.default.relative(repoPath, absolutePath).split(import_node_path31.default.sep).join("/")
14474
+ contractPath: import_node_path32.default.relative(repoPath, absolutePath).split(import_node_path32.default.sep).join("/")
14055
14475
  };
14056
14476
  }
14057
14477
  function resolveProfilePlan(options) {
@@ -14065,12 +14485,12 @@ function resolveProfilePlan(options) {
14065
14485
  contractPath: loaded.contractPath
14066
14486
  });
14067
14487
  }
14068
- var import_node_fs30, import_node_path31, import_yaml7, CONTRACT_VERSION, MAX_CONTRACT_BYTES, MAX_APP_MAPPINGS, MAX_BINDING_KEYS, MAX_BINDING_VALUES, MAX_LITERAL_LENGTH, BINDING_KEY;
14488
+ var import_node_fs31, import_node_path32, import_yaml7, CONTRACT_VERSION, MAX_CONTRACT_BYTES, MAX_APP_MAPPINGS, MAX_BINDING_KEYS, MAX_BINDING_VALUES, MAX_LITERAL_LENGTH, BINDING_KEY;
14069
14489
  var init_profile_plan = __esm({
14070
14490
  "src/core/profile-plan.ts"() {
14071
14491
  "use strict";
14072
- import_node_fs30 = __toESM(require("fs"));
14073
- import_node_path31 = __toESM(require("path"));
14492
+ import_node_fs31 = __toESM(require("fs"));
14493
+ import_node_path32 = __toESM(require("path"));
14074
14494
  import_yaml7 = require("yaml");
14075
14495
  init_paths();
14076
14496
  init_profile_resolution();
@@ -14102,7 +14522,7 @@ async function runProfileResolveCommand(options) {
14102
14522
  async function runProfilePlanCommand(options) {
14103
14523
  const report = resolveProfilePlan(options);
14104
14524
  if (options.output) {
14105
- writeFileAtomically(import_node_path32.default.resolve(options.output), `${JSON.stringify(report, null, 2)}
14525
+ writeFileAtomically(import_node_path33.default.resolve(options.output), `${JSON.stringify(report, null, 2)}
14106
14526
  `);
14107
14527
  }
14108
14528
  if (options.json) {
@@ -14138,14 +14558,14 @@ function printProfilePlanSummary(report, output2) {
14138
14558
  process.stdout.write(`Binding ${key}: ${renderValues(report.bindings[key] ?? [])}
14139
14559
  `);
14140
14560
  }
14141
- if (output2) process.stdout.write(`Output: ${import_node_path32.default.resolve(output2)}
14561
+ if (output2) process.stdout.write(`Output: ${import_node_path33.default.resolve(output2)}
14142
14562
  `);
14143
14563
  }
14144
- var import_node_path32;
14564
+ var import_node_path33;
14145
14565
  var init_profile = __esm({
14146
14566
  "src/commands/profile.ts"() {
14147
14567
  "use strict";
14148
- import_node_path32 = __toESM(require("path"));
14568
+ import_node_path33 = __toESM(require("path"));
14149
14569
  init_atomic_file();
14150
14570
  init_output();
14151
14571
  init_profile_plan();
@@ -14435,7 +14855,7 @@ function sanitizeRouterId(value) {
14435
14855
  return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
14436
14856
  }
14437
14857
  function repoHash(repoPath) {
14438
- return (0, import_node_crypto10.createHash)("sha1").update(import_node_path33.default.resolve(repoPath)).digest("hex").slice(0, 12);
14858
+ return (0, import_node_crypto10.createHash)("sha1").update(import_node_path34.default.resolve(repoPath)).digest("hex").slice(0, 12);
14439
14859
  }
14440
14860
  function asDockerApp(app) {
14441
14861
  return app.runtime === "docker";
@@ -14514,11 +14934,11 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
14514
14934
  if (dockerApps.length === 0) {
14515
14935
  throw new Error("No docker apps selected to prepare compose overlay.");
14516
14936
  }
14517
- const cachePath = import_node_path33.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
14518
- import_node_fs31.default.mkdirSync(cachePath, { recursive: true });
14519
- const overlayPath = import_node_path33.default.join(cachePath, "compose.devrouter.yml");
14937
+ const cachePath = import_node_path34.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
14938
+ import_node_fs32.default.mkdirSync(cachePath, { recursive: true });
14939
+ const overlayPath = import_node_path34.default.join(cachePath, "compose.devrouter.yml");
14520
14940
  const overlayDocument = buildOverlayDocument(dockerApps, publishTcpPorts);
14521
- import_node_fs31.default.writeFileSync(overlayPath, import_yaml8.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
14941
+ import_node_fs32.default.writeFileSync(overlayPath, import_yaml8.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
14522
14942
  return {
14523
14943
  overlayPath,
14524
14944
  composeFiles: ensureComposeFiles(dockerApps),
@@ -14638,14 +15058,14 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
14638
15058
  const port = Number(match[1]);
14639
15059
  return Number.isInteger(port) && port > 0 ? port : void 0;
14640
15060
  }
14641
- var import_node_child_process27, import_node_crypto10, import_node_fs31, import_node_path33, import_yaml8;
15061
+ var import_node_child_process27, import_node_crypto10, import_node_fs32, import_node_path34, import_yaml8;
14642
15062
  var init_docker_run = __esm({
14643
15063
  "src/core/docker-run.ts"() {
14644
15064
  "use strict";
14645
15065
  import_node_child_process27 = require("child_process");
14646
15066
  import_node_crypto10 = require("crypto");
14647
- import_node_fs31 = __toESM(require("fs"));
14648
- import_node_path33 = __toESM(require("path"));
15067
+ import_node_fs32 = __toESM(require("fs"));
15068
+ import_node_path34 = __toESM(require("path"));
14649
15069
  import_yaml8 = __toESM(require("yaml"));
14650
15070
  init_docker_error_guidance();
14651
15071
  init_paths();
@@ -15467,13 +15887,13 @@ function measureWorktreeConsumption(worktreePath, options) {
15467
15887
  const startedAt = Date.now();
15468
15888
  let rootStat;
15469
15889
  try {
15470
- rootStat = import_node_fs32.default.lstatSync(worktreePath);
15890
+ rootStat = import_node_fs33.default.lstatSync(worktreePath);
15471
15891
  } catch (error) {
15472
15892
  return { status: "unknown", reason: describeError(error, worktreePath) };
15473
15893
  }
15474
15894
  let rootEntries;
15475
15895
  try {
15476
- rootEntries = import_node_fs32.default.readdirSync(worktreePath, { withFileTypes: true });
15896
+ rootEntries = import_node_fs33.default.readdirSync(worktreePath, { withFileTypes: true });
15477
15897
  } catch (error) {
15478
15898
  return { status: "unknown", reason: describeError(error, worktreePath) };
15479
15899
  }
@@ -15499,10 +15919,10 @@ function measureWorktreeConsumption(worktreePath, options) {
15499
15919
  timedOut = true;
15500
15920
  break;
15501
15921
  }
15502
- const entryPath = import_node_path34.default.join(dirPath, entry.name);
15922
+ const entryPath = import_node_path35.default.join(dirPath, entry.name);
15503
15923
  let entryStat;
15504
15924
  try {
15505
- entryStat = import_node_fs32.default.lstatSync(entryPath);
15925
+ entryStat = import_node_fs33.default.lstatSync(entryPath);
15506
15926
  } catch (error) {
15507
15927
  if (error?.code === "ENOENT") continue;
15508
15928
  unreadableReason = describeIncompleteWalk(error);
@@ -15512,7 +15932,7 @@ function measureWorktreeConsumption(worktreePath, options) {
15512
15932
  if (!entryStat.isDirectory()) continue;
15513
15933
  let childEntries;
15514
15934
  try {
15515
- childEntries = import_node_fs32.default.readdirSync(entryPath, { withFileTypes: true });
15935
+ childEntries = import_node_fs33.default.readdirSync(entryPath, { withFileTypes: true });
15516
15936
  } catch (error) {
15517
15937
  unreadableReason = describeIncompleteWalk(error);
15518
15938
  break;
@@ -15576,12 +15996,12 @@ function describeError(error, worktreePath) {
15576
15996
  }
15577
15997
  return `could not stat worktree path '${worktreePath}': ${error?.message ?? String(error)}`;
15578
15998
  }
15579
- var import_node_fs32, import_node_path34, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
15999
+ var import_node_fs33, import_node_path35, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
15580
16000
  var init_workspace_consumption = __esm({
15581
16001
  "src/core/workspace-consumption.ts"() {
15582
16002
  "use strict";
15583
- import_node_fs32 = __toESM(require("fs"));
15584
- import_node_path34 = __toESM(require("path"));
16003
+ import_node_fs33 = __toESM(require("fs"));
16004
+ import_node_path35 = __toESM(require("path"));
15585
16005
  init_devpod_environment();
15586
16006
  DEFAULT_DEADLINE_MS = 1e4;
15587
16007
  BLOCK_SIZE_BYTES = 512;
@@ -15589,7 +16009,7 @@ var init_workspace_consumption = __esm({
15589
16009
  });
15590
16010
 
15591
16011
  // src/core/workspace-cleanup.ts
15592
- function isRecord4(value) {
16012
+ function isRecord5(value) {
15593
16013
  return value !== null && typeof value === "object" && !Array.isArray(value);
15594
16014
  }
15595
16015
  function isSha(value) {
@@ -15682,7 +16102,7 @@ function evaluateWorkspaceActivity(evidence, cutoff) {
15682
16102
  }
15683
16103
  function readGitSnapshot(worktree, commandRunner) {
15684
16104
  const comparablePath = comparableWorkspacePath(worktree.path);
15685
- if (worktree.prunable || !import_node_fs33.default.existsSync(comparablePath)) {
16105
+ if (worktree.prunable || !import_node_fs34.default.existsSync(comparablePath)) {
15686
16106
  return { worktree, checkout: "missing", head: null, committerDate: null };
15687
16107
  }
15688
16108
  const head = gitOutput(comparablePath, ["rev-parse", "--verify", "HEAD"], commandRunner);
@@ -15733,14 +16153,14 @@ function parseGitHubChanges(value, project, branch) {
15733
16153
  if (!Array.isArray(value)) return void 0;
15734
16154
  const changes = [];
15735
16155
  for (const item of value) {
15736
- if (!isRecord4(item)) return void 0;
15737
- const repository = isRecord4(item.repository) ? item.repository.nameWithOwner : void 0;
15738
- const headRepository = isRecord4(item.headRepository) ? item.headRepository.nameWithOwner : void 0;
16156
+ if (!isRecord5(item)) return void 0;
16157
+ const repository = isRecord5(item.repository) ? item.repository.nameWithOwner : void 0;
16158
+ const headRepository = isRecord5(item.headRepository) ? item.headRepository.nameWithOwner : void 0;
15739
16159
  if (repository !== project || headRepository !== project || item.headRefName !== branch || !isSha(item.headRefOid) || typeof item.baseRefName !== "string") {
15740
16160
  continue;
15741
16161
  }
15742
16162
  const merged = item.state === "MERGED" && validTimestamp(item.mergedAt);
15743
- const mergeCommit = isRecord4(item.mergeCommit) && isSha(item.mergeCommit.oid) ? item.mergeCommit.oid : void 0;
16163
+ const mergeCommit = isRecord5(item.mergeCommit) && isSha(item.mergeCommit.oid) ? item.mergeCommit.oid : void 0;
15744
16164
  const baseSha = isSha(item.baseRefOid) ? item.baseRefOid : void 0;
15745
16165
  changes.push({
15746
16166
  sourceBranch: branch,
@@ -15757,14 +16177,14 @@ function parseGitLabChanges(value, _project, branch) {
15757
16177
  if (!Array.isArray(value)) return void 0;
15758
16178
  const changes = [];
15759
16179
  for (const item of value) {
15760
- if (!isRecord4(item)) return void 0;
16180
+ if (!isRecord5(item)) return void 0;
15761
16181
  const sourceProjectId = item.source_project_id;
15762
16182
  const targetProjectId = item.target_project_id;
15763
16183
  const sameProject = typeof sourceProjectId === "number" && typeof targetProjectId === "number" && sourceProjectId === targetProjectId;
15764
16184
  if (!sameProject || item.source_branch !== branch || !isSha(item.sha) || typeof item.target_branch !== "string") {
15765
16185
  continue;
15766
16186
  }
15767
- const diffRefs = isRecord4(item.diff_refs) ? item.diff_refs : void 0;
16187
+ const diffRefs = isRecord5(item.diff_refs) ? item.diff_refs : void 0;
15768
16188
  const mergeCommitSha = isSha(item.merge_commit_sha) ? item.merge_commit_sha : void 0;
15769
16189
  const baseSha = diffRefs && isSha(diffRefs.base_sha) ? diffRefs.base_sha : void 0;
15770
16190
  changes.push({
@@ -16207,7 +16627,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
16207
16627
  containers = measureContainersFn(worktreePaths);
16208
16628
  } catch (error) {
16209
16629
  const reason = `container measurement failed: ${describeCause(error)}`;
16210
- containers = new Map(worktreePaths.map((path34) => [path34, unknownContainers(reason)]));
16630
+ containers = new Map(worktreePaths.map((path35) => [path35, unknownContainers(reason)]));
16211
16631
  }
16212
16632
  }
16213
16633
  const rows = records.map((record) => {
@@ -16256,12 +16676,12 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
16256
16676
  workspaces: rows
16257
16677
  };
16258
16678
  }
16259
- var import_node_child_process29, import_node_fs33, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
16679
+ var import_node_child_process29, import_node_fs34, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
16260
16680
  var init_workspace_cleanup = __esm({
16261
16681
  "src/core/workspace-cleanup.ts"() {
16262
16682
  "use strict";
16263
16683
  import_node_child_process29 = require("child_process");
16264
- import_node_fs33 = __toESM(require("fs"));
16684
+ import_node_fs34 = __toESM(require("fs"));
16265
16685
  init_devpod_workspaces();
16266
16686
  init_host_routes();
16267
16687
  init_repo_config();
@@ -16438,7 +16858,7 @@ var init_version = __esm({
16438
16858
 
16439
16859
  // src/cli.ts
16440
16860
  var import_commander = require("commander");
16441
- var CLI_VERSION = true ? "0.0.52" : "0.0.0-dev";
16861
+ var CLI_VERSION = true ? "0.0.53" : "0.0.0-dev";
16442
16862
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
16443
16863
  function withErrorHandling(action2) {
16444
16864
  return async (...args) => {