@devrouter/cli 0.0.52 → 0.0.54

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.54" : "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`.',
@@ -5742,25 +5746,40 @@ function assertManagedContainerConfigUnchanged(options) {
5742
5746
  const fileArgs = configFiles.flatMap((file) => ["-f", file]);
5743
5747
  const result = (() => {
5744
5748
  try {
5745
- return (0, import_node_child_process7.spawnSync)(
5749
+ const composeArgs = [
5750
+ "compose",
5751
+ "--project-name",
5752
+ composeProject,
5753
+ "--project-directory",
5754
+ workingDirectory
5755
+ ];
5756
+ const env = managedComposeEnvironment(options.workspace);
5757
+ const rendered = (0, import_node_child_process7.spawnSync)(
5746
5758
  "docker",
5747
- [
5748
- "compose",
5749
- "--project-name",
5750
- composeProject,
5751
- "--project-directory",
5752
- workingDirectory,
5753
- ...fileArgs,
5754
- "config",
5755
- "--hash",
5756
- service
5757
- ],
5759
+ [...composeArgs, ...fileArgs, "config", "--format", "json"],
5758
5760
  {
5759
5761
  cwd: workingDirectory,
5760
5762
  encoding: "utf-8",
5761
- env: managedComposeEnvironment(options.workspace),
5763
+ env,
5762
5764
  stdio: ["ignore", "pipe", "ignore"],
5763
- timeout: COMPOSE_HASH_TIMEOUT_MS
5765
+ timeout: COMPOSE_HASH_TIMEOUT_MS,
5766
+ maxBuffer: 1024 * 1024
5767
+ }
5768
+ );
5769
+ if (rendered.status !== 0 || rendered.error || !rendered.stdout?.trim()) {
5770
+ throw new Error("Compose configuration rendering failed.");
5771
+ }
5772
+ return (0, import_node_child_process7.spawnSync)(
5773
+ "docker",
5774
+ [...composeArgs, "-f", "-", "config", "--no-interpolate", "--hash", service],
5775
+ {
5776
+ cwd: workingDirectory,
5777
+ encoding: "utf-8",
5778
+ env,
5779
+ input: rendered.stdout,
5780
+ stdio: ["pipe", "pipe", "ignore"],
5781
+ timeout: COMPOSE_HASH_TIMEOUT_MS,
5782
+ maxBuffer: 1024 * 1024
5764
5783
  }
5765
5784
  );
5766
5785
  } catch {
@@ -5814,13 +5833,14 @@ function startExactManagedServices(options) {
5814
5833
  );
5815
5834
  }
5816
5835
  }
5817
- function stopExactManagedService(containerId, service) {
5836
+ function stopExactManagedService(containerId, service, options) {
5818
5837
  assertSafeContainerId(containerId);
5819
5838
  const result = (0, import_node_child_process7.spawnSync)("docker", ["stop", containerId], {
5820
5839
  encoding: "utf-8",
5821
- stdio: "inherit"
5840
+ stdio: "inherit",
5841
+ ...options ? { timeout: options.timeoutMs } : {}
5822
5842
  });
5823
- if (result.status !== 0) {
5843
+ if (result.status !== 0 || result.error) {
5824
5844
  throw new Error(`Could not stop exact managed service '${service}' (${containerId}).`);
5825
5845
  }
5826
5846
  }
@@ -5844,6 +5864,198 @@ var init_devcontainer_profile = __esm({
5844
5864
  });
5845
5865
 
5846
5866
  // src/core/devpod-environment.ts
5867
+ function isRecord2(value) {
5868
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5869
+ }
5870
+ function assertFullContainerId(value) {
5871
+ if (typeof value !== "string" || !FULL_CONTAINER_ID_PATTERN.test(value)) {
5872
+ throw new Error("Managed stop Docker inspection returned an invalid container id.");
5873
+ }
5874
+ }
5875
+ function assertUniqueContainerIds(ids) {
5876
+ if (new Set(ids).size !== ids.length) {
5877
+ throw new Error("Managed stop Docker inspection returned duplicate container ids.");
5878
+ }
5879
+ }
5880
+ function parseDockerLines(stdout) {
5881
+ const lines = stdout.split(/\r?\n/);
5882
+ while (lines.at(-1) === "") lines.pop();
5883
+ if (lines.some((line) => line === "")) {
5884
+ throw new Error("Managed stop Docker inspection returned an empty record.");
5885
+ }
5886
+ return lines;
5887
+ }
5888
+ function runManagedStopDocker(args) {
5889
+ const result = (0, import_node_child_process8.spawnSync)("docker", args, {
5890
+ encoding: "utf-8",
5891
+ timeout: MANAGED_STOP_DOCKER_TIMEOUT_MS,
5892
+ maxBuffer: MANAGED_STOP_DOCKER_MAX_BUFFER
5893
+ });
5894
+ if (result.error || result.status !== 0) {
5895
+ throw new Error("Managed stop Docker inspection failed.");
5896
+ }
5897
+ if (typeof result.stdout !== "string") {
5898
+ throw new Error("Managed stop Docker inspection returned invalid output.");
5899
+ }
5900
+ return result.stdout;
5901
+ }
5902
+ function listManagedStopContainerIds(composeProject) {
5903
+ const stdout = runManagedStopDocker([
5904
+ "ps",
5905
+ "-a",
5906
+ "--no-trunc",
5907
+ "--filter",
5908
+ `label=${MANAGED_STOP_COMPOSE_LABEL}=${composeProject}`,
5909
+ "--format",
5910
+ "{{.ID}}"
5911
+ ]);
5912
+ const ids = parseDockerLines(stdout);
5913
+ ids.forEach(assertFullContainerId);
5914
+ assertUniqueContainerIds(ids);
5915
+ return ids;
5916
+ }
5917
+ function assertString(value) {
5918
+ if (typeof value !== "string") {
5919
+ throw new Error("Managed stop Docker inspection returned an invalid field.");
5920
+ }
5921
+ }
5922
+ function validateManagedStopSnapshot(value, composeProject) {
5923
+ if (!isRecord2(value)) {
5924
+ throw new Error("Managed stop Docker inspection returned an invalid record.");
5925
+ }
5926
+ assertFullContainerId(value.id);
5927
+ if (!isRecord2(value.state)) {
5928
+ throw new Error("Managed stop Docker inspection returned an invalid state.");
5929
+ }
5930
+ const status = value.state.Status;
5931
+ if (status !== "running" && status !== "exited" && status !== "created") {
5932
+ throw new Error("Managed stop Docker inspection returned an invalid state status.");
5933
+ }
5934
+ if (typeof value.state.Running !== "boolean") {
5935
+ throw new Error("Managed stop Docker inspection returned an invalid running flag.");
5936
+ }
5937
+ if (typeof value.state.Paused !== "boolean" || value.state.Paused) {
5938
+ throw new Error("Managed stop Docker inspection returned an invalid paused flag.");
5939
+ }
5940
+ if (typeof value.state.Restarting !== "boolean" || value.state.Restarting) {
5941
+ throw new Error("Managed stop Docker inspection returned an invalid restarting flag.");
5942
+ }
5943
+ if (typeof value.state.Dead !== "boolean" || value.state.Dead) {
5944
+ throw new Error("Managed stop Docker inspection returned an invalid dead flag.");
5945
+ }
5946
+ if (status === "running" !== value.state.Running) {
5947
+ throw new Error("Managed stop Docker inspection returned contradictory state.");
5948
+ }
5949
+ let health;
5950
+ if (value.state.Health !== void 0 && value.state.Health !== null) {
5951
+ if (!isRecord2(value.state.Health)) {
5952
+ throw new Error("Managed stop Docker inspection returned an invalid health field.");
5953
+ }
5954
+ assertString(value.state.Health.Status);
5955
+ health = { Status: value.state.Health.Status };
5956
+ }
5957
+ if (!isRecord2(value.labels)) {
5958
+ throw new Error("Managed stop Docker inspection returned invalid labels.");
5959
+ }
5960
+ for (const label of MANAGED_STOP_IDENTITY_LABELS) {
5961
+ assertString(value.labels[label]);
5962
+ }
5963
+ if (value.labels[MANAGED_STOP_COMPOSE_LABEL] !== composeProject) {
5964
+ throw new Error("Managed stop Docker inspection returned a foreign Compose project.");
5965
+ }
5966
+ if (!Array.isArray(value.mounts)) {
5967
+ throw new Error("Managed stop Docker inspection returned invalid mounts.");
5968
+ }
5969
+ const mounts = value.mounts.map((mount) => {
5970
+ if (!isRecord2(mount)) {
5971
+ throw new Error("Managed stop Docker inspection returned an invalid mount.");
5972
+ }
5973
+ assertString(mount.Type);
5974
+ assertString(mount.Source);
5975
+ assertString(mount.Destination);
5976
+ return {
5977
+ Type: mount.Type,
5978
+ Source: mount.Source,
5979
+ Destination: mount.Destination
5980
+ };
5981
+ });
5982
+ if (!isRecord2(value.networks)) {
5983
+ throw new Error("Managed stop Docker inspection returned invalid networks.");
5984
+ }
5985
+ return {
5986
+ id: value.id,
5987
+ state: {
5988
+ ...health ? { Health: health } : {},
5989
+ Status: status,
5990
+ Running: value.state.Running,
5991
+ Paused: false,
5992
+ Restarting: false,
5993
+ Dead: false
5994
+ },
5995
+ labels: {
5996
+ "com.docker.compose.project": value.labels["com.docker.compose.project"],
5997
+ "com.docker.compose.service": value.labels["com.docker.compose.service"],
5998
+ "com.docker.compose.project.working_dir": value.labels["com.docker.compose.project.working_dir"],
5999
+ "com.docker.compose.project.config_files": value.labels["com.docker.compose.project.config_files"],
6000
+ "com.docker.compose.config-hash": value.labels["com.docker.compose.config-hash"]
6001
+ },
6002
+ mounts,
6003
+ networks: {}
6004
+ };
6005
+ }
6006
+ function inspectManagedStopPopulation(ids, composeProject) {
6007
+ const stdout = runManagedStopDocker([
6008
+ "inspect",
6009
+ "--format",
6010
+ MANAGED_STOP_INSPECT_TEMPLATE,
6011
+ ...ids
6012
+ ]);
6013
+ const lines = parseDockerLines(stdout);
6014
+ if (lines.length !== ids.length) {
6015
+ throw new Error("Managed stop Docker inspection returned an incomplete population.");
6016
+ }
6017
+ const expected = new Set(ids);
6018
+ const seen = /* @__PURE__ */ new Set();
6019
+ const snapshots = lines.map((line) => {
6020
+ let parsed;
6021
+ try {
6022
+ parsed = JSON.parse(line);
6023
+ } catch {
6024
+ throw new Error("Managed stop Docker inspection returned malformed JSON.");
6025
+ }
6026
+ const snapshot = validateManagedStopSnapshot(parsed, composeProject);
6027
+ if (!expected.has(snapshot.id) || seen.has(snapshot.id)) {
6028
+ throw new Error("Managed stop Docker inspection returned an unexpected population.");
6029
+ }
6030
+ seen.add(snapshot.id);
6031
+ return snapshot;
6032
+ });
6033
+ return snapshots;
6034
+ }
6035
+ function requireSameContainerPopulation(expected, actual) {
6036
+ if (expected.length !== actual.length) {
6037
+ throw new Error("Managed stop Docker inspection observed a changed population.");
6038
+ }
6039
+ const expectedSet = new Set(expected);
6040
+ if (actual.some((id) => !expectedSet.has(id))) {
6041
+ throw new Error("Managed stop Docker inspection observed a changed population.");
6042
+ }
6043
+ }
6044
+ function inspectManagedStopContainers(composeProject) {
6045
+ if (typeof composeProject !== "string" || composeProject.length === 0 || composeProject !== composeProject.trim() || !MANAGED_STOP_PROJECT_PATTERN.test(composeProject)) {
6046
+ throw new Error("Managed stop Docker inspection requires a safe Compose project.");
6047
+ }
6048
+ const listedIds = listManagedStopContainerIds(composeProject);
6049
+ if (listedIds.length === 0) {
6050
+ const confirmedIds2 = listManagedStopContainerIds(composeProject);
6051
+ requireSameContainerPopulation(listedIds, confirmedIds2);
6052
+ return [];
6053
+ }
6054
+ const snapshots = inspectManagedStopPopulation(listedIds, composeProject);
6055
+ const confirmedIds = listManagedStopContainerIds(composeProject);
6056
+ requireSameContainerPopulation(listedIds, confirmedIds);
6057
+ return snapshots;
6058
+ }
5847
6059
  function inspectWorkspaceContainers(options) {
5848
6060
  let ids = options?.ids;
5849
6061
  if (!ids) {
@@ -5927,7 +6139,7 @@ function resolveRunningWorkspaceContainer(repoPath) {
5927
6139
  }
5928
6140
  return { id: container.id, workspacePath: repoMount.Destination };
5929
6141
  }
5930
- var import_node_child_process8, import_node_path16, SAFE_INSPECT_TEMPLATE, SIZE_INSPECT_TEMPLATE;
6142
+ 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
6143
  var init_devpod_environment = __esm({
5932
6144
  "src/core/devpod-environment.ts"() {
5933
6145
  "use strict";
@@ -5939,6 +6151,19 @@ var init_devpod_environment = __esm({
5939
6151
  /}$/,
5940
6152
  ',"sizeRw":{{json (index . "SizeRw")}},"sizeRootFs":{{json (index . "SizeRootFs")}}}'
5941
6153
  );
6154
+ 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":{}}';
6155
+ MANAGED_STOP_PROJECT_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
6156
+ FULL_CONTAINER_ID_PATTERN = /^[0-9a-f]{64}$/;
6157
+ MANAGED_STOP_DOCKER_TIMEOUT_MS = 5e3;
6158
+ MANAGED_STOP_DOCKER_MAX_BUFFER = 1024 * 1024;
6159
+ MANAGED_STOP_COMPOSE_LABEL = "com.docker.compose.project";
6160
+ MANAGED_STOP_IDENTITY_LABELS = [
6161
+ "com.docker.compose.project",
6162
+ "com.docker.compose.service",
6163
+ "com.docker.compose.project.working_dir",
6164
+ "com.docker.compose.project.config_files",
6165
+ "com.docker.compose.config-hash"
6166
+ ];
5942
6167
  }
5943
6168
  });
5944
6169
 
@@ -6907,7 +7132,8 @@ function listDevsyWorkspaces() {
6907
7132
  }
6908
7133
  const workspace = {
6909
7134
  id: candidate.id,
6910
- source: { localFolder: source.localFolder }
7135
+ source: { localFolder: source.localFolder },
7136
+ ...typeof candidate.context === "string" ? { context: candidate.context } : {}
6911
7137
  };
6912
7138
  if ("lastUsed" in candidate) {
6913
7139
  if (typeof candidate.lastUsed === "string") {
@@ -7586,372 +7812,898 @@ var init_devpod_workspaces = __esm({
7586
7812
  }
7587
7813
  });
7588
7814
 
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
- );
7815
+ // src/core/route-publication.ts
7816
+ function routedAppsFromConfig(config) {
7817
+ return config.apps.filter((app) => app.kind !== "dependency");
7613
7818
  }
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
7819
+ function configuredProxyAppsFromConfig(config) {
7820
+ return routedAppsFromConfig(config).filter(
7821
+ (app) => app.runtime === "proxy"
7626
7822
  );
7627
7823
  }
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) {
7824
+ function proxyAppsFromConfig(config) {
7825
+ const routedApps = routedAppsFromConfig(config);
7826
+ const proxyApps = configuredProxyAppsFromConfig(config);
7827
+ const unsupported = routedApps.filter((app) => app.runtime !== "proxy");
7828
+ if (unsupported.length > 0) {
7667
7829
  throw new Error(
7668
- `devsy workspace ${action2}${force ? " --force" : ""} failed for '${devsyId}': ${commandFailure2(result) || "unknown error"}`
7830
+ `Environment reconciliation supports proxy runtime apps only; unsupported: ${unsupported.map((app) => `${app.name} (${app.runtime})`).join(", ")}`
7669
7831
  );
7670
7832
  }
7833
+ return proxyApps;
7671
7834
  }
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") {
7835
+ async function ensureRouteInfrastructure(apps, options = {}) {
7836
+ ensureRouterFiles();
7837
+ await ensureNetwork(DEVNET_NAME);
7838
+ const tlsCoverage = await ensureTLSHostsCovered(
7839
+ apps.map((app) => app.host),
7840
+ options
7841
+ );
7842
+ for (const app of apps) {
7843
+ if (app.protocol === "tcp") {
7844
+ if (!isTLSEnabled()) {
7701
7845
  throw new Error(
7702
- `Devsy workspace '${devsyId}' still owns '${worktreePath}' after forced provider delete.`
7846
+ `TCP route '${app.name}' requires TLS. Run: ${tlsSetupCommand(options.repoPath)}`
7703
7847
  );
7704
7848
  }
7849
+ activateTcpProtocol(app.tcpProtocol);
7705
7850
  }
7706
- return { status: "changed" };
7707
- });
7708
- }
7709
- function stopOwnedDevsyWorkspace(devsyId, worktreePath) {
7710
- return mutateOwnedDevsyWorkspace("stop", devsyId, worktreePath);
7851
+ }
7852
+ startRouterStack();
7853
+ return tlsCoverage;
7711
7854
  }
7712
- function deleteOwnedDevsyWorkspace(devsyId, worktreePath) {
7713
- return mutateOwnedDevsyWorkspace("delete", devsyId, worktreePath);
7855
+ async function replacePublishedProxyRoutes(repoPath, config, workspace, options = {}) {
7856
+ const apps = proxyAppsFromConfig(config);
7857
+ const tlsCoverage = options.prepareInfrastructure === false ? { refreshed: false } : await ensureRouteInfrastructure(apps, { repoPath });
7858
+ const routes = apps.map((app) => {
7859
+ const upstream = parseUpstream(app.upstream);
7860
+ return {
7861
+ name: app.name,
7862
+ host: app.host,
7863
+ protocol: app.protocol,
7864
+ tcpProtocol: app.protocol === "tcp" ? app.tcpProtocol : void 0,
7865
+ repoPath,
7866
+ port: upstream.port,
7867
+ mode: "proxy",
7868
+ upstreamHost: upstream.upstreamHost,
7869
+ workspace
7870
+ };
7871
+ });
7872
+ replaceHostRoutesForRepo(repoPath, routes);
7873
+ return { routes, tlsRefreshed: tlsCoverage.refreshed };
7714
7874
  }
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);
7875
+ var init_route_publication = __esm({
7876
+ "src/core/route-publication.ts"() {
7877
+ "use strict";
7878
+ init_docker();
7879
+ init_host_routes();
7880
+ init_router();
7881
+ init_tls();
7722
7882
  }
7723
- return id;
7724
- }
7725
- async function startDevsyWorkspace(options) {
7726
- const activity = options.recreate ? "Devsy recreate" : "Devsy start";
7727
- let agent;
7728
- try {
7729
- agent = requireReadyDevsyAgent();
7883
+ });
7884
+
7885
+ // src/core/workspace-ownership.ts
7886
+ function commandError(command, repoPath, stderr) {
7887
+ return new Error(
7888
+ `${command} failed for '${repoPath}': ${stderr?.trim() || "not a Git repository"}`
7889
+ );
7890
+ }
7891
+ function resolveGitCommonDir(repoPath) {
7892
+ const result = (0, import_node_child_process15.spawnSync)("git", ["-C", repoPath, "rev-parse", "--git-common-dir"], {
7893
+ encoding: "utf-8",
7894
+ env: READ_ONLY_GIT_ENV
7895
+ });
7896
+ const output2 = result.stdout.trim();
7897
+ if (result.status !== 0 || !output2) {
7898
+ throw commandError("Could not resolve the Git common directory", repoPath, result.stderr);
7899
+ }
7900
+ return comparableWorkspacePath(import_node_path20.default.isAbsolute(output2) ? output2 : import_node_path20.default.resolve(repoPath, output2));
7901
+ }
7902
+ function resolveGitTopLevel(repoPath) {
7903
+ const result = (0, import_node_child_process15.spawnSync)("git", ["-C", repoPath, "rev-parse", "--show-toplevel"], {
7904
+ encoding: "utf-8",
7905
+ env: READ_ONLY_GIT_ENV
7906
+ });
7907
+ const output2 = result.stdout.trim();
7908
+ if (result.status !== 0 || !output2) {
7909
+ throw commandError("Could not resolve the Git checkout root", repoPath, result.stderr);
7910
+ }
7911
+ return comparableWorkspacePath(output2);
7912
+ }
7913
+ function listGitWorktrees(repoPath) {
7914
+ const result = (0, import_node_child_process15.spawnSync)("git", ["-C", repoPath, "worktree", "list", "--porcelain"], {
7915
+ encoding: "utf-8",
7916
+ env: READ_ONLY_GIT_ENV
7917
+ });
7918
+ if (result.status !== 0) {
7919
+ throw commandError("git worktree list", repoPath, result.stderr);
7920
+ }
7921
+ const worktrees = [];
7922
+ let current = {};
7923
+ const finish = () => {
7924
+ if (!current.path) return;
7925
+ worktrees.push({
7926
+ path: comparableWorkspacePath(current.path),
7927
+ branch: current.branch,
7928
+ locked: current.locked ?? false,
7929
+ prunable: current.prunable ?? false
7930
+ });
7931
+ current = {};
7932
+ };
7933
+ for (const line of `${result.stdout}
7934
+ `.split("\n")) {
7935
+ if (line.startsWith("worktree ")) {
7936
+ finish();
7937
+ current.path = line.slice("worktree ".length).trim();
7938
+ } else if (line.startsWith("branch ")) {
7939
+ current.branch = line.slice("branch ".length).trim().replace(/^refs\/heads\//, "");
7940
+ } else if (line === "locked" || line.startsWith("locked ")) {
7941
+ current.locked = true;
7942
+ } else if (line === "prunable" || line.startsWith("prunable ")) {
7943
+ current.prunable = true;
7944
+ } else if (line === "") {
7945
+ finish();
7946
+ }
7947
+ }
7948
+ return worktrees;
7949
+ }
7950
+ function ownershipDirectory(repoPath) {
7951
+ return import_node_path20.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
7952
+ }
7953
+ function validateWorkspace(value, label) {
7954
+ if (typeof value !== "string" || wsFromBranch(value) !== value) {
7955
+ throw new Error(`invalid workspace ownership ${label}`);
7956
+ }
7957
+ return value;
7958
+ }
7959
+ function validateTimestamp(value, label) {
7960
+ if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
7961
+ throw new Error(`invalid workspace ownership ${label}`);
7962
+ }
7963
+ return value;
7964
+ }
7965
+ function validateRecord(value, expectedWorkspace) {
7966
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
7967
+ throw new Error("invalid workspace ownership record");
7968
+ }
7969
+ const candidate = value;
7970
+ if (candidate.version !== OWNERSHIP_VERSION) {
7971
+ throw new Error(`unsupported workspace ownership version '${String(candidate.version)}'`);
7972
+ }
7973
+ const workspace = validateWorkspace(candidate.workspace, "workspace");
7974
+ if (expectedWorkspace && workspace !== expectedWorkspace) {
7975
+ throw new Error(
7976
+ `workspace ownership file '${expectedWorkspace}' contains identity '${workspace}'`
7977
+ );
7978
+ }
7979
+ if (typeof candidate.worktreePath !== "string" || !import_node_path20.default.isAbsolute(candidate.worktreePath)) {
7980
+ throw new Error("invalid workspace ownership worktreePath");
7981
+ }
7982
+ if (candidate.branch !== null && typeof candidate.branch !== "string") {
7983
+ throw new Error("invalid workspace ownership branch");
7984
+ }
7985
+ const devpodId = validateWorkspace(candidate.devpodId, "devpodId");
7986
+ return {
7987
+ version: OWNERSHIP_VERSION,
7988
+ workspace,
7989
+ worktreePath: comparableWorkspacePath(candidate.worktreePath),
7990
+ branch: candidate.branch,
7991
+ devpodId,
7992
+ createdAt: validateTimestamp(candidate.createdAt, "createdAt"),
7993
+ updatedAt: validateTimestamp(candidate.updatedAt, "updatedAt")
7994
+ };
7995
+ }
7996
+ function recordPath(repoPath, workspace) {
7997
+ return import_node_path20.default.join(
7998
+ ownershipDirectory(repoPath),
7999
+ `${validateWorkspace(workspace, "workspace")}.json`
8000
+ );
8001
+ }
8002
+ function readRecordFile(filePath, expectedWorkspace) {
8003
+ let parsed;
8004
+ try {
8005
+ parsed = JSON.parse(import_node_fs20.default.readFileSync(filePath, "utf-8"));
7730
8006
  } catch (error) {
7731
- if (!(error instanceof DevsyAgentReadinessError)) throw error;
7732
- const repair = devsyAgentRepairSuggestion(error.inspection);
7733
- throw new Error(`${error.message}. ${repair}`);
8007
+ if (error instanceof SyntaxError) {
8008
+ throw new Error(`invalid workspace ownership JSON at '${filePath}'`);
8009
+ }
8010
+ throw error;
7734
8011
  }
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.`);
8012
+ return validateRecord(parsed, expectedWorkspace);
8013
+ }
8014
+ function readWorkspaceOwnership(repoPath, workspace) {
8015
+ const filePath = recordPath(repoPath, workspace);
8016
+ try {
8017
+ return readRecordFile(filePath, workspace);
8018
+ } catch (error) {
8019
+ if (error.code === "ENOENT") return void 0;
8020
+ throw error;
8021
+ }
8022
+ }
8023
+ function listWorkspaceOwnership(repoPath) {
8024
+ const directory = ownershipDirectory(repoPath);
8025
+ return listWorkspaceOwnershipInDirectory(directory);
8026
+ }
8027
+ function listWorkspaceOwnershipInDirectory(directory) {
8028
+ let entries;
8029
+ try {
8030
+ entries = import_node_fs20.default.readdirSync(directory, { withFileTypes: true });
8031
+ } catch (error) {
8032
+ if (error.code === "ENOENT") return [];
8033
+ throw error;
8034
+ }
8035
+ return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
8036
+ const workspace = entry.name.slice(0, -".json".length);
8037
+ validateWorkspace(workspace, "filename");
8038
+ return readRecordFile(import_node_path20.default.join(directory, entry.name), workspace);
8039
+ });
8040
+ }
8041
+ function writeWorkspaceOwnershipInDirectory(directory, input2) {
8042
+ const workspace = validateWorkspace(input2.workspace, "workspace");
8043
+ const devpodId = validateWorkspace(input2.devpodId, "devpodId");
8044
+ const worktreePath = comparableWorkspacePath(input2.worktreePath);
8045
+ const filePath = import_node_path20.default.join(directory, `${workspace}.json`);
8046
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8047
+ const records = listWorkspaceOwnershipInDirectory(directory);
8048
+ const existing = records.find((record2) => record2.workspace === workspace);
8049
+ if (existing && !sameWorkspacePath(existing.worktreePath, worktreePath)) {
8050
+ throw new Error(
8051
+ `Workspace '${workspace}' already belongs to '${existing.worktreePath}', refusing '${worktreePath}'.`
8052
+ );
8053
+ }
8054
+ const pathOwner = records.find(
8055
+ (record2) => record2.workspace !== workspace && sameWorkspacePath(record2.worktreePath, worktreePath)
8056
+ );
8057
+ if (pathOwner) {
8058
+ throw new Error(
8059
+ `Worktree '${worktreePath}' is already owned by workspace '${pathOwner.workspace}'.`
8060
+ );
8061
+ }
8062
+ if (existing && existing.devpodId !== devpodId) {
8063
+ throw new Error(
8064
+ `Workspace '${workspace}' already owns DevPod '${existing.devpodId}', refusing '${devpodId}'.`
8065
+ );
8066
+ }
8067
+ const record = {
8068
+ version: OWNERSHIP_VERSION,
8069
+ workspace,
8070
+ worktreePath,
8071
+ branch: input2.branch ?? null,
8072
+ devpodId,
8073
+ createdAt: existing?.createdAt ?? validateTimestamp(now, "createdAt"),
8074
+ updatedAt: validateTimestamp(now, "updatedAt")
8075
+ };
8076
+ writeFileAtomically(filePath, `${JSON.stringify(record, null, 2)}
8077
+ `);
8078
+ return record;
8079
+ }
8080
+ function removeWorkspaceOwnershipInDirectory(directory, workspace) {
8081
+ const filePath = import_node_path20.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
8082
+ try {
8083
+ import_node_fs20.default.rmSync(filePath);
8084
+ return true;
8085
+ } catch (error) {
8086
+ if (error.code === "ENOENT") return false;
8087
+ throw error;
8088
+ }
8089
+ }
8090
+ function sameOwnershipRecord(left, right) {
8091
+ 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;
8092
+ }
8093
+ function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
8094
+ const filePath = import_node_path20.default.join(
8095
+ directory,
8096
+ `${validateWorkspace(expected.workspace, "workspace")}.json`
8097
+ );
8098
+ let current;
8099
+ try {
8100
+ current = readRecordFile(filePath, expected.workspace);
8101
+ } catch (error) {
8102
+ if (error.code === "ENOENT") return "absent";
8103
+ throw error;
8104
+ }
8105
+ if (!sameOwnershipRecord(current, expected)) return "changed";
8106
+ import_node_fs20.default.rmSync(filePath);
8107
+ return "removed";
8108
+ }
8109
+ function withWorkspaceOwnershipTransaction(repoPath, operation, options = {}) {
8110
+ const directory = ownershipDirectory(repoPath);
8111
+ import_node_fs20.default.mkdirSync(directory, { recursive: true });
8112
+ return withFileLockSync(
8113
+ import_node_path20.default.join(directory, ".lock"),
8114
+ {
8115
+ activity: "workspace ownership transaction",
8116
+ target: `'${repoPath}'`,
8117
+ waitMs: options.waitMs ?? 5e3
8118
+ },
8119
+ () => operation({
8120
+ list: () => listWorkspaceOwnershipInDirectory(directory),
8121
+ write: (input2) => writeWorkspaceOwnershipInDirectory(directory, input2),
8122
+ remove: (workspace) => removeWorkspaceOwnershipInDirectory(directory, workspace),
8123
+ removeIfMatches: (expected) => removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected)
8124
+ })
8125
+ );
8126
+ }
8127
+ function providerPathOwner(providerWorkspaces, worktreePath) {
8128
+ const owners = providerWorkspaces.filter(
8129
+ (workspace) => sameWorkspacePath(workspace.source.localFolder, worktreePath)
8130
+ );
8131
+ if (owners.length > 1) {
8132
+ throw new Error(
8133
+ `Worktree '${worktreePath}' is registered by multiple workspace runtimes; no identity was claimed.`
8134
+ );
8135
+ }
8136
+ return owners[0];
8137
+ }
8138
+ function persistedWorkspaceOwners(repoPath, worktreePath) {
8139
+ const owners = /* @__PURE__ */ new Map();
8140
+ for (const worktree of listGitWorktrees(repoPath)) {
8141
+ if (sameWorkspacePath(worktree.path, worktreePath)) continue;
8142
+ let workspace;
8143
+ try {
8144
+ workspace = readPersistedWorkspace(worktree.path);
8145
+ } catch (error) {
8146
+ if (!import_node_fs20.default.existsSync(worktree.path)) continue;
8147
+ throw error;
8148
+ }
8149
+ if (!workspace) continue;
8150
+ const existing = owners.get(workspace);
8151
+ if (existing && !sameWorkspacePath(existing, worktree.path)) {
8152
+ throw new Error(`Persisted workspace identity '${workspace}' belongs to multiple worktrees.`);
8153
+ }
8154
+ owners.set(workspace, worktree.path);
8155
+ }
8156
+ return owners;
8157
+ }
8158
+ function claimConflict(workspace, devpodId, worktreePath, records, providerWorkspaces, persistedOwners, exactRecord) {
8159
+ const recordOwner = records.find(
8160
+ (record) => record !== exactRecord && (record.workspace === workspace || record.devpodId === devpodId)
8161
+ );
8162
+ if (recordOwner) {
8163
+ return `workspace owner record '${recordOwner.workspace}' for '${recordOwner.worktreePath}'`;
8164
+ }
8165
+ const providerOwner = providerWorkspaces.find(
8166
+ (providerWorkspace) => providerWorkspace.id === devpodId && !sameWorkspacePath(providerWorkspace.source.localFolder, worktreePath)
8167
+ );
8168
+ if (providerOwner) {
8169
+ return `workspace runtime identity '${providerOwner.id}' already belongs to '${providerOwner.source.localFolder}'`;
8170
+ }
8171
+ const persistedOwner = persistedOwners.get(workspace);
8172
+ if (persistedOwner) {
8173
+ return `persisted checkout metadata for '${persistedOwner}'`;
8174
+ }
8175
+ return void 0;
8176
+ }
8177
+ function claimWorkspaceIdentity(repoPath, input2) {
8178
+ const worktreePath = comparableWorkspacePath(repoPath);
8179
+ const exactProvider = providerPathOwner(input2.providerWorkspaces, worktreePath);
8180
+ return withWorkspaceOwnershipTransaction(repoPath, (transaction) => {
8181
+ const records = transaction.list();
8182
+ const exactRecords = records.filter(
8183
+ (record) => sameWorkspacePath(record.worktreePath, worktreePath)
8184
+ );
8185
+ if (exactRecords.length > 1) {
8186
+ throw new Error(
8187
+ `Worktree '${worktreePath}' has multiple workspace owner records; no identity was claimed.`
8188
+ );
8189
+ }
8190
+ const exactRecord = exactRecords[0];
8191
+ const persisted = readPersistedWorkspace(worktreePath);
8192
+ const persistedOwners = persistedWorkspaceOwners(repoPath, worktreePath);
8193
+ if (exactRecord) {
8194
+ if (persisted && persisted !== exactRecord.workspace) {
8195
+ throw new Error(
8196
+ `Persisted workspace identity '${persisted}' disagrees with owner record '${exactRecord.workspace}'.`
8197
+ );
7742
8198
  }
8199
+ if (exactProvider && exactProvider.id !== exactRecord.devpodId) {
8200
+ throw new Error(
8201
+ `Workspace runtime '${exactProvider.id}' disagrees with owner record '${exactRecord.devpodId}'.`
8202
+ );
8203
+ }
8204
+ const conflict2 = claimConflict(
8205
+ exactRecord.workspace,
8206
+ exactRecord.devpodId,
8207
+ worktreePath,
8208
+ records,
8209
+ input2.providerWorkspaces,
8210
+ persistedOwners,
8211
+ exactRecord
8212
+ );
8213
+ if (conflict2) {
8214
+ throw new Error(
8215
+ `Workspace '${exactRecord.workspace}' conflicts with ${conflict2}; no identity was claimed.`
8216
+ );
8217
+ }
8218
+ if (!persisted) persistWorkspace(worktreePath, exactRecord.workspace);
8219
+ return transaction.write({
8220
+ workspace: exactRecord.workspace,
8221
+ worktreePath,
8222
+ branch: input2.branch ?? null,
8223
+ devpodId: exactRecord.devpodId
8224
+ });
7743
8225
  }
7744
- if (!devsyId && options.recreate) {
7745
- throw new Error("Cannot recreate a Devsy workspace before its exact id is known.");
8226
+ if (persisted && exactProvider && persisted !== exactProvider.id) {
8227
+ throw new Error(
8228
+ `Persisted workspace identity '${persisted}' disagrees with workspace runtime '${exactProvider.id}'.`
8229
+ );
7746
8230
  }
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}`
8231
+ let workspace = persisted ?? exactProvider?.id;
8232
+ let devpodId = exactProvider?.id ?? persisted;
8233
+ if (!workspace || !devpodId) {
8234
+ if (input2.unavailableRuntimes.length > 0) {
8235
+ throw new Error(
8236
+ `Cannot claim a new workspace identity because these runtime registries are unavailable: ${input2.unavailableRuntimes.join(", ")}.`
8237
+ );
8238
+ }
8239
+ const candidate = workspaceIdentityCandidates(input2.source).find(
8240
+ (next) => !claimConflict(
8241
+ next,
8242
+ next,
8243
+ worktreePath,
8244
+ records,
8245
+ input2.providerWorkspaces,
8246
+ persistedOwners
8247
+ )
8248
+ );
8249
+ if (!candidate) {
8250
+ throw new Error(
8251
+ `Could not allocate a collision-safe workspace identity for '${worktreePath}'.`
8252
+ );
8253
+ }
8254
+ workspace = candidate;
8255
+ devpodId = candidate;
8256
+ }
8257
+ const conflict = claimConflict(
8258
+ workspace,
8259
+ devpodId,
8260
+ worktreePath,
8261
+ records,
8262
+ input2.providerWorkspaces,
8263
+ persistedOwners
8264
+ );
8265
+ if (conflict) {
8266
+ throw new Error(
8267
+ `Workspace '${workspace}' conflicts with ${conflict}; no identity was claimed.`
7757
8268
  );
7758
8269
  }
7759
- if (options.inactivityTimeout) {
7760
- args.push("--provider-option", `INACTIVITY_TIMEOUT=${options.inactivityTimeout}`);
8270
+ const written = transaction.write({
8271
+ workspace,
8272
+ worktreePath,
8273
+ branch: input2.branch ?? null,
8274
+ devpodId
8275
+ });
8276
+ try {
8277
+ persistWorkspace(worktreePath, workspace);
8278
+ } catch (error) {
8279
+ const cleanup = transaction.removeIfMatches(written);
8280
+ if (cleanup !== "removed") {
8281
+ const detail = error instanceof Error ? error.message : String(error);
8282
+ throw new Error(
8283
+ `Could not persist workspace identity and owner-record rollback was '${cleanup}': ${detail}`
8284
+ );
8285
+ }
8286
+ throw error;
7761
8287
  }
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;
8288
+ return written;
8289
+ });
8290
+ }
8291
+ function removeWorkspaceOwnership(repoPath, workspace) {
8292
+ return withWorkspaceOwnershipTransaction(
8293
+ repoPath,
8294
+ (transaction) => transaction.remove(workspace)
8295
+ );
8296
+ }
8297
+ function inspectWorkspaceOwnership(record, worktrees, devpods) {
8298
+ const worktree = worktrees.find(
8299
+ (candidate) => sameWorkspacePath(candidate.path, record.worktreePath)
8300
+ );
8301
+ const devpodOwnership = devpods ? inspectDevpodWorkspaceOwnership(devpods, record.devpodId, record.worktreePath) : void 0;
8302
+ const devpodStatus = devpodOwnership?.status ?? "unknown";
8303
+ if (devpodStatus === "conflict") {
8304
+ return { ownerStatus: "conflict", devpodStatus, worktree };
8305
+ }
8306
+ if (worktree?.locked) {
8307
+ return { ownerStatus: "locked", devpodStatus, worktree };
8308
+ }
8309
+ if ((!worktree || worktree.prunable) && import_node_fs20.default.existsSync(record.worktreePath)) {
8310
+ return { ownerStatus: "conflict", devpodStatus, worktree };
8311
+ }
8312
+ if (!worktree || worktree.prunable) {
8313
+ return { ownerStatus: "missing", devpodStatus, worktree };
8314
+ }
8315
+ let persisted;
8316
+ try {
8317
+ persisted = readPersistedWorkspace(worktree.path);
8318
+ } catch {
8319
+ return { ownerStatus: "conflict", devpodStatus, worktree };
8320
+ }
8321
+ return {
8322
+ ownerStatus: persisted === record.workspace ? "present" : "conflict",
8323
+ devpodStatus,
8324
+ worktree
8325
+ };
8326
+ }
8327
+ function listMissingWorkspaceOwnership(repoPath) {
8328
+ const worktrees = listGitWorktrees(repoPath);
8329
+ return listWorkspaceOwnership(repoPath).filter(
8330
+ (record) => inspectWorkspaceOwnership(record, worktrees, void 0).ownerStatus === "missing"
8331
+ );
8332
+ }
8333
+ var import_node_child_process15, import_node_fs20, import_node_path20, READ_ONLY_GIT_ENV, OWNERSHIP_VERSION, OWNERSHIP_DIR;
8334
+ var init_workspace_ownership = __esm({
8335
+ "src/core/workspace-ownership.ts"() {
8336
+ "use strict";
8337
+ import_node_child_process15 = require("child_process");
8338
+ import_node_fs20 = __toESM(require("fs"));
8339
+ import_node_path20 = __toESM(require("path"));
8340
+ init_atomic_file();
8341
+ init_devpod_workspaces();
8342
+ init_file_lock();
8343
+ init_workspace();
8344
+ READ_ONLY_GIT_ENV = { ...process.env, GIT_OPTIONAL_LOCKS: "0" };
8345
+ OWNERSHIP_VERSION = 1;
8346
+ OWNERSHIP_DIR = import_node_path20.default.join("devrouter", "workspaces");
8347
+ }
8348
+ });
8349
+
8350
+ // src/core/managed-devsy-stop.ts
8351
+ function sameSet(left, right) {
8352
+ return JSON.stringify([...left].sort()) === JSON.stringify([...right].sort());
8353
+ }
8354
+ function containerIdentity(container) {
8355
+ return JSON.stringify({ id: container.id, labels: container.labels, mounts: container.mounts });
8356
+ }
8357
+ function stopRetainedManagedDevsyWorkspace(options) {
8358
+ const { repoPath, devsyId } = options;
8359
+ const linked = isLinkedWorktree(repoPath);
8360
+ const workspace = linked ? resolveWorktreeWorkspace(repoPath) : void 0;
8361
+ const retainedState = readManagedRuntimeState(repoPath, workspace);
8362
+ if (!retainedState) return false;
8363
+ const state = retainedState;
8364
+ if (state.devpodId !== devsyId || linked && !workspace) {
8365
+ throw new Error("Managed stop requires the exact retained workspace identity.");
8366
+ }
8367
+ const workspaceEnv = workspace ? { token: workspace, gitCommonDir: resolveGitCommonDir(repoPath) } : void 0;
8368
+ function registration() {
8369
+ resetWorkspaceRuntimeCaches();
8370
+ if (resolveWorkspaceRuntimeOrDefault(repoPath) !== "devsy") {
8371
+ throw new Error("Managed stop provider selection changed.");
7775
8372
  }
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}`;
8373
+ const owner = inspectDevsyWorkspaceOwnership(listDevsyWorkspaces(), devsyId, repoPath);
8374
+ if (owner.status !== "owned") {
8375
+ throw new Error("Managed stop requires one exact retained Devsy registration.");
8376
+ }
8377
+ if (workspace) {
8378
+ const record = readWorkspaceOwnership(repoPath, workspace);
8379
+ if (!record || record.devpodId !== devsyId || !sameWorkspacePath(record.worktreePath, repoPath) || resolveWorktreeWorkspace(repoPath) !== workspace || resolveGitCommonDir(repoPath) !== workspaceEnv?.gitCommonDir) {
8380
+ throw new Error("Managed stop workspace ownership changed.");
8381
+ }
8382
+ }
8383
+ return owner.workspace;
8384
+ }
8385
+ const context = registration().context;
8386
+ if (!context || !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(context) || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(devsyId)) {
8387
+ throw new Error("Managed stop requires a valid exact Devsy context and workspace ID.");
8388
+ }
8389
+ const devsyRoot = import_node_path21.default.resolve(process.env.DEVSY_HOME || import_node_path21.default.join(import_node_os2.default.homedir(), ".devsy"));
8390
+ const featureDirectory = import_node_path21.default.join(
8391
+ devsyRoot,
8392
+ "contexts",
8393
+ context,
8394
+ "workspaces",
8395
+ devsyId,
8396
+ "agent",
8397
+ ".docker-compose"
8398
+ );
8399
+ function prove(previous) {
8400
+ if (registration().context !== context || JSON.stringify(readManagedRuntimeState(repoPath, workspace)) !== JSON.stringify(state)) {
8401
+ throw new Error("Managed stop retained context or runtime record changed.");
8402
+ }
8403
+ const runtime = loadRuntimeConfig(repoPath, workspace ?? "", state.profile);
8404
+ const managed = runtime.config.managedRuntime;
8405
+ if (!managed || runtime.profile !== state.profile || runtime.workspace !== workspace) {
8406
+ throw new Error("Managed stop requires the recorded managed profile.");
8407
+ }
8408
+ const plan = inspectManagedDevcontainerConfig({
8409
+ repoPath,
8410
+ config: runtime.config,
8411
+ profile: runtime.resolvedProfile,
8412
+ linked
8413
+ });
8414
+ const selectedProcesses = runtime.resolvedProfile?.processes;
8415
+ const processes = !runtime.resolvedProfile || selectedProcesses?.length === 1 && selectedProcesses[0] === "*" ? managed.processes : selectedProcesses ?? [];
8416
+ if (state.sourceConfigSha256 !== plan.sourceConfigSha256 || state.effectiveConfigSha256 !== plan.effectiveConfigSha256 || !sameSet(
8417
+ state.desired.apps,
8418
+ proxyAppsFromConfig(runtime.config).map((app) => app.name)
8419
+ ) || !sameSet(state.desired.services, plan.desiredProfileServices) || !sameSet(state.desired.processes, processes) || inspectManagedDevcontainerGeneratedConfig(plan).status !== "valid") {
8420
+ throw new Error("Managed stop requires unchanged recorded resources and configuration.");
8421
+ }
8422
+ const containers = inspectManagedStopContainers(state.composeProject);
8423
+ const services = /* @__PURE__ */ new Set();
8424
+ for (const container of containers) {
8425
+ const service = container.labels["com.docker.compose.service"] ?? "";
8426
+ if (!plan.nativeRunServices.includes(service) || services.has(service) || container.labels["com.docker.compose.project"] !== state.composeProject || !sameWorkspacePath(
8427
+ container.labels["com.docker.compose.project.working_dir"] ?? "",
8428
+ plan.composeDirectory
8429
+ )) {
8430
+ throw new Error("Managed stop found unexpected or duplicate project membership.");
7782
8431
  }
7783
- if (failedStartMayHaveAttached(devsyId, options.repoPath)) {
7784
- throw new DevsyStartPostconditionError(message);
8432
+ services.add(service);
8433
+ const files = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").map((file) => file.trim());
8434
+ 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]))) {
8435
+ throw new Error("Managed stop Compose file identity changed.");
7785
8436
  }
7786
- throw new Error(message);
8437
+ for (const file of files.slice(plan.composeFiles.length)) {
8438
+ if (import_node_path21.default.dirname(file) !== featureDirectory || !/^docker-compose\.devcontainer\.containerFeatures-[a-zA-Z0-9_-]+\.yml$/.test(
8439
+ import_node_path21.default.basename(file)
8440
+ ) || import_node_fs21.default.realpathSync(file) !== import_node_path21.default.join(import_node_fs21.default.realpathSync(devsyRoot), import_node_path21.default.relative(devsyRoot, file))) {
8441
+ throw new Error("Managed stop refuses a foreign or escaped provider Compose file.");
8442
+ }
8443
+ }
8444
+ if (previous) {
8445
+ const retained = previous.find((entry) => entry.id === container.id);
8446
+ if (!retained || containerIdentity(retained) !== containerIdentity(container) || !retained.state.Running && container.state.Running) {
8447
+ throw new Error("Managed stop container identity or quiescent state changed.");
8448
+ }
8449
+ }
8450
+ }
8451
+ if (plan.desiredServices.some((service) => !services.has(service)) || previous && !sameSet(
8452
+ previous.map((c) => c.id),
8453
+ containers.map((c) => c.id)
8454
+ )) {
8455
+ throw new Error("Managed stop cannot prove the complete retained service population.");
8456
+ }
8457
+ const primary = containers.find(
8458
+ (c) => c.labels["com.docker.compose.service"] === plan.primaryService
8459
+ );
8460
+ if (primary?.mounts.filter(
8461
+ (mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath)
8462
+ ).length !== 1) {
8463
+ throw new Error("Managed stop cannot prove the exact primary workspace mount.");
8464
+ }
8465
+ assertManagedContainerConfigUnchanged({ plan, containers, workspace: workspaceEnv });
8466
+ if (registration().context !== context) {
8467
+ throw new Error("Managed stop provider context changed during inspection.");
7787
8468
  }
8469
+ const status = inspectDevsyRuntimeStatus(devsyId);
8470
+ if (status !== "running" && status !== "stopped" || status === "running" !== primary.state.Running) {
8471
+ throw new Error("Managed stop requires consistent provider and primary container state.");
8472
+ }
8473
+ return { containers, primary, status };
8474
+ }
8475
+ const initial = prove();
8476
+ let providerError;
8477
+ let providerFailed = false;
8478
+ if (initial.status === "running") {
7788
8479
  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.`);
8480
+ options.stopProvider();
8481
+ } catch (error) {
8482
+ providerFailed = true;
8483
+ providerError = error;
8484
+ }
8485
+ }
8486
+ try {
8487
+ const stopped = () => {
8488
+ const current = prove(initial.containers);
8489
+ if (current.status !== "stopped" || current.primary.state.Running) {
8490
+ throw new Error("Managed stop has not stopped the exact primary container.");
7793
8491
  }
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
- );
8492
+ return current.containers;
8493
+ };
8494
+ stopped();
8495
+ for (const retained of initial.containers) {
8496
+ if (!retained.state.Running || retained.id === initial.primary.id) continue;
8497
+ const current = stopped().find((entry) => entry.id === retained.id);
8498
+ if (current?.state.Running) {
8499
+ stopExactManagedService(current.id, current.labels["com.docker.compose.service"] ?? "", {
8500
+ timeoutMs: 3e4
8501
+ });
7800
8502
  }
7801
- return devsyId;
7802
- } catch (error) {
7803
- const message = error instanceof Error ? error.message : String(error);
7804
- throw new DevsyStartPostconditionError(message);
7805
8503
  }
7806
- });
8504
+ if (stopped().some((container) => container.state.Running)) {
8505
+ throw new Error("Managed stop left a retained service running.");
8506
+ }
8507
+ } catch (error) {
8508
+ if (providerFailed) {
8509
+ throw new AggregateError(
8510
+ [providerError, error],
8511
+ "Devsy provider stop failed; complete retained shutdown could not be verified.",
8512
+ { cause: providerError }
8513
+ );
8514
+ }
8515
+ throw error;
8516
+ }
8517
+ if (providerFailed) throw providerError;
8518
+ return true;
7807
8519
  }
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"() {
8520
+ var import_node_fs21, import_node_os2, import_node_path21;
8521
+ var init_managed_devsy_stop = __esm({
8522
+ "src/core/managed-devsy-stop.ts"() {
7811
8523
  "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();
8524
+ import_node_fs21 = __toESM(require("fs"));
8525
+ import_node_os2 = __toESM(require("os"));
8526
+ import_node_path21 = __toESM(require("path"));
8527
+ init_devcontainer_profile();
8528
+ init_devpod_environment();
7816
8529
  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;
8530
+ init_managed_runtime_state();
8531
+ init_repo_config();
8532
+ init_route_publication();
8533
+ init_workspace();
8534
+ init_workspace_ownership();
8535
+ init_workspace_runtime();
7825
8536
  }
7826
8537
  });
7827
8538
 
7828
- // src/core/devpod-mutation.ts
7829
- function withMutationLock2(activity, target, operation) {
7830
- import_node_fs21.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
8539
+ // src/core/devsy-mutation.ts
8540
+ function failedStartMayHaveAttached(devsyId, repoPath) {
8541
+ try {
8542
+ const attached = listDevsyWorkspaces();
8543
+ const attachedId = devsyId ?? selectDevsyWorkspace(attached, repoPath)?.id;
8544
+ if (!attachedId) return false;
8545
+ return inspectDevsyWorkspaceOwnership(attached, attachedId, repoPath).status !== "absent";
8546
+ } catch {
8547
+ return true;
8548
+ }
8549
+ }
8550
+ function withMutationLock(activity, target, operation) {
8551
+ import_node_fs22.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
7831
8552
  return withFileLockSync(
7832
- DEVPOD_MUTATION_LOCK_FILE,
8553
+ DEVSY_MUTATION_LOCK_FILE,
7833
8554
  {
7834
8555
  activity,
7835
8556
  target: `'${target}'`,
7836
- waitMs: DEVPOD_MUTATION_WAIT_MS,
8557
+ waitMs: DEVSY_MUTATION_WAIT_MS,
7837
8558
  fair: true,
7838
8559
  onWait: createStderrWaitReporter(activity, `'${target}'`)
7839
8560
  },
7840
8561
  operation
7841
8562
  );
7842
8563
  }
7843
- function commandFailure3(result) {
8564
+ function withMutationLockAsync(activity, target, operation) {
8565
+ import_node_fs22.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
8566
+ return withFileLock(
8567
+ DEVSY_MUTATION_LOCK_FILE,
8568
+ {
8569
+ activity,
8570
+ target: `'${target}'`,
8571
+ waitMs: DEVSY_MUTATION_WAIT_MS,
8572
+ fair: true,
8573
+ onWait: createStderrWaitReporter(activity, `'${target}'`)
8574
+ },
8575
+ operation
8576
+ );
8577
+ }
8578
+ function commandFailure2(result) {
7844
8579
  return [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
7845
8580
  }
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" });
8581
+ function runDevsyUp(args, env, quiet) {
8582
+ return new Promise((resolve) => {
8583
+ const child = (0, import_node_child_process16.spawn)("devsy", args, {
8584
+ stdio: ["inherit", quiet ? 2 : "inherit", "pipe"],
8585
+ env
8586
+ });
8587
+ const stderr = child.stderr;
8588
+ if (!stderr) throw new Error("Devsy startup stderr pipe was not created.");
8589
+ let stderrTail = Buffer.alloc(0);
8590
+ let spawnError;
8591
+ stderr.on("data", (chunk) => {
8592
+ const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
8593
+ const writable = process.stderr.write(value);
8594
+ if (!writable) {
8595
+ stderr.pause();
8596
+ process.stderr.once("drain", () => stderr.resume());
8597
+ }
8598
+ if (value.length >= DEVSY_STDERR_TAIL_BYTES) {
8599
+ stderrTail = Buffer.from(value.subarray(value.length - DEVSY_STDERR_TAIL_BYTES));
8600
+ } else {
8601
+ const combined = Buffer.concat([stderrTail, value]);
8602
+ stderrTail = combined.length > DEVSY_STDERR_TAIL_BYTES ? combined.subarray(combined.length - DEVSY_STDERR_TAIL_BYTES) : combined;
8603
+ }
8604
+ });
8605
+ child.once("error", (error) => {
8606
+ spawnError = error;
8607
+ });
8608
+ child.once("close", (status) => {
8609
+ resolve({ status, error: spawnError, stderrTail: stderrTail.toString("utf-8") });
8610
+ });
8611
+ });
8612
+ }
8613
+ function runDevsyAction(action2, devsyId, force = false) {
8614
+ const args = action2 === "delete" ? ["delete", devsyId, ...force ? ["--force"] : [], "--ignore-not-found"] : ["stop", devsyId];
8615
+ const result = (0, import_node_child_process16.spawnSync)("devsy", ["workspace", ...args], { encoding: "utf-8" });
7849
8616
  if (result.status !== 0) {
7850
8617
  throw new Error(
7851
- `devpod ${action2}${force ? " --force" : ""} failed for '${devpodId}': ${commandFailure3(result) || "unknown error"}`
8618
+ `devsy workspace ${action2}${force ? " --force" : ""} failed for '${devsyId}': ${commandFailure2(result) || "unknown error"}`
7852
8619
  );
7853
8620
  }
7854
8621
  }
7855
- function inspectExactOwnership2(devpodId, worktreePath) {
7856
- const ownership = inspectDevpodWorkspaceOwnership(
7857
- listDevpodWorkspaces(worktreePath),
7858
- devpodId,
7859
- worktreePath
7860
- );
8622
+ function inspectExactOwnership(devsyId, worktreePath) {
8623
+ const ownership = inspectDevsyWorkspaceOwnership(listDevsyWorkspaces(), devsyId, worktreePath);
7861
8624
  if (ownership.status === "conflict") throw new Error(ownership.reason);
7862
8625
  return ownership;
7863
8626
  }
7864
- function mutateOwnedDevpodWorkspace(action2, devpodId, worktreePath) {
7865
- return withMutationLock2(`DevPod ${action2}`, worktreePath, () => {
7866
- const before = inspectExactOwnership2(devpodId, worktreePath);
8627
+ function mutateOwnedDevsyWorkspace(action2, devsyId, worktreePath) {
8628
+ return withMutationLock(`Devsy ${action2}`, worktreePath, () => {
8629
+ if (action2 === "stop" && stopRetainedManagedDevsyWorkspace({
8630
+ repoPath: worktreePath,
8631
+ devsyId,
8632
+ stopProvider: () => runDevsyAction("stop", devsyId)
8633
+ }))
8634
+ return { status: "changed" };
8635
+ const before = inspectExactOwnership(devsyId, worktreePath);
7867
8636
  if (before.status === "absent") return { status: "absent" };
7868
- runDevpodAction(action2, devpodId);
7869
- let after = inspectExactOwnership2(devpodId, worktreePath);
8637
+ runDevsyAction(action2, devsyId);
8638
+ let after = inspectExactOwnership(devsyId, worktreePath);
7870
8639
  if (action2 === "stop" && after.status !== "owned") {
7871
- throw new Error(`DevPod '${devpodId}' no longer owns '${worktreePath}' after provider stop.`);
8640
+ throw new Error(
8641
+ `Devsy workspace '${devsyId}' no longer owns '${worktreePath}' after provider stop.`
8642
+ );
7872
8643
  }
7873
8644
  if (action2 === "delete" && after.status === "owned") {
7874
- const runtime = inspectDevpodRuntimeStatus(devpodId, worktreePath);
8645
+ const runtime = inspectDevsyRuntimeStatus(devsyId);
7875
8646
  if (runtime !== "not-found") {
7876
8647
  throw new Error(
7877
- `DevPod '${devpodId}' still owns '${worktreePath}' after provider delete (runtime=${runtime}).`
8648
+ `Devsy workspace '${devsyId}' still owns '${worktreePath}' after provider delete (runtime=${runtime}).`
7878
8649
  );
7879
8650
  }
7880
- after = inspectExactOwnership2(devpodId, worktreePath);
8651
+ after = inspectExactOwnership(devsyId, worktreePath);
7881
8652
  if (after.status === "owned") {
7882
- runDevpodAction("delete", devpodId, true);
7883
- after = inspectExactOwnership2(devpodId, worktreePath);
8653
+ runDevsyAction("delete", devsyId, true);
8654
+ after = inspectExactOwnership(devsyId, worktreePath);
7884
8655
  }
7885
8656
  if (after.status !== "absent") {
7886
8657
  throw new Error(
7887
- `DevPod '${devpodId}' still owns '${worktreePath}' after forced provider delete.`
8658
+ `Devsy workspace '${devsyId}' still owns '${worktreePath}' after forced provider delete.`
7888
8659
  );
7889
8660
  }
7890
8661
  }
7891
8662
  return { status: "changed" };
7892
8663
  });
7893
8664
  }
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;
8665
+ function stopOwnedDevsyWorkspace(devsyId, worktreePath) {
8666
+ return mutateOwnedDevsyWorkspace("stop", devsyId, worktreePath);
7903
8667
  }
7904
- function deleteOwnedDevpodWorkspace(devpodId, worktreePath) {
7905
- if (resolveWorkspaceRuntimeOrDefault(worktreePath) === "devsy") {
7906
- const result2 = deleteOwnedDevsyWorkspace(devpodId, worktreePath);
7907
- resetWorkspaceRuntimeCaches();
7908
- return result2;
8668
+ function deleteOwnedDevsyWorkspace(devsyId, worktreePath) {
8669
+ return mutateOwnedDevsyWorkspace("delete", devsyId, worktreePath);
8670
+ }
8671
+ function assertDevsyTarget(devsyId, repoPath) {
8672
+ const workspaces = listDevsyWorkspaces();
8673
+ const existing = selectDevsyWorkspace(workspaces, repoPath);
8674
+ const id = devsyId ?? existing?.id;
8675
+ if (id) {
8676
+ const before = inspectDevsyWorkspaceOwnership(workspaces, id, repoPath);
8677
+ if (before.status === "conflict") throw new Error(before.reason);
7909
8678
  }
7910
- const result = mutateOwnedDevpodWorkspace("delete", devpodId, worktreePath);
7911
- resetWorkspaceRuntimeCaches();
7912
- return result;
8679
+ return id;
7913
8680
  }
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
- }
8681
+ async function startDevsyWorkspace(options) {
8682
+ const activity = options.recreate ? "Devsy recreate" : "Devsy start";
8683
+ let agent;
8684
+ try {
8685
+ agent = requireReadyDevsyAgent();
8686
+ } catch (error) {
8687
+ if (!(error instanceof DevsyAgentReadinessError)) throw error;
8688
+ const repair = devsyAgentRepairSuggestion(error.inspection);
8689
+ throw new Error(`${error.message}. ${repair}`);
7935
8690
  }
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.`);
8691
+ return withMutationLockAsync(activity, options.repoPath, async () => {
8692
+ let devsyId = assertDevsyTarget(options.devsyId, options.repoPath);
8693
+ if (devsyId && options.recreate) {
8694
+ const attached = listDevsyWorkspaces();
8695
+ const ownership = inspectDevsyWorkspaceOwnership(attached, devsyId, options.repoPath);
8696
+ if (ownership.status !== "owned") {
8697
+ throw new Error(`Cannot recreate Devsy workspace '${devsyId}' without one exact owner.`);
7945
8698
  }
7946
- } else if (options.recreate) {
7947
- throw new Error("Cannot recreate a DevPod before its exact id is known.");
7948
8699
  }
7949
- const args = ["up", options.repoPath];
7950
- if (devpodId) args.push("--id", devpodId);
7951
- if (options.devcontainerPath) {
7952
- args.push("--devcontainer-path", options.devcontainerPath);
8700
+ if (!devsyId && options.recreate) {
8701
+ throw new Error("Cannot recreate a Devsy workspace before its exact id is known.");
7953
8702
  }
7954
- args.push("--open-ide=false");
8703
+ const args = ["workspace", "up", options.repoPath];
8704
+ if (devsyId) args.push("--id", devsyId);
8705
+ if (options.devcontainerPath) args.push("--devcontainer", options.devcontainerPath);
8706
+ args.push("--ide-launch", "skip");
7955
8707
  if (options.workspace) {
7956
8708
  args.push(
7957
8709
  "--workspace-env",
@@ -7960,8 +8712,12 @@ async function startDevpodWorkspace(options) {
7960
8712
  `DEVROUTER_WORKSPACE=${options.workspace.token}`
7961
8713
  );
7962
8714
  }
8715
+ if (options.inactivityTimeout) {
8716
+ args.push("--provider-option", `INACTIVITY_TIMEOUT=${options.inactivityTimeout}`);
8717
+ }
7963
8718
  if (options.recreate) args.push("--recreate");
7964
8719
  const env = { ...process.env };
8720
+ env.DEVSY_AGENT_BINARY = agent.binaryPath;
7965
8721
  if (options.workspace) {
7966
8722
  env.WORKSPACE = options.workspace.token;
7967
8723
  env.DEVROUTER_WORKSPACE = options.workspace.token;
@@ -7973,515 +8729,252 @@ async function startDevpodWorkspace(options) {
7973
8729
  delete env.DEVROUTER_GIT_COMMON_DIR;
7974
8730
  delete env.DEVCONTAINER_COMPOSE_OVERLAY;
7975
8731
  }
7976
- const result = (0, import_node_child_process16.spawnSync)("devpod", args, {
7977
- stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
7978
- env
7979
- });
8732
+ const result = await runDevsyUp(args, env, options.quiet ?? false);
7980
8733
  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.`);
8734
+ let message = `devsy workspace up failed for '${devsyId ?? options.repoPath}'.`;
8735
+ if (result.error?.message) message += ` ${result.error.message}`;
8736
+ if (AGENT_ACQUISITION_RE.test(result.stderrTail)) {
8737
+ message += ` Devsy rejected the verified agent source. Run: ${DEVSY_AGENT_SETUP_COMMAND}`;
7988
8738
  }
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
- );
8739
+ if (failedStartMayHaveAttached(devsyId, options.repoPath)) {
8740
+ throw new DevsyStartPostconditionError(message);
7995
8741
  }
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}'`);
8742
+ throw new Error(message);
8147
8743
  }
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;
8744
+ try {
8745
+ const attached = listDevsyWorkspaces();
8746
+ devsyId ??= selectDevsyWorkspace(attached, options.repoPath)?.id;
8747
+ if (!devsyId) {
8748
+ throw new Error(`Devsy did not attach '${options.repoPath}' after startup.`);
8749
+ }
8750
+ const ownership = inspectDevsyWorkspaceOwnership(attached, devsyId, options.repoPath);
8751
+ if (ownership.status === "conflict") throw new Error(ownership.reason);
8752
+ if (ownership.status !== "owned") {
8753
+ throw new Error(
8754
+ `Devsy did not attach '${options.repoPath}' as '${devsyId}' after startup.`
8755
+ );
8756
+ }
8757
+ return devsyId;
8758
+ } catch (error) {
8759
+ const message = error instanceof Error ? error.message : String(error);
8760
+ throw new DevsyStartPostconditionError(message);
8761
+ }
8762
+ });
8230
8763
  }
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;
8764
+ 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;
8765
+ var init_devsy_mutation = __esm({
8766
+ "src/core/devsy-mutation.ts"() {
8767
+ "use strict";
8768
+ import_node_child_process16 = require("child_process");
8769
+ import_node_fs22 = __toESM(require("fs"));
8770
+ import_node_path22 = __toESM(require("path"));
8771
+ init_devsy_agent();
8772
+ init_devsy_workspaces();
8773
+ init_file_lock();
8774
+ init_managed_devsy_stop();
8775
+ init_router();
8776
+ DEVSY_MUTATION_LOCK_FILE = import_node_path22.default.join(DEVROUTER_HOME, "devsy-mutation.lock");
8777
+ DEVSY_MUTATION_WAIT_MS = 18e5;
8778
+ DevsyStartPostconditionError = class extends Error {
8779
+ };
8780
+ AGENT_ACQUISITION_RE = /inject agent.*agent binary not found/i;
8781
+ DEVSY_STDERR_TAIL_BYTES = 8192;
8242
8782
  }
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 });
8783
+ });
8784
+
8785
+ // src/core/devpod-mutation.ts
8786
+ function withMutationLock2(activity, target, operation) {
8787
+ import_node_fs23.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
8250
8788
  return withFileLockSync(
8251
- import_node_path22.default.join(directory, ".lock"),
8789
+ DEVPOD_MUTATION_LOCK_FILE,
8252
8790
  {
8253
- activity: "workspace ownership transaction",
8254
- target: `'${repoPath}'`,
8255
- waitMs: options.waitMs ?? 5e3
8791
+ activity,
8792
+ target: `'${target}'`,
8793
+ waitMs: DEVPOD_MUTATION_WAIT_MS,
8794
+ fair: true,
8795
+ onWait: createStderrWaitReporter(activity, `'${target}'`)
8256
8796
  },
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
- })
8797
+ operation
8263
8798
  );
8264
8799
  }
8265
- function providerPathOwner(providerWorkspaces, worktreePath) {
8266
- const owners = providerWorkspaces.filter(
8267
- (workspace) => sameWorkspacePath(workspace.source.localFolder, worktreePath)
8268
- );
8269
- if (owners.length > 1) {
8800
+ function commandFailure3(result) {
8801
+ return [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
8802
+ }
8803
+ function runDevpodAction(action2, devpodId, force = false) {
8804
+ const args = action2 === "delete" ? [action2, devpodId, ...force ? ["--force"] : [], "--ignore-not-found"] : [action2, devpodId];
8805
+ const result = (0, import_node_child_process17.spawnSync)("devpod", args, { encoding: "utf-8" });
8806
+ if (result.status !== 0) {
8270
8807
  throw new Error(
8271
- `Worktree '${worktreePath}' is registered by multiple workspace runtimes; no identity was claimed.`
8808
+ `devpod ${action2}${force ? " --force" : ""} failed for '${devpodId}': ${commandFailure3(result) || "unknown error"}`
8272
8809
  );
8273
8810
  }
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
8811
  }
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)
8812
+ function inspectExactOwnership2(devpodId, worktreePath) {
8813
+ const ownership = inspectDevpodWorkspaceOwnership(
8814
+ listDevpodWorkspaces(worktreePath),
8815
+ devpodId,
8816
+ worktreePath
8305
8817
  );
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;
8818
+ if (ownership.status === "conflict") throw new Error(ownership.reason);
8819
+ return ownership;
8314
8820
  }
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
- );
8821
+ function mutateOwnedDevpodWorkspace(action2, devpodId, worktreePath) {
8822
+ return withMutationLock2(`DevPod ${action2}`, worktreePath, () => {
8823
+ const before = inspectExactOwnership2(devpodId, worktreePath);
8824
+ if (before.status === "absent") return { status: "absent" };
8825
+ runDevpodAction(action2, devpodId);
8826
+ let after = inspectExactOwnership2(devpodId, worktreePath);
8827
+ if (action2 === "stop" && after.status !== "owned") {
8828
+ throw new Error(`DevPod '${devpodId}' no longer owns '${worktreePath}' after provider stop.`);
8327
8829
  }
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) {
8830
+ if (action2 === "delete" && after.status === "owned") {
8831
+ const runtime = inspectDevpodRuntimeStatus(devpodId, worktreePath);
8832
+ if (runtime !== "not-found") {
8333
8833
  throw new Error(
8334
- `Persisted workspace identity '${persisted}' disagrees with owner record '${exactRecord.workspace}'.`
8834
+ `DevPod '${devpodId}' still owns '${worktreePath}' after provider delete (runtime=${runtime}).`
8335
8835
  );
8336
8836
  }
8337
- if (exactProvider && exactProvider.id !== exactRecord.devpodId) {
8338
- throw new Error(
8339
- `Workspace runtime '${exactProvider.id}' disagrees with owner record '${exactRecord.devpodId}'.`
8340
- );
8837
+ after = inspectExactOwnership2(devpodId, worktreePath);
8838
+ if (after.status === "owned") {
8839
+ runDevpodAction("delete", devpodId, true);
8840
+ after = inspectExactOwnership2(devpodId, worktreePath);
8341
8841
  }
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) {
8842
+ if (after.status !== "absent") {
8352
8843
  throw new Error(
8353
- `Workspace '${exactRecord.workspace}' conflicts with ${conflict2}; no identity was claimed.`
8844
+ `DevPod '${devpodId}' still owns '${worktreePath}' after forced provider delete.`
8354
8845
  );
8355
8846
  }
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
8847
  }
8364
- if (persisted && exactProvider && persisted !== exactProvider.id) {
8365
- throw new Error(
8366
- `Persisted workspace identity '${persisted}' disagrees with workspace runtime '${exactProvider.id}'.`
8367
- );
8848
+ return { status: "changed" };
8849
+ });
8850
+ }
8851
+ function stopOwnedDevpodWorkspace(devpodId, worktreePath) {
8852
+ try {
8853
+ resetWorkspaceRuntimeCaches();
8854
+ if (resolveWorkspaceRuntimeOrDefault(worktreePath) === "devsy") {
8855
+ return stopOwnedDevsyWorkspace(devpodId, worktreePath);
8368
8856
  }
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
- );
8857
+ return mutateOwnedDevpodWorkspace("stop", devpodId, worktreePath);
8858
+ } finally {
8859
+ resetWorkspaceRuntimeCaches();
8860
+ }
8861
+ }
8862
+ function deleteOwnedDevpodWorkspace(devpodId, worktreePath) {
8863
+ if (resolveWorkspaceRuntimeOrDefault(worktreePath) === "devsy") {
8864
+ const result2 = deleteOwnedDevsyWorkspace(devpodId, worktreePath);
8865
+ resetWorkspaceRuntimeCaches();
8866
+ return result2;
8867
+ }
8868
+ const result = mutateOwnedDevpodWorkspace("delete", devpodId, worktreePath);
8869
+ resetWorkspaceRuntimeCaches();
8870
+ return result;
8871
+ }
8872
+ async function startDevpodWorkspace(options) {
8873
+ if (resolveWorkspaceRuntimeOrDefault(options.repoPath) === "devsy") {
8874
+ try {
8875
+ const result = await startDevsyWorkspace({
8876
+ repoPath: options.repoPath,
8877
+ devsyId: options.devpodId,
8878
+ devcontainerPath: options.devcontainerPath,
8879
+ recreate: options.recreate,
8880
+ quiet: options.quiet,
8881
+ workspace: options.workspace,
8882
+ inactivityTimeout: readWorkspaceRuntimeConfig().devsyInactivityTimeout
8883
+ });
8884
+ resetWorkspaceRuntimeCaches();
8885
+ return result;
8886
+ } catch (error) {
8887
+ if (error instanceof DevsyStartPostconditionError) {
8888
+ resetWorkspaceRuntimeCaches();
8889
+ throw new DevpodStartPostconditionError(error.message);
8376
8890
  }
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
- );
8891
+ throw error;
8892
+ }
8893
+ }
8894
+ const activity = options.recreate ? "DevPod recreate" : "DevPod start";
8895
+ return withMutationLock2(activity, options.repoPath, () => {
8896
+ const workspaces = listDevpodWorkspaces(options.repoPath);
8897
+ let devpodId = options.devpodId ?? selectDevpodWorkspace(workspaces, options.repoPath)?.id;
8898
+ if (devpodId) {
8899
+ const before = inspectDevpodWorkspaceOwnership(workspaces, devpodId, options.repoPath);
8900
+ if (before.status === "conflict") throw new Error(before.reason);
8901
+ if (options.recreate && before.status !== "owned") {
8902
+ throw new Error(`Cannot recreate DevPod '${devpodId}' without one exact owner.`);
8391
8903
  }
8392
- workspace = candidate;
8393
- devpodId = candidate;
8904
+ } else if (options.recreate) {
8905
+ throw new Error("Cannot recreate a DevPod before its exact id is known.");
8394
8906
  }
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.`
8907
+ const args = ["up", options.repoPath];
8908
+ if (devpodId) args.push("--id", devpodId);
8909
+ if (options.devcontainerPath) {
8910
+ args.push("--devcontainer-path", options.devcontainerPath);
8911
+ }
8912
+ args.push("--open-ide=false");
8913
+ if (options.workspace) {
8914
+ args.push(
8915
+ "--workspace-env",
8916
+ `WORKSPACE=${options.workspace.token}`,
8917
+ "--workspace-env",
8918
+ `DEVROUTER_WORKSPACE=${options.workspace.token}`
8406
8919
  );
8407
8920
  }
8408
- const written = transaction.write({
8409
- workspace,
8410
- worktreePath,
8411
- branch: input2.branch ?? null,
8412
- devpodId
8921
+ if (options.recreate) args.push("--recreate");
8922
+ const env = { ...process.env };
8923
+ if (options.workspace) {
8924
+ env.WORKSPACE = options.workspace.token;
8925
+ env.DEVROUTER_WORKSPACE = options.workspace.token;
8926
+ env.DEVROUTER_GIT_COMMON_DIR = options.workspace.gitCommonDir;
8927
+ env.DEVCONTAINER_COMPOSE_OVERLAY = "docker-compose.devrouter.yml";
8928
+ } else {
8929
+ delete env.WORKSPACE;
8930
+ delete env.DEVROUTER_WORKSPACE;
8931
+ delete env.DEVROUTER_GIT_COMMON_DIR;
8932
+ delete env.DEVCONTAINER_COMPOSE_OVERLAY;
8933
+ }
8934
+ const result = (0, import_node_child_process17.spawnSync)("devpod", args, {
8935
+ stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
8936
+ env
8413
8937
  });
8938
+ if (result.status !== 0) {
8939
+ throw new Error(`devpod up failed for '${devpodId ?? options.repoPath}'.`);
8940
+ }
8414
8941
  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);
8942
+ const attached = listDevpodWorkspaces(options.repoPath);
8943
+ devpodId ??= selectDevpodWorkspace(attached, options.repoPath)?.id;
8944
+ if (!devpodId) {
8945
+ throw new Error(`DevPod did not attach '${options.repoPath}' after startup.`);
8946
+ }
8947
+ const ownership = inspectDevpodWorkspaceOwnership(attached, devpodId, options.repoPath);
8948
+ if (ownership.status === "conflict") throw new Error(ownership.reason);
8949
+ if (ownership.status !== "owned") {
8420
8950
  throw new Error(
8421
- `Could not persist workspace identity and owner-record rollback was '${cleanup}': ${detail}`
8951
+ `DevPod did not attach '${options.repoPath}' as '${devpodId}' after startup.`
8422
8952
  );
8423
8953
  }
8424
- throw error;
8954
+ resetWorkspaceRuntimeCaches();
8955
+ return devpodId;
8956
+ } catch (error) {
8957
+ const message = error instanceof Error ? error.message : String(error);
8958
+ throw new DevpodStartPostconditionError(message);
8425
8959
  }
8426
- return written;
8427
8960
  });
8428
8961
  }
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"() {
8962
+ var import_node_child_process17, import_node_fs23, import_node_path23, DEVPOD_MUTATION_LOCK_FILE, DEVPOD_MUTATION_WAIT_MS, DevpodStartPostconditionError;
8963
+ var init_devpod_mutation = __esm({
8964
+ "src/core/devpod-mutation.ts"() {
8474
8965
  "use strict";
8475
8966
  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();
8967
+ import_node_fs23 = __toESM(require("fs"));
8968
+ import_node_path23 = __toESM(require("path"));
8479
8969
  init_devpod_workspaces();
8970
+ init_devsy_mutation();
8480
8971
  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");
8972
+ init_router();
8973
+ init_workspace_runtime();
8974
+ DEVPOD_MUTATION_LOCK_FILE = import_node_path23.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
8975
+ DEVPOD_MUTATION_WAIT_MS = 18e5;
8976
+ DevpodStartPostconditionError = class extends Error {
8977
+ };
8485
8978
  }
8486
8979
  });
8487
8980
 
@@ -8495,10 +8988,10 @@ function inRepositoryWorkspaceScope(repoPath, worktreePath, livePaths) {
8495
8988
  if (livePaths.some((candidate) => sameWorkspacePath(candidate, worktreePath))) return true;
8496
8989
  const comparableRepo = comparableWorkspacePath(repoPath);
8497
8990
  const comparableWorktree = comparableWorkspacePath(worktreePath);
8498
- const localRoot = import_node_path23.default.join(comparableRepo, "trees") + import_node_path23.default.sep;
8991
+ const localRoot = import_node_path24.default.join(comparableRepo, "trees") + import_node_path24.default.sep;
8499
8992
  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);
8993
+ const legacyPrefix = `${import_node_path24.default.basename(comparableRepo)}-`;
8994
+ return import_node_path24.default.dirname(comparableWorktree) === import_node_path24.default.dirname(comparableRepo) && import_node_path24.default.basename(comparableWorktree).startsWith(legacyPrefix);
8502
8995
  }
8503
8996
  function previewActions(devpodStatus, routeCount, includeRecord) {
8504
8997
  const actions = [
@@ -8750,11 +9243,11 @@ function applyWorkspaceGc(plan) {
8750
9243
  candidates
8751
9244
  };
8752
9245
  }
8753
- var import_node_path23;
9246
+ var import_node_path24;
8754
9247
  var init_workspace_gc = __esm({
8755
9248
  "src/core/workspace-gc.ts"() {
8756
9249
  "use strict";
8757
- import_node_path23 = __toESM(require("path"));
9250
+ import_node_path24 = __toESM(require("path"));
8758
9251
  init_devpod_mutation();
8759
9252
  init_devpod_workspaces();
8760
9253
  init_host_routes();
@@ -8824,11 +9317,11 @@ function inspectPostgresCredentials(repoPath, config) {
8824
9317
  parseErrors.push(`${app.name}: ${composeFile} (${message})`);
8825
9318
  continue;
8826
9319
  }
8827
- if (!import_node_fs23.default.existsSync(absolutePath)) {
9320
+ if (!import_node_fs24.default.existsSync(absolutePath)) {
8828
9321
  continue;
8829
9322
  }
8830
9323
  try {
8831
- const raw = import_node_fs23.default.readFileSync(absolutePath, "utf-8");
9324
+ const raw = import_node_fs24.default.readFileSync(absolutePath, "utf-8");
8832
9325
  const parsed = import_yaml5.default.parse(raw);
8833
9326
  const root = asRecord2(parsed);
8834
9327
  const services = asRecord2(root?.services);
@@ -9107,7 +9600,7 @@ async function buildDoctorReport(options = {}) {
9107
9600
  const config = runtimeConfig.config;
9108
9601
  loadedConfig = config;
9109
9602
  loadedWorkspace = runtimeConfig.workspace;
9110
- const cliVersion = true ? "0.0.52" : "0.0.0-dev";
9603
+ const cliVersion = true ? "0.0.54" : "0.0.0-dev";
9111
9604
  const configVersion = config.devrouter?.version;
9112
9605
  if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
9113
9606
  addCheck(checks, {
@@ -9137,9 +9630,9 @@ async function buildDoctorReport(options = {}) {
9137
9630
  (app) => app.docker.composeFiles.map((filePath) => ({
9138
9631
  app: app.name,
9139
9632
  filePath,
9140
- absolutePath: import_node_path24.default.resolve(repo.path, filePath)
9633
+ absolutePath: import_node_path25.default.resolve(repo.path, filePath)
9141
9634
  }))
9142
- ).filter((entry) => !import_node_fs23.default.existsSync(entry.absolutePath));
9635
+ ).filter((entry) => !import_node_fs24.default.existsSync(entry.absolutePath));
9143
9636
  addCheck(checks, {
9144
9637
  id: "repo.compose-files",
9145
9638
  level: missingComposeFiles.length === 0 ? "ok" : "error",
@@ -9182,8 +9675,8 @@ async function buildDoctorReport(options = {}) {
9182
9675
  const missingHostCwds = config.apps.filter((app) => app.runtime === "host").map((app) => ({
9183
9676
  app: app.name,
9184
9677
  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));
9678
+ absolutePath: import_node_path25.default.resolve(repo.path, app.hostRun.cwd)
9679
+ })).filter((entry) => !import_node_fs24.default.existsSync(entry.absolutePath));
9187
9680
  addCheck(checks, {
9188
9681
  id: "repo.host-cwd",
9189
9682
  level: missingHostCwds.length === 0 ? "ok" : "error",
@@ -9327,12 +9820,12 @@ async function buildDoctorReport(options = {}) {
9327
9820
  nextSteps
9328
9821
  };
9329
9822
  }
9330
- var import_node_fs23, import_node_path24, import_yaml5, POSTGRES_DEFAULTS;
9823
+ var import_node_fs24, import_node_path25, import_yaml5, POSTGRES_DEFAULTS;
9331
9824
  var init_doctor = __esm({
9332
9825
  "src/core/doctor.ts"() {
9333
9826
  "use strict";
9334
- import_node_fs23 = __toESM(require("fs"));
9335
- import_node_path24 = __toESM(require("path"));
9827
+ import_node_fs24 = __toESM(require("fs"));
9828
+ import_node_path25 = __toESM(require("path"));
9336
9829
  import_yaml5 = __toESM(require("yaml"));
9337
9830
  init_docker();
9338
9831
  init_host_routes();
@@ -9779,76 +10272,6 @@ var init_http_route_probe = __esm({
9779
10272
  }
9780
10273
  });
9781
10274
 
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
10275
  // src/core/traefik-route-health.ts
9853
10276
  function sleep(ms) {
9854
10277
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -9893,7 +10316,7 @@ function inspectRouterApi(protocol) {
9893
10316
  )
9894
10317
  };
9895
10318
  }
9896
- function isRecord2(value) {
10319
+ function isRecord3(value) {
9897
10320
  return value !== null && typeof value === "object" && !Array.isArray(value);
9898
10321
  }
9899
10322
  function inspectNamedRouterApi(protocol, resource, name) {
@@ -9924,7 +10347,7 @@ function inspectNamedRouterApi(protocol, resource, name) {
9924
10347
  details: `${protocol.toUpperCase()} ${resource.slice(0, -1)} API returned invalid JSON`
9925
10348
  };
9926
10349
  }
9927
- if (!isRecord2(value)) {
10350
+ if (!isRecord3(value)) {
9928
10351
  return {
9929
10352
  ok: false,
9930
10353
  details: `${protocol.toUpperCase()} ${resource.slice(0, -1)} API returned a non-object`
@@ -9953,19 +10376,19 @@ function renderExpectedRoute(route) {
9953
10376
  };
9954
10377
  const document = buildHostRoutesDocument([routeState], false);
9955
10378
  const protocolDocument = document[protocol];
9956
- if (!isRecord2(protocolDocument)) return void 0;
10379
+ if (!isRecord3(protocolDocument)) return void 0;
9957
10380
  const routers = protocolDocument.routers;
9958
10381
  const services = protocolDocument.services;
9959
- if (!isRecord2(routers) || !isRecord2(services)) return void 0;
10382
+ if (!isRecord3(routers) || !isRecord3(services)) return void 0;
9960
10383
  const routerKey = routerName.slice(0, routerName.lastIndexOf("@"));
9961
10384
  const router = routers[routerKey];
9962
10385
  const service = services[routerKey];
9963
- if (!isRecord2(router) || !isRecord2(service)) return void 0;
10386
+ if (!isRecord3(router) || !isRecord3(service)) return void 0;
9964
10387
  const rule = router.rule;
9965
10388
  const serviceReference = router.service;
9966
10389
  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])) {
10390
+ const servers = isRecord3(loadBalancer) ? loadBalancer.servers : void 0;
10391
+ if (typeof rule !== "string" || serviceReference !== routerKey || !isRecord3(loadBalancer) || !Array.isArray(servers) || servers.length !== 1 || !isRecord3(servers[0])) {
9969
10392
  return void 0;
9970
10393
  }
9971
10394
  const serverField = protocol === "http" ? "url" : "address";
@@ -10009,9 +10432,9 @@ function inspectExpectedRouteMatch(route) {
10009
10432
  return { ok: false, routes: [routeName], details: serviceResult.details };
10010
10433
  }
10011
10434
  const loadBalancer = serviceResult.value.loadBalancer;
10012
- const servers = isRecord2(loadBalancer) ? loadBalancer.servers : void 0;
10435
+ const servers = isRecord3(loadBalancer) ? loadBalancer.servers : void 0;
10013
10436
  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) {
10437
+ 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
10438
  return {
10016
10439
  ok: false,
10017
10440
  routes: [routeName],
@@ -10108,7 +10531,7 @@ async function ensureTraefikRouteExpectation(routes, expectation, options) {
10108
10531
  throw new Error(
10109
10532
  "Traefik routes did not reload; shared router restart is disabled for this operation."
10110
10533
  );
10111
- import_node_fs24.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
10534
+ import_node_fs25.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
10112
10535
  return withFileLock(
10113
10536
  ROUTER_RELOAD_LOCK_FILE,
10114
10537
  {
@@ -10152,18 +10575,18 @@ async function ensureTraefikRoutesMatch(routes, options = {}) {
10152
10575
  async function ensureTraefikRoutesRemoved(routes, options = {}) {
10153
10576
  return ensureTraefikRouteExpectation(routes, "removed", options);
10154
10577
  }
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;
10578
+ 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
10579
  var init_traefik_route_health = __esm({
10157
10580
  "src/core/traefik-route-health.ts"() {
10158
10581
  "use strict";
10159
10582
  import_node_child_process20 = require("child_process");
10160
- import_node_fs24 = __toESM(require("fs"));
10161
- import_node_path25 = __toESM(require("path"));
10583
+ import_node_fs25 = __toESM(require("fs"));
10584
+ import_node_path26 = __toESM(require("path"));
10162
10585
  init_file_lock();
10163
10586
  init_host_routes();
10164
10587
  init_router();
10165
10588
  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");
10589
+ ROUTER_RELOAD_LOCK_FILE = import_node_path26.default.join(DEVROUTER_HOME, "router-reload.lock");
10167
10590
  ROUTER_API_REQUEST_TIMEOUT_SECONDS = "2";
10168
10591
  DEFAULT_INITIAL_TIMEOUT_MS = 3e3;
10169
10592
  DEFAULT_RECOVERY_TIMEOUT_MS = 1e4;
@@ -10175,11 +10598,11 @@ var init_traefik_route_health = __esm({
10175
10598
  // src/core/workspace-ensure.ts
10176
10599
  function assertOverlay(container, repoPath) {
10177
10600
  const workingDir = container.labels["com.docker.compose.project.working_dir"];
10178
- if (!workingDir || !sameWorkspacePath(workingDir, import_node_path26.default.join(repoPath, ".devcontainer"))) {
10601
+ if (!workingDir || !sameWorkspacePath(workingDir, import_node_path27.default.join(repoPath, ".devcontainer"))) {
10179
10602
  throw new Error(`Container '${container.id}' does not belong to the exact worktree.`);
10180
10603
  }
10181
10604
  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);
10605
+ const expectedOverlay = import_node_path27.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
10183
10606
  if (!configFiles.some((configFile) => sameWorkspacePath(configFile, expectedOverlay))) {
10184
10607
  throw new Error(`Container '${container.id}' was not started with ${DEVCONTAINER_OVERLAY}.`);
10185
10608
  }
@@ -10369,7 +10792,7 @@ function hasExactManagedComposeProject(repoPath, state) {
10369
10792
  }).some((container) => {
10370
10793
  const workingDir = container.labels["com.docker.compose.project.working_dir"];
10371
10794
  return Boolean(
10372
- workingDir && sameWorkspacePath(workingDir, import_node_path26.default.join(repoPath, ".devcontainer"))
10795
+ workingDir && sameWorkspacePath(workingDir, import_node_path27.default.join(repoPath, ".devcontainer"))
10373
10796
  );
10374
10797
  });
10375
10798
  } catch {
@@ -10726,7 +11149,7 @@ function resolvePrimaryTarget(repoPath) {
10726
11149
  }
10727
11150
  function isPrimaryCheckout(repoPath) {
10728
11151
  try {
10729
- return import_node_fs25.default.statSync(import_node_path26.default.join(repoPath, ".git")).isDirectory();
11152
+ return import_node_fs26.default.statSync(import_node_path27.default.join(repoPath, ".git")).isDirectory();
10730
11153
  } catch {
10731
11154
  return false;
10732
11155
  }
@@ -10784,8 +11207,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
10784
11207
  const target = options.repair ? resolveRepairTarget(repoPath, linked) : linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
10785
11208
  let devpodId = target.devpodId;
10786
11209
  if (target.kind === "linked") {
10787
- const overlayPath = import_node_path26.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
10788
- if (!import_node_fs25.default.existsSync(overlayPath)) {
11210
+ const overlayPath = import_node_path27.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
11211
+ if (!import_node_fs26.default.existsSync(overlayPath)) {
10789
11212
  throw new Error(`Missing required DevPod compose overlay: ${overlayPath}`);
10790
11213
  }
10791
11214
  }
@@ -10859,7 +11282,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
10859
11282
  }
10860
11283
  const apps = proxyAppsFromConfig(runtime.config);
10861
11284
  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";
11285
+ const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path27.default.basename(repoPath)) ?? "app";
10863
11286
  for (const [index, app] of apps.entries()) {
10864
11287
  if (!parsedUpstreams[index].host.startsWith(`${aliasPrefix}-`)) {
10865
11288
  const owner = target.kind === "linked" ? "workspace" : "checkout";
@@ -11427,9 +11850,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
11427
11850
  return withWorkspaceLifecycleLock(repoPath, async () => {
11428
11851
  if (!options.repair) return ensureLocked();
11429
11852
  const runtime = resolveWorkspaceRuntimeOrDefault(repoPath);
11430
- import_node_fs25.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
11853
+ import_node_fs26.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
11431
11854
  return withFileLock(
11432
- import_node_path26.default.join(DEVROUTER_HOME, `${runtime}-mutation.lock`),
11855
+ import_node_path27.default.join(DEVROUTER_HOME, `${runtime}-mutation.lock`),
11433
11856
  {
11434
11857
  activity: "Managed runtime repair",
11435
11858
  target: repoPath,
@@ -11441,13 +11864,13 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
11441
11864
  );
11442
11865
  });
11443
11866
  }
11444
- var import_node_child_process21, import_node_fs25, import_node_path26, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
11867
+ var import_node_child_process21, import_node_fs26, import_node_path27, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
11445
11868
  var init_workspace_ensure = __esm({
11446
11869
  "src/core/workspace-ensure.ts"() {
11447
11870
  "use strict";
11448
11871
  import_node_child_process21 = require("child_process");
11449
- import_node_fs25 = __toESM(require("fs"));
11450
- import_node_path26 = __toESM(require("path"));
11872
+ import_node_fs26 = __toESM(require("fs"));
11873
+ import_node_path27 = __toESM(require("path"));
11451
11874
  init_devcontainer_profile();
11452
11875
  init_devpod_environment();
11453
11876
  init_devpod_mutation();
@@ -11536,7 +11959,7 @@ function warnMissingWorkspaceOwnership(repoPath) {
11536
11959
  );
11537
11960
  }
11538
11961
  function defaultWorktreePath(mainRepo, ws) {
11539
- return import_node_path27.default.join(mainRepo, "trees", ws);
11962
+ return import_node_path28.default.join(mainRepo, "trees", ws);
11540
11963
  }
11541
11964
  function assertDefaultWorktreeRootIgnored(mainRepo) {
11542
11965
  const ignored = (0, import_node_child_process22.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
@@ -11544,12 +11967,12 @@ function assertDefaultWorktreeRootIgnored(mainRepo) {
11544
11967
  });
11545
11968
  if (ignored.status !== 0) {
11546
11969
  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.`
11970
+ `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
11971
  );
11549
11972
  }
11550
11973
  }
11551
11974
  function legacyDefaultWorktreePath(mainRepo, ws) {
11552
- return import_node_path27.default.join(import_node_path27.default.dirname(mainRepo), `${import_node_path27.default.basename(mainRepo)}-${ws}`);
11975
+ return import_node_path28.default.join(import_node_path28.default.dirname(mainRepo), `${import_node_path28.default.basename(mainRepo)}-${ws}`);
11553
11976
  }
11554
11977
  function teardownFallbackPath(mainRepo, workspace) {
11555
11978
  const candidates = [
@@ -11661,7 +12084,7 @@ function assertFullDownPreflight(mainRepo, target) {
11661
12084
  if (sameWorkspacePath(target.worktreePath, mainRepo)) {
11662
12085
  throw new Error("Refusing to remove the primary Git checkout.");
11663
12086
  }
11664
- if (!target.worktree || target.worktree.prunable || !import_node_fs26.default.existsSync(target.worktreePath)) return;
12087
+ if (!target.worktree || target.worktree.prunable || !import_node_fs27.default.existsSync(target.worktreePath)) return;
11665
12088
  if (target.worktree.locked) {
11666
12089
  throw new Error(
11667
12090
  `Worktree '${target.worktreePath}' is locked; unlock it before workspace down.`
@@ -11701,9 +12124,9 @@ async function workspaceUp(branch, opts = {}) {
11701
12124
  (candidate) => defaultWorktreePath(mainRepo, candidate)
11702
12125
  );
11703
12126
  const generatedPath = candidatePaths.find(
11704
- (candidate) => !import_node_fs26.default.existsSync(candidate) && !worktrees.some((worktree) => sameWorkspacePath(worktree.path, candidate))
12127
+ (candidate) => !import_node_fs27.default.existsSync(candidate) && !worktrees.some((worktree) => sameWorkspacePath(worktree.path, candidate))
11705
12128
  );
11706
- const selectedPath = opts.path ? import_node_path27.default.resolve(opts.path) : existingBranch?.path ?? generatedPath;
12129
+ const selectedPath = opts.path ? import_node_path28.default.resolve(opts.path) : existingBranch?.path ?? generatedPath;
11707
12130
  if (!selectedPath) {
11708
12131
  throw new Error(`Could not allocate a collision-safe worktree path for branch '${branch}'.`);
11709
12132
  }
@@ -11712,7 +12135,7 @@ async function workspaceUp(branch, opts = {}) {
11712
12135
  `Branch '${branch}' already uses worktree '${existingBranch.path}', not '${selectedPath}'.`
11713
12136
  );
11714
12137
  }
11715
- if (import_node_fs26.default.existsSync(selectedPath)) {
12138
+ if (import_node_fs27.default.existsSync(selectedPath)) {
11716
12139
  const registered = worktrees.find(
11717
12140
  (worktree) => sameWorkspacePath(worktree.path, selectedPath)
11718
12141
  );
@@ -11806,6 +12229,11 @@ async function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = fals
11806
12229
  assertDevpodTargetSafe(resolved, worktrees, devpods);
11807
12230
  const devpodId = resolved.record?.devpodId ?? resolved.workspace;
11808
12231
  const mutation = action2 === "stop" ? stopOwnedDevpodWorkspace(devpodId, resolved.worktreePath) : deleteOwnedDevpodWorkspace(devpodId, resolved.worktreePath);
12232
+ if (action2 === "stop" && mutation.status === "absent" && readManagedRuntimeState(resolved.worktreePath, resolved.workspace)) {
12233
+ throw new Error(
12234
+ "Retained managed runtime has no exact provider registration; routes were preserved."
12235
+ );
12236
+ }
11809
12237
  const routes = removeWorkspaceRoutesForWorktree(resolved.workspace, resolved.worktreePath);
11810
12238
  await ensureTraefikRoutesRemoved(routes);
11811
12239
  if (!quiet) {
@@ -11838,7 +12266,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
11838
12266
  opts.quiet
11839
12267
  );
11840
12268
  if (removeWorktree) {
11841
- if (resolved.worktree && !resolved.worktree.prunable && import_node_fs26.default.existsSync(resolved.worktreePath)) {
12269
+ if (resolved.worktree && !resolved.worktree.prunable && import_node_fs27.default.existsSync(resolved.worktreePath)) {
11842
12270
  const rm = (0, import_node_child_process22.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
11843
12271
  encoding: "utf-8"
11844
12272
  });
@@ -11857,7 +12285,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
11857
12285
  }
11858
12286
  return result;
11859
12287
  };
11860
- return resolved.worktree && !resolved.worktree.prunable && import_node_fs26.default.existsSync(resolved.worktreePath) ? withWorkspaceLifecycleLock(resolved.worktreePath, operation) : operation();
12288
+ return resolved.worktree && !resolved.worktree.prunable && import_node_fs27.default.existsSync(resolved.worktreePath) ? withWorkspaceLifecycleLock(resolved.worktreePath, operation) : operation();
11861
12289
  }
11862
12290
  async function mutateWorkspaceOwnedPath(action2, worktreePath, opts = {}) {
11863
12291
  const mainRepo = resolveRepoPath(opts.repoPath);
@@ -11892,15 +12320,16 @@ async function workspaceStop(target, opts = {}) {
11892
12320
  async function workspaceDown(target, opts = {}) {
11893
12321
  return runWorkspaceLifecycle("down", target, opts);
11894
12322
  }
11895
- var import_node_child_process22, import_node_fs26, import_node_path27, WORKSPACE_ALLOCATION_LOCK_WAIT_MS;
12323
+ var import_node_child_process22, import_node_fs27, import_node_path28, WORKSPACE_ALLOCATION_LOCK_WAIT_MS;
11896
12324
  var init_workspace_lifecycle = __esm({
11897
12325
  "src/core/workspace-lifecycle.ts"() {
11898
12326
  "use strict";
11899
12327
  import_node_child_process22 = require("child_process");
11900
- import_node_fs26 = __toESM(require("fs"));
11901
- import_node_path27 = __toESM(require("path"));
12328
+ import_node_fs27 = __toESM(require("fs"));
12329
+ import_node_path28 = __toESM(require("path"));
11902
12330
  init_devpod_mutation();
11903
12331
  init_devpod_workspaces();
12332
+ init_managed_runtime_state();
11904
12333
  init_repo_config();
11905
12334
  init_route_state();
11906
12335
  init_traefik_route_health();
@@ -11952,6 +12381,11 @@ async function environmentStop(repoPath, options = {}) {
11952
12381
  return withWorkspaceLifecycleLock(repoPath, async () => {
11953
12382
  const devpod = selectDevpodWorkspace(listDevpodWorkspaces(repoPath), repoPath);
11954
12383
  const mutation = devpod ? options.delete ? deleteOwnedDevpodWorkspace(devpod.id, repoPath) : stopOwnedDevpodWorkspace(devpod.id, repoPath) : { status: "absent" };
12384
+ if (!options.delete && mutation.status === "absent" && readManagedRuntimeState(repoPath, workspace)) {
12385
+ throw new Error(
12386
+ "Retained managed runtime has no exact provider registration; routes were preserved."
12387
+ );
12388
+ }
11955
12389
  const removedRoutes = removeHostRoutesWhere(
11956
12390
  (route) => sameWorkspacePath(route.repoPath, repoPath)
11957
12391
  );
@@ -11973,6 +12407,7 @@ var init_environment_stop = __esm({
11973
12407
  init_devpod_mutation();
11974
12408
  init_devpod_workspaces();
11975
12409
  init_host_routes();
12410
+ init_managed_runtime_state();
11976
12411
  init_traefik_route_health();
11977
12412
  init_workspace();
11978
12413
  init_workspace_lifecycle();
@@ -12470,17 +12905,17 @@ function asRecord3(value) {
12470
12905
  return value;
12471
12906
  }
12472
12907
  function readJson(filePath) {
12473
- if (!import_node_fs27.default.existsSync(filePath)) {
12908
+ if (!import_node_fs28.default.existsSync(filePath)) {
12474
12909
  return void 0;
12475
12910
  }
12476
12911
  try {
12477
- return JSON.parse(import_node_fs27.default.readFileSync(filePath, "utf-8"));
12912
+ return JSON.parse(import_node_fs28.default.readFileSync(filePath, "utf-8"));
12478
12913
  } catch {
12479
12914
  return void 0;
12480
12915
  }
12481
12916
  }
12482
12917
  function relative(repoPath, filePath) {
12483
- return import_node_path28.default.relative(repoPath, filePath) || ".";
12918
+ return import_node_path29.default.relative(repoPath, filePath) || ".";
12484
12919
  }
12485
12920
  function redactEnvAssignments(value) {
12486
12921
  return value.replace(
@@ -12521,7 +12956,7 @@ function inspectPackageManager(repoPath, pkg) {
12521
12956
  ["bun.lock", "bun"]
12522
12957
  ];
12523
12958
  for (const [fileName, name] of lockfiles) {
12524
- if (import_node_fs27.default.existsSync(import_node_path28.default.join(repoPath, fileName))) {
12959
+ if (import_node_fs28.default.existsSync(import_node_path29.default.join(repoPath, fileName))) {
12525
12960
  return { name, source: fileName };
12526
12961
  }
12527
12962
  }
@@ -12536,9 +12971,9 @@ function inspectNode(repoPath, pkg) {
12536
12971
  if (typeof engines?.node === "string") {
12537
12972
  return { version: engines.node, source: "package.json:engines.node" };
12538
12973
  }
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();
12974
+ const nvmrc = import_node_path29.default.join(repoPath, ".nvmrc");
12975
+ if (import_node_fs28.default.existsSync(nvmrc)) {
12976
+ const version = import_node_fs28.default.readFileSync(nvmrc, "utf-8").trim();
12542
12977
  return { version, source: ".nvmrc" };
12543
12978
  }
12544
12979
  return void 0;
@@ -12599,7 +13034,7 @@ function configuredComposeFiles(repoPath) {
12599
13034
  const files = config.apps.filter(
12600
13035
  (app) => app.runtime === "docker"
12601
13036
  ).flatMap((app) => app.docker.composeFiles).filter(
12602
- (fileName) => !import_node_path28.default.isAbsolute(fileName) && !import_node_path28.default.normalize(fileName).startsWith("..")
13037
+ (fileName) => !import_node_path29.default.isAbsolute(fileName) && !import_node_path29.default.normalize(fileName).startsWith("..")
12603
13038
  );
12604
13039
  return Array.from(new Set(files));
12605
13040
  } catch {
@@ -12617,7 +13052,7 @@ function composeFiles(repoPath) {
12617
13052
  ...configuredComposeFiles(repoPath)
12618
13053
  ];
12619
13054
  return Array.from(new Set(candidates)).filter(
12620
- (fileName) => import_node_fs27.default.existsSync(import_node_path28.default.join(repoPath, fileName))
13055
+ (fileName) => import_node_fs28.default.existsSync(import_node_path29.default.join(repoPath, fileName))
12621
13056
  );
12622
13057
  }
12623
13058
  function stringArray2(value) {
@@ -12655,7 +13090,7 @@ function inspectServices(repoPath) {
12655
13090
  const services = [];
12656
13091
  for (const fileName of composeFiles(repoPath)) {
12657
13092
  try {
12658
- const parsed = import_yaml6.default.parse(import_node_fs27.default.readFileSync(import_node_path28.default.join(repoPath, fileName), "utf-8"));
13093
+ const parsed = import_yaml6.default.parse(import_node_fs28.default.readFileSync(import_node_path29.default.join(repoPath, fileName), "utf-8"));
12659
13094
  const serviceMap = asRecord3(asRecord3(parsed)?.services);
12660
13095
  for (const [name, value] of Object.entries(serviceMap ?? {})) {
12661
13096
  const service = asRecord3(value);
@@ -12690,8 +13125,8 @@ function inspectServices(repoPath) {
12690
13125
  return services;
12691
13126
  }
12692
13127
  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");
13128
+ const files = import_node_fs28.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
13129
+ const content = import_node_fs28.default.readFileSync(import_node_path29.default.join(repoPath, fileName), "utf-8");
12695
13130
  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
13131
  return { path: fileName, names };
12697
13132
  });
@@ -12705,18 +13140,18 @@ function inspectEnvFiles(repoPath) {
12705
13140
  };
12706
13141
  }
12707
13142
  function inspectDevcontainer(repoPath) {
12708
- const dir = import_node_path28.default.join(repoPath, ".devcontainer");
12709
- if (!import_node_fs27.default.existsSync(dir)) {
13143
+ const dir = import_node_path29.default.join(repoPath, ".devcontainer");
13144
+ if (!import_node_fs28.default.existsSync(dir)) {
12710
13145
  return { exists: false, files: [] };
12711
13146
  }
12712
13147
  return {
12713
13148
  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}`)
13149
+ 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
13150
  };
12716
13151
  }
12717
13152
  function inspectDevrouter(repoPath) {
12718
13153
  const configPath = getRepoConfigPath(repoPath);
12719
- if (!import_node_fs27.default.existsSync(configPath)) {
13154
+ if (!import_node_fs28.default.existsSync(configPath)) {
12720
13155
  return {
12721
13156
  exists: false,
12722
13157
  configPath,
@@ -12760,15 +13195,15 @@ function inspectAgentGuidance(repoPath) {
12760
13195
  ["AGENTS.md", "agents"],
12761
13196
  ["CLAUDE.md", "claude"]
12762
13197
  ]) {
12763
- if (import_node_fs27.default.existsSync(import_node_path28.default.join(repoPath, fileName))) {
13198
+ if (import_node_fs28.default.existsSync(import_node_path29.default.join(repoPath, fileName))) {
12764
13199
  results.push({ path: fileName, kind });
12765
13200
  }
12766
13201
  }
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)) {
13202
+ const skillsDir = import_node_path29.default.join(repoPath, ".agents", "skills");
13203
+ if (import_node_fs28.default.existsSync(skillsDir)) {
13204
+ for (const name of import_node_fs28.default.readdirSync(skillsDir).sort()) {
13205
+ const skillPath = import_node_path29.default.join(skillsDir, name, "SKILL.md");
13206
+ if (import_node_fs28.default.existsSync(skillPath)) {
12772
13207
  results.push({ path: relative(repoPath, skillPath), kind: "skill" });
12773
13208
  }
12774
13209
  }
@@ -12811,7 +13246,7 @@ function buildIssues(report) {
12811
13246
  }
12812
13247
  function inspectRepo(options = {}) {
12813
13248
  const repoPath = resolveRepoPath(options.repo);
12814
- const pkg = readJson(import_node_path28.default.join(repoPath, "package.json"));
13249
+ const pkg = readJson(import_node_path29.default.join(repoPath, "package.json"));
12815
13250
  const scripts = inspectScripts(pkg);
12816
13251
  const reportWithoutIssues = {
12817
13252
  repoPath,
@@ -12830,12 +13265,12 @@ function inspectRepo(options = {}) {
12830
13265
  issues: buildIssues(reportWithoutIssues)
12831
13266
  };
12832
13267
  }
12833
- var import_node_fs27, import_node_path28, import_yaml6;
13268
+ var import_node_fs28, import_node_path29, import_yaml6;
12834
13269
  var init_repo_inspect = __esm({
12835
13270
  "src/core/repo-inspect.ts"() {
12836
13271
  "use strict";
12837
- import_node_fs27 = __toESM(require("fs"));
12838
- import_node_path28 = __toESM(require("path"));
13272
+ import_node_fs28 = __toESM(require("fs"));
13273
+ import_node_path29 = __toESM(require("path"));
12839
13274
  import_yaml6 = __toESM(require("yaml"));
12840
13275
  init_repo_config();
12841
13276
  }
@@ -12940,7 +13375,7 @@ function requiredFileChecks(repoPath) {
12940
13375
  ".devcontainer/docker-compose.yml",
12941
13376
  ".devrouter.yml"
12942
13377
  ];
12943
- const missing = required.filter((fileName) => !import_node_fs28.default.existsSync(import_node_path29.default.join(repoPath, fileName)));
13378
+ const missing = required.filter((fileName) => !import_node_fs29.default.existsSync(import_node_path30.default.join(repoPath, fileName)));
12944
13379
  return {
12945
13380
  id: "repo.devcontainer.verify-files",
12946
13381
  level: missing.length === 0 ? "ok" : "error",
@@ -13159,12 +13594,12 @@ async function verifyDevcontainer(options = {}) {
13159
13594
  nextSteps: collectNextSteps3(checks)
13160
13595
  };
13161
13596
  }
13162
- var import_node_fs28, import_node_path29;
13597
+ var import_node_fs29, import_node_path30;
13163
13598
  var init_devcontainer_verify = __esm({
13164
13599
  "src/core/devcontainer-verify.ts"() {
13165
13600
  "use strict";
13166
- import_node_fs28 = __toESM(require("fs"));
13167
- import_node_path29 = __toESM(require("path"));
13601
+ import_node_fs29 = __toESM(require("fs"));
13602
+ import_node_path30 = __toESM(require("path"));
13168
13603
  init_capabilities();
13169
13604
  init_doctor();
13170
13605
  init_host_routes();
@@ -13461,7 +13896,7 @@ function packageManagerIssues(repo) {
13461
13896
  }
13462
13897
  function plannedFiles(repoPath, version) {
13463
13898
  const repo = inspectRepo({ repo: repoPath });
13464
- const projectName = sanitizeProjectName(import_node_path30.default.basename(repo.repoPath));
13899
+ const projectName = sanitizeProjectName(import_node_path31.default.basename(repo.repoPath));
13465
13900
  const nodeMajor = majorVersion(repo.node?.version, "24");
13466
13901
  const pnpmVersion = repo.packageManager?.name === "pnpm" && repo.packageManager.version ? repo.packageManager.version : DEFAULT_PNPM_VERSION;
13467
13902
  const port = inferPort2(repo);
@@ -13506,8 +13941,8 @@ function plannedFiles(repoPath, version) {
13506
13941
  };
13507
13942
  }
13508
13943
  function classifyFile(repoPath, file) {
13509
- const absolutePath = import_node_path30.default.join(repoPath, file.relativePath);
13510
- if (!import_node_fs29.default.existsSync(absolutePath)) {
13944
+ const absolutePath = import_node_path31.default.join(repoPath, file.relativePath);
13945
+ if (!import_node_fs30.default.existsSync(absolutePath)) {
13511
13946
  return {
13512
13947
  path: file.relativePath,
13513
13948
  action: "create",
@@ -13515,7 +13950,7 @@ function classifyFile(repoPath, file) {
13515
13950
  bytes: Buffer.byteLength(file.content)
13516
13951
  };
13517
13952
  }
13518
- const current = import_node_fs29.default.readFileSync(absolutePath, "utf-8");
13953
+ const current = import_node_fs30.default.readFileSync(absolutePath, "utf-8");
13519
13954
  if (!current.includes(MANAGED_MARKER2)) {
13520
13955
  return {
13521
13956
  path: file.relativePath,
@@ -13572,11 +14007,11 @@ function buildPlan(repoPath, dryRun, version) {
13572
14007
  };
13573
14008
  }
13574
14009
  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");
14010
+ const absolutePath = import_node_path31.default.join(repoPath, file.relativePath);
14011
+ import_node_fs30.default.mkdirSync(import_node_path31.default.dirname(absolutePath), { recursive: true });
14012
+ import_node_fs30.default.writeFileSync(absolutePath, file.content, "utf-8");
13578
14013
  if (file.executable) {
13579
- import_node_fs29.default.chmodSync(absolutePath, 493);
14014
+ import_node_fs30.default.chmodSync(absolutePath, 493);
13580
14015
  }
13581
14016
  }
13582
14017
  function writeDevcontainer(options = {}) {
@@ -13614,12 +14049,12 @@ function writeDevcontainer(options = {}) {
13614
14049
  nextSteps: postWriteNextSteps(repoPath)
13615
14050
  };
13616
14051
  }
13617
- var import_node_fs29, import_node_path30, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
14052
+ var import_node_fs30, import_node_path31, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
13618
14053
  var init_devcontainer_write = __esm({
13619
14054
  "src/core/devcontainer-write.ts"() {
13620
14055
  "use strict";
13621
- import_node_fs29 = __toESM(require("fs"));
13622
- import_node_path30 = __toESM(require("path"));
14056
+ import_node_fs30 = __toESM(require("fs"));
14057
+ import_node_path31 = __toESM(require("path"));
13623
14058
  init_repo_config();
13624
14059
  init_repo_inspect();
13625
14060
  MANAGED_MARKER2 = "devrouter:managed devcontainer";
@@ -13805,11 +14240,11 @@ var init_profile_resolution = __esm({
13805
14240
  function fail(message) {
13806
14241
  throw new Error(message);
13807
14242
  }
13808
- function isRecord3(value) {
14243
+ function isRecord4(value) {
13809
14244
  return typeof value === "object" && value !== null && !Array.isArray(value);
13810
14245
  }
13811
14246
  function requireRecord(value, label) {
13812
- if (!isRecord3(value)) fail(`${label} must be a mapping.`);
14247
+ if (!isRecord4(value)) fail(`${label} must be a mapping.`);
13813
14248
  return value;
13814
14249
  }
13815
14250
  function requireExactKeys(value, allowed, label) {
@@ -14026,13 +14461,13 @@ function buildProfilePlanReport(options) {
14026
14461
  return { ...report, contractPath, bindings };
14027
14462
  }
14028
14463
  function loadContract(repoPath, contractPath) {
14029
- if (import_node_path31.default.isAbsolute(contractPath)) {
14464
+ if (import_node_path32.default.isAbsolute(contractPath)) {
14030
14465
  fail("profile plan contract path must be relative to the repository.");
14031
14466
  }
14032
14467
  const absolutePath = assertPathWithinRepo(contractPath, repoPath, "Profile plan contract");
14033
14468
  let metadata;
14034
14469
  try {
14035
- metadata = import_node_fs30.default.lstatSync(absolutePath);
14470
+ metadata = import_node_fs31.default.lstatSync(absolutePath);
14036
14471
  } catch (error) {
14037
14472
  fail(`could not inspect profile plan contract '${contractPath}': ${error.message}`);
14038
14473
  }
@@ -14042,16 +14477,16 @@ function loadContract(repoPath, contractPath) {
14042
14477
  if (metadata.size > MAX_CONTRACT_BYTES) {
14043
14478
  fail(`profile plan contract exceeds the ${MAX_CONTRACT_BYTES}-byte limit.`);
14044
14479
  }
14045
- const realRepo = import_node_fs30.default.realpathSync(repoPath);
14046
- const realContract = import_node_fs30.default.realpathSync(absolutePath);
14480
+ const realRepo = import_node_fs31.default.realpathSync(repoPath);
14481
+ const realContract = import_node_fs31.default.realpathSync(absolutePath);
14047
14482
  assertPathWithinRepo(realContract, realRepo, "Profile plan contract");
14048
- const source = import_node_fs30.default.readFileSync(absolutePath, "utf-8");
14483
+ const source = import_node_fs31.default.readFileSync(absolutePath, "utf-8");
14049
14484
  if (Buffer.byteLength(source, "utf-8") > MAX_CONTRACT_BYTES) {
14050
14485
  fail(`profile plan contract exceeds the ${MAX_CONTRACT_BYTES}-byte limit.`);
14051
14486
  }
14052
14487
  return {
14053
14488
  contract: parseProfilePlanContract(source),
14054
- contractPath: import_node_path31.default.relative(repoPath, absolutePath).split(import_node_path31.default.sep).join("/")
14489
+ contractPath: import_node_path32.default.relative(repoPath, absolutePath).split(import_node_path32.default.sep).join("/")
14055
14490
  };
14056
14491
  }
14057
14492
  function resolveProfilePlan(options) {
@@ -14065,12 +14500,12 @@ function resolveProfilePlan(options) {
14065
14500
  contractPath: loaded.contractPath
14066
14501
  });
14067
14502
  }
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;
14503
+ 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
14504
  var init_profile_plan = __esm({
14070
14505
  "src/core/profile-plan.ts"() {
14071
14506
  "use strict";
14072
- import_node_fs30 = __toESM(require("fs"));
14073
- import_node_path31 = __toESM(require("path"));
14507
+ import_node_fs31 = __toESM(require("fs"));
14508
+ import_node_path32 = __toESM(require("path"));
14074
14509
  import_yaml7 = require("yaml");
14075
14510
  init_paths();
14076
14511
  init_profile_resolution();
@@ -14102,7 +14537,7 @@ async function runProfileResolveCommand(options) {
14102
14537
  async function runProfilePlanCommand(options) {
14103
14538
  const report = resolveProfilePlan(options);
14104
14539
  if (options.output) {
14105
- writeFileAtomically(import_node_path32.default.resolve(options.output), `${JSON.stringify(report, null, 2)}
14540
+ writeFileAtomically(import_node_path33.default.resolve(options.output), `${JSON.stringify(report, null, 2)}
14106
14541
  `);
14107
14542
  }
14108
14543
  if (options.json) {
@@ -14138,14 +14573,14 @@ function printProfilePlanSummary(report, output2) {
14138
14573
  process.stdout.write(`Binding ${key}: ${renderValues(report.bindings[key] ?? [])}
14139
14574
  `);
14140
14575
  }
14141
- if (output2) process.stdout.write(`Output: ${import_node_path32.default.resolve(output2)}
14576
+ if (output2) process.stdout.write(`Output: ${import_node_path33.default.resolve(output2)}
14142
14577
  `);
14143
14578
  }
14144
- var import_node_path32;
14579
+ var import_node_path33;
14145
14580
  var init_profile = __esm({
14146
14581
  "src/commands/profile.ts"() {
14147
14582
  "use strict";
14148
- import_node_path32 = __toESM(require("path"));
14583
+ import_node_path33 = __toESM(require("path"));
14149
14584
  init_atomic_file();
14150
14585
  init_output();
14151
14586
  init_profile_plan();
@@ -14435,7 +14870,7 @@ function sanitizeRouterId(value) {
14435
14870
  return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
14436
14871
  }
14437
14872
  function repoHash(repoPath) {
14438
- return (0, import_node_crypto10.createHash)("sha1").update(import_node_path33.default.resolve(repoPath)).digest("hex").slice(0, 12);
14873
+ return (0, import_node_crypto10.createHash)("sha1").update(import_node_path34.default.resolve(repoPath)).digest("hex").slice(0, 12);
14439
14874
  }
14440
14875
  function asDockerApp(app) {
14441
14876
  return app.runtime === "docker";
@@ -14514,11 +14949,11 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
14514
14949
  if (dockerApps.length === 0) {
14515
14950
  throw new Error("No docker apps selected to prepare compose overlay.");
14516
14951
  }
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");
14952
+ const cachePath = import_node_path34.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
14953
+ import_node_fs32.default.mkdirSync(cachePath, { recursive: true });
14954
+ const overlayPath = import_node_path34.default.join(cachePath, "compose.devrouter.yml");
14520
14955
  const overlayDocument = buildOverlayDocument(dockerApps, publishTcpPorts);
14521
- import_node_fs31.default.writeFileSync(overlayPath, import_yaml8.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
14956
+ import_node_fs32.default.writeFileSync(overlayPath, import_yaml8.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
14522
14957
  return {
14523
14958
  overlayPath,
14524
14959
  composeFiles: ensureComposeFiles(dockerApps),
@@ -14638,14 +15073,14 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
14638
15073
  const port = Number(match[1]);
14639
15074
  return Number.isInteger(port) && port > 0 ? port : void 0;
14640
15075
  }
14641
- var import_node_child_process27, import_node_crypto10, import_node_fs31, import_node_path33, import_yaml8;
15076
+ var import_node_child_process27, import_node_crypto10, import_node_fs32, import_node_path34, import_yaml8;
14642
15077
  var init_docker_run = __esm({
14643
15078
  "src/core/docker-run.ts"() {
14644
15079
  "use strict";
14645
15080
  import_node_child_process27 = require("child_process");
14646
15081
  import_node_crypto10 = require("crypto");
14647
- import_node_fs31 = __toESM(require("fs"));
14648
- import_node_path33 = __toESM(require("path"));
15082
+ import_node_fs32 = __toESM(require("fs"));
15083
+ import_node_path34 = __toESM(require("path"));
14649
15084
  import_yaml8 = __toESM(require("yaml"));
14650
15085
  init_docker_error_guidance();
14651
15086
  init_paths();
@@ -15467,13 +15902,13 @@ function measureWorktreeConsumption(worktreePath, options) {
15467
15902
  const startedAt = Date.now();
15468
15903
  let rootStat;
15469
15904
  try {
15470
- rootStat = import_node_fs32.default.lstatSync(worktreePath);
15905
+ rootStat = import_node_fs33.default.lstatSync(worktreePath);
15471
15906
  } catch (error) {
15472
15907
  return { status: "unknown", reason: describeError(error, worktreePath) };
15473
15908
  }
15474
15909
  let rootEntries;
15475
15910
  try {
15476
- rootEntries = import_node_fs32.default.readdirSync(worktreePath, { withFileTypes: true });
15911
+ rootEntries = import_node_fs33.default.readdirSync(worktreePath, { withFileTypes: true });
15477
15912
  } catch (error) {
15478
15913
  return { status: "unknown", reason: describeError(error, worktreePath) };
15479
15914
  }
@@ -15499,10 +15934,10 @@ function measureWorktreeConsumption(worktreePath, options) {
15499
15934
  timedOut = true;
15500
15935
  break;
15501
15936
  }
15502
- const entryPath = import_node_path34.default.join(dirPath, entry.name);
15937
+ const entryPath = import_node_path35.default.join(dirPath, entry.name);
15503
15938
  let entryStat;
15504
15939
  try {
15505
- entryStat = import_node_fs32.default.lstatSync(entryPath);
15940
+ entryStat = import_node_fs33.default.lstatSync(entryPath);
15506
15941
  } catch (error) {
15507
15942
  if (error?.code === "ENOENT") continue;
15508
15943
  unreadableReason = describeIncompleteWalk(error);
@@ -15512,7 +15947,7 @@ function measureWorktreeConsumption(worktreePath, options) {
15512
15947
  if (!entryStat.isDirectory()) continue;
15513
15948
  let childEntries;
15514
15949
  try {
15515
- childEntries = import_node_fs32.default.readdirSync(entryPath, { withFileTypes: true });
15950
+ childEntries = import_node_fs33.default.readdirSync(entryPath, { withFileTypes: true });
15516
15951
  } catch (error) {
15517
15952
  unreadableReason = describeIncompleteWalk(error);
15518
15953
  break;
@@ -15576,12 +16011,12 @@ function describeError(error, worktreePath) {
15576
16011
  }
15577
16012
  return `could not stat worktree path '${worktreePath}': ${error?.message ?? String(error)}`;
15578
16013
  }
15579
- var import_node_fs32, import_node_path34, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
16014
+ var import_node_fs33, import_node_path35, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
15580
16015
  var init_workspace_consumption = __esm({
15581
16016
  "src/core/workspace-consumption.ts"() {
15582
16017
  "use strict";
15583
- import_node_fs32 = __toESM(require("fs"));
15584
- import_node_path34 = __toESM(require("path"));
16018
+ import_node_fs33 = __toESM(require("fs"));
16019
+ import_node_path35 = __toESM(require("path"));
15585
16020
  init_devpod_environment();
15586
16021
  DEFAULT_DEADLINE_MS = 1e4;
15587
16022
  BLOCK_SIZE_BYTES = 512;
@@ -15589,7 +16024,7 @@ var init_workspace_consumption = __esm({
15589
16024
  });
15590
16025
 
15591
16026
  // src/core/workspace-cleanup.ts
15592
- function isRecord4(value) {
16027
+ function isRecord5(value) {
15593
16028
  return value !== null && typeof value === "object" && !Array.isArray(value);
15594
16029
  }
15595
16030
  function isSha(value) {
@@ -15682,7 +16117,7 @@ function evaluateWorkspaceActivity(evidence, cutoff) {
15682
16117
  }
15683
16118
  function readGitSnapshot(worktree, commandRunner) {
15684
16119
  const comparablePath = comparableWorkspacePath(worktree.path);
15685
- if (worktree.prunable || !import_node_fs33.default.existsSync(comparablePath)) {
16120
+ if (worktree.prunable || !import_node_fs34.default.existsSync(comparablePath)) {
15686
16121
  return { worktree, checkout: "missing", head: null, committerDate: null };
15687
16122
  }
15688
16123
  const head = gitOutput(comparablePath, ["rev-parse", "--verify", "HEAD"], commandRunner);
@@ -15733,14 +16168,14 @@ function parseGitHubChanges(value, project, branch) {
15733
16168
  if (!Array.isArray(value)) return void 0;
15734
16169
  const changes = [];
15735
16170
  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;
16171
+ if (!isRecord5(item)) return void 0;
16172
+ const repository = isRecord5(item.repository) ? item.repository.nameWithOwner : void 0;
16173
+ const headRepository = isRecord5(item.headRepository) ? item.headRepository.nameWithOwner : void 0;
15739
16174
  if (repository !== project || headRepository !== project || item.headRefName !== branch || !isSha(item.headRefOid) || typeof item.baseRefName !== "string") {
15740
16175
  continue;
15741
16176
  }
15742
16177
  const merged = item.state === "MERGED" && validTimestamp(item.mergedAt);
15743
- const mergeCommit = isRecord4(item.mergeCommit) && isSha(item.mergeCommit.oid) ? item.mergeCommit.oid : void 0;
16178
+ const mergeCommit = isRecord5(item.mergeCommit) && isSha(item.mergeCommit.oid) ? item.mergeCommit.oid : void 0;
15744
16179
  const baseSha = isSha(item.baseRefOid) ? item.baseRefOid : void 0;
15745
16180
  changes.push({
15746
16181
  sourceBranch: branch,
@@ -15757,14 +16192,14 @@ function parseGitLabChanges(value, _project, branch) {
15757
16192
  if (!Array.isArray(value)) return void 0;
15758
16193
  const changes = [];
15759
16194
  for (const item of value) {
15760
- if (!isRecord4(item)) return void 0;
16195
+ if (!isRecord5(item)) return void 0;
15761
16196
  const sourceProjectId = item.source_project_id;
15762
16197
  const targetProjectId = item.target_project_id;
15763
16198
  const sameProject = typeof sourceProjectId === "number" && typeof targetProjectId === "number" && sourceProjectId === targetProjectId;
15764
16199
  if (!sameProject || item.source_branch !== branch || !isSha(item.sha) || typeof item.target_branch !== "string") {
15765
16200
  continue;
15766
16201
  }
15767
- const diffRefs = isRecord4(item.diff_refs) ? item.diff_refs : void 0;
16202
+ const diffRefs = isRecord5(item.diff_refs) ? item.diff_refs : void 0;
15768
16203
  const mergeCommitSha = isSha(item.merge_commit_sha) ? item.merge_commit_sha : void 0;
15769
16204
  const baseSha = diffRefs && isSha(diffRefs.base_sha) ? diffRefs.base_sha : void 0;
15770
16205
  changes.push({
@@ -16207,7 +16642,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
16207
16642
  containers = measureContainersFn(worktreePaths);
16208
16643
  } catch (error) {
16209
16644
  const reason = `container measurement failed: ${describeCause(error)}`;
16210
- containers = new Map(worktreePaths.map((path34) => [path34, unknownContainers(reason)]));
16645
+ containers = new Map(worktreePaths.map((path35) => [path35, unknownContainers(reason)]));
16211
16646
  }
16212
16647
  }
16213
16648
  const rows = records.map((record) => {
@@ -16256,12 +16691,12 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
16256
16691
  workspaces: rows
16257
16692
  };
16258
16693
  }
16259
- var import_node_child_process29, import_node_fs33, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
16694
+ var import_node_child_process29, import_node_fs34, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
16260
16695
  var init_workspace_cleanup = __esm({
16261
16696
  "src/core/workspace-cleanup.ts"() {
16262
16697
  "use strict";
16263
16698
  import_node_child_process29 = require("child_process");
16264
- import_node_fs33 = __toESM(require("fs"));
16699
+ import_node_fs34 = __toESM(require("fs"));
16265
16700
  init_devpod_workspaces();
16266
16701
  init_host_routes();
16267
16702
  init_repo_config();
@@ -16438,7 +16873,7 @@ var init_version = __esm({
16438
16873
 
16439
16874
  // src/cli.ts
16440
16875
  var import_commander = require("commander");
16441
- var CLI_VERSION = true ? "0.0.52" : "0.0.0-dev";
16876
+ var CLI_VERSION = true ? "0.0.54" : "0.0.0-dev";
16442
16877
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
16443
16878
  function withErrorHandling(action2) {
16444
16879
  return async (...args) => {