@devrouter/cli 0.0.32 → 0.0.34

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/README.md CHANGED
@@ -118,11 +118,11 @@ vars. Use it when you are not (yet) on a devcontainer. Fully supported.
118
118
  The current `devrouter repo devcontainer write` scaffold is intentionally narrow:
119
119
  Node + pnpm + Postgres. Other package managers stop with a JSON diagnostic
120
120
  instead of writing files that would need manual repair.
121
- The generated image extracts `devrouter-process` from the exact Devrouter
122
- package tarball without installing the CLI dependency tree. Its `post-start.sh`
123
- uses that helper for locked, owned, idempotent background startup. Application
124
- commands and environment setup remain repository-owned; route readiness remains
125
- part of `devrouter ensure`.
121
+ The generated image contains no Devrouter package or helper. `devrouter ensure`
122
+ delivers its matching `devrouter-process` helper to the exact running container,
123
+ then invokes the repository-owned `post-start.sh` for locked, owned, idempotent
124
+ background startup. Application commands and environment setup remain
125
+ repository-owned; route readiness remains part of `devrouter ensure`.
126
126
  Use `devrouter repo devcontainer verify --json` for read-only PR evidence; add
127
127
  `--live --yes` only for compatibility checks. Normal startup uses `ensure`.
128
128
 
package/dist/devrouter.js CHANGED
@@ -43,6 +43,7 @@ function buildDevrouterSection() {
43
43
  `\`${DEVROUTER_SKILL_REL_PATH}\``,
44
44
  "",
45
45
  "Quick validation sequence:",
46
+ "- Managed devcontainer consumer images contain no devrouter package or helper; `devrouter ensure` delivers the matching helper at runtime.",
46
47
  "- `devrouter up`",
47
48
  "- `devrouter tls install` (required when repo defines tcp/postgres apps)",
48
49
  "- `devrouter app ls --repo .`",
@@ -347,6 +348,7 @@ For host/docker runtime apps only:
347
348
 
348
349
  ## Runtime behavior notes
349
350
 
351
+ - Managed devcontainer images contain no devrouter package or helper. \`devrouter ensure\` delivers its matching process helper to the exact running container and invokes the repository-owned \`.devcontainer/post-start.sh\`; keep \`.devrouter.yml\` as the only consumer-side devrouter version pin.
350
352
  - \`devrouter app run\` auto-starts Docker dependencies and waits for health. Host app runs stop auto-started docker deps on exit; docker app runs leave target services running until explicit cleanup.
351
353
  - Host-runtime dependencies are NOT auto-started (v1).
352
354
  - \`kind=dependency\` entries do not create routes and cannot be direct targets for \`devrouter app run\`, \`devrouter app exec\`, or \`devrouter open\`.
@@ -1794,7 +1796,7 @@ function loadRepoConfig(repoPath) {
1794
1796
  const config = parseConfig(parsed ?? {}, configPath);
1795
1797
  const requiredVersion = config.devrouter?.version;
1796
1798
  if (requiredVersion && !hasWarnedVersionMismatch) {
1797
- const cliVersion = true ? "0.0.32" : "0.0.0-dev";
1799
+ const cliVersion = true ? "0.0.34" : "0.0.0-dev";
1798
1800
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
1799
1801
  hasWarnedVersionMismatch = true;
1800
1802
  process.stderr.write(
@@ -2298,7 +2300,7 @@ function buildOnboardingPrompt(options = {}) {
2298
2300
  "- The owner record survives linked-worktree removal and binds the exact path to its DevPod ID. First use reuses an exact-path DevPod or derives a sanitized branch/path identity. Later flags or `DEVROUTER_WORKSPACE` may repeat but cannot rename it. Ambiguous identities fail closed. The primary checkout stays non-namespaced.",
2299
2301
  `- When a workspace is active: hosts auto-namespace (\`web.localhost\` \u2192 \`web.<ws>.localhost\`), \`${WORKSPACE_PLACEHOLDER}\` in \`upstream\` is substituted with the token, and the docker \`router\` key is suffixed per workspace. The runtime config is computed in memory only \u2014 the committed \`.devrouter.yml\` is never rewritten.`,
2300
2302
  "- TLS: namespaced hosts (`web.<ws>.localhost`) are not covered by the `*.localhost` wildcard; devrouter auto-extends the mkcert cert SANs for active hosts when TLS is enabled.",
2301
- "- Lifecycle: after one-time setup, use `devrouter ensure .` for both primary and linked checkouts; never branch on checkout kind or use live verify as startup. Use `devrouter stop .` for a non-destructive pause and `devrouter exec . -- <command...>` for container commands. `workspace up` creates linked worktrees; destructive down/GC remains ledger-scoped.",
2303
+ "- Lifecycle: after one-time setup, use `devrouter ensure .` for both primary and linked checkouts; never branch on checkout kind or use live verify as startup. Managed consumer images contain no devrouter package/helper: ensure delivers its matching helper at runtime and invokes the repository-owned post-start adapter. Keep `.devrouter.yml` as the only consumer-side version pin. Use `devrouter stop .` for a non-destructive pause and `devrouter exec . -- <command...>` for container commands. `workspace up` creates linked worktrees; destructive down/GC remains ledger-scoped.",
2302
2304
  "- 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.",
2303
2305
  "- Workspace commands require Git. Normal config, app, status, and doctor flows work from a `.devrouter.yml` folder without `.git`. Git has no worktree-removal hook; use `workspace ls`, doctor, or dry-run GC after out-of-band removal.",
2304
2306
  "- devcontainer integration: `devcontainer.json` lists the base compose file then `${localEnv:DEVCONTAINER_COMPOSE_OVERLAY:docker-compose.default.yml}`. The default overlay contains `services: {}`; `.devcontainer/docker-compose.devrouter.yml` passes `WORKSPACE` and `DEVROUTER_WORKSPACE` into the app and bind-mounts `${DEVROUTER_GIT_COMMON_DIR}` to the same absolute app-container path. Ensure proves exact DevPod ownership, overlay/Git mounts, env, aliases, health, Git, HTTP route reachability, and unique running TCP upstream ownership before success.",
@@ -3291,6 +3293,19 @@ function buildDevcontainerChecks(repoPath, config, workspace) {
3291
3293
  details: compose.publishedPorts.length > 0 ? compose.publishedPorts.join(", ") : void 0,
3292
3294
  suggestion: compose.parseError || compose.publishedPorts.length > 0 ? "Remove published ports and route services through devnet aliases with devrouter proxy apps." : void 0
3293
3295
  });
3296
+ const dockerfilePath = import_node_path7.default.join(devcontainerDir, "Dockerfile");
3297
+ const dockerfileExists = import_node_fs8.default.existsSync(dockerfilePath);
3298
+ const dockerfile = dockerfileExists ? import_node_fs8.default.readFileSync(dockerfilePath, "utf-8") : "";
3299
+ const devrouterArtifacts = ["@devrouter/cli", "devrouter-process"].filter(
3300
+ (artifact) => dockerfile.includes(artifact)
3301
+ );
3302
+ checks.push({
3303
+ id: "repo.devcontainer.no-devrouter-image-install",
3304
+ level: !dockerfileExists ? "warn" : devrouterArtifacts.length === 0 ? "ok" : "error",
3305
+ summary: !dockerfileExists ? "No .devcontainer/Dockerfile found to inspect for devrouter artifacts." : devrouterArtifacts.length === 0 ? "Consumer image does not install or extract devrouter artifacts." : "Consumer image installs or extracts devrouter artifacts.",
3306
+ details: devrouterArtifacts.length > 0 ? `artifacts=${devrouterArtifacts.join(", ")}` : void 0,
3307
+ suggestion: !dockerfileExists ? "Inspect the consumer image definition and keep devrouter package/helper installation out of it." : devrouterArtifacts.length > 0 ? "Remove devrouter package/helper installation from the Dockerfile; devrouter ensure delivers the helper at runtime." : void 0
3308
+ });
3294
3309
  if (!config) {
3295
3310
  checks.push({
3296
3311
  id: "repo.devcontainer.upstream-alias-match",
@@ -5087,7 +5102,7 @@ async function buildDoctorReport(options = {}) {
5087
5102
  const config = runtimeConfig.config;
5088
5103
  loadedConfig = config;
5089
5104
  loadedWorkspace = runtimeConfig.workspace;
5090
- const cliVersion = true ? "0.0.32" : "0.0.0-dev";
5105
+ const cliVersion = true ? "0.0.34" : "0.0.0-dev";
5091
5106
  const configVersion = config.devrouter?.version;
5092
5107
  if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
5093
5108
  addCheck(checks, {
@@ -5760,6 +5775,107 @@ var init_http_route_probe = __esm({
5760
5775
  }
5761
5776
  });
5762
5777
 
5778
+ // src/core/managed-post-start.ts
5779
+ function commandFailure(result) {
5780
+ return [result.error?.message, result.stderr, result.stdout].filter(Boolean).map((value) => String(value).trim()).filter(Boolean).join("\n");
5781
+ }
5782
+ function readRegularFile(filePath) {
5783
+ if (!import_node_fs15.default.existsSync(filePath) || !import_node_fs15.default.lstatSync(filePath).isFile()) return void 0;
5784
+ return import_node_fs15.default.readFileSync(filePath, "utf-8");
5785
+ }
5786
+ function resolveProcessHelperPath() {
5787
+ const candidates = [
5788
+ import_node_path15.default.resolve(__dirname, "..", "bin", "devrouter-process"),
5789
+ import_node_path15.default.resolve(__dirname, "..", "..", "bin", "devrouter-process")
5790
+ ];
5791
+ const helperPath = candidates.find((candidate) => import_node_fs15.default.existsSync(candidate));
5792
+ if (!helperPath) {
5793
+ throw new Error("Could not locate the packaged devrouter-process helper.");
5794
+ }
5795
+ return helperPath;
5796
+ }
5797
+ function resolveManagedPostStartPlan(repoPath) {
5798
+ const adapterPath = import_node_path15.default.join(repoPath, MANAGED_ADAPTER_PATH);
5799
+ const adapter = readRegularFile(adapterPath);
5800
+ if (adapter === void 0) return { kind: "unmanaged" };
5801
+ if (!adapter.includes(MANAGED_MARKER)) return { kind: "unmanaged" };
5802
+ if (adapter.includes("DEVROUTER_PROCESS_HELPER")) {
5803
+ return { kind: "runtime", adapterPath: MANAGED_ADAPTER_PATH };
5804
+ }
5805
+ const dockerfilePath = import_node_path15.default.join(repoPath, ".devcontainer", "Dockerfile");
5806
+ const devcontainerPath = import_node_path15.default.join(repoPath, ".devcontainer", "devcontainer.json");
5807
+ const dockerfile = readRegularFile(dockerfilePath) ?? "";
5808
+ const devcontainer = readRegularFile(devcontainerPath) ?? "";
5809
+ if (dockerfile.includes("devrouter-process") && devcontainer.includes("postStartCommand")) {
5810
+ return { kind: "legacy" };
5811
+ }
5812
+ throw new Error(
5813
+ "Managed post-start must use DEVROUTER_PROCESS_HELPER before removing the legacy image helper and postStartCommand. Regenerate or migrate the devcontainer, then retry devrouter ensure."
5814
+ );
5815
+ }
5816
+ function runManagedPostStart(options) {
5817
+ if (options.plan.kind !== "runtime") return;
5818
+ const helper = import_node_fs15.default.readFileSync(resolveProcessHelperPath());
5819
+ const delivered = (0, import_node_child_process11.spawnSync)(
5820
+ "docker",
5821
+ ["exec", "-i", options.container.id, "sh", "-c", DELIVERY_SCRIPT],
5822
+ { input: helper, encoding: "utf-8" }
5823
+ );
5824
+ if (delivered.status !== 0) {
5825
+ const details = commandFailure(delivered);
5826
+ throw new Error(
5827
+ `Could not deliver the managed process helper${details ? `: ${details}` : "."}`
5828
+ );
5829
+ }
5830
+ const started = (0, import_node_child_process11.spawnSync)(
5831
+ "docker",
5832
+ [
5833
+ "exec",
5834
+ "--workdir",
5835
+ options.container.workspacePath,
5836
+ "--env",
5837
+ `DEVROUTER_PROCESS_HELPER=${RUNTIME_HELPER_PATH}`,
5838
+ options.container.id,
5839
+ "bash",
5840
+ options.plan.adapterPath
5841
+ ],
5842
+ { stdio: options.quiet ? ["ignore", 2, "inherit"] : "inherit" }
5843
+ );
5844
+ if (started.status !== 0) {
5845
+ const details = commandFailure(started);
5846
+ throw new Error(`Managed post-start failed${details ? `: ${details}` : "."}`);
5847
+ }
5848
+ }
5849
+ var import_node_child_process11, import_node_fs15, import_node_path15, MANAGED_MARKER, MANAGED_ADAPTER_PATH, RUNTIME_HELPER_PATH, DELIVERY_SCRIPT;
5850
+ var init_managed_post_start = __esm({
5851
+ "src/core/managed-post-start.ts"() {
5852
+ "use strict";
5853
+ import_node_child_process11 = require("child_process");
5854
+ import_node_fs15 = __toESM(require("fs"));
5855
+ import_node_path15 = __toESM(require("path"));
5856
+ MANAGED_MARKER = "devrouter:managed devcontainer";
5857
+ MANAGED_ADAPTER_PATH = ".devcontainer/post-start.sh";
5858
+ RUNTIME_HELPER_PATH = "/tmp/devrouter/bin/devrouter-process";
5859
+ DELIVERY_SCRIPT = `set -eu
5860
+ umask 077
5861
+ runtime_root=/tmp/devrouter
5862
+ runtime_bin="$runtime_root/bin"
5863
+ if [ -L "$runtime_root" ] || [ -L "$runtime_bin" ]; then
5864
+ echo "Refusing symlinked devrouter runtime path." >&2
5865
+ exit 1
5866
+ fi
5867
+ mkdir -p "$runtime_bin"
5868
+ chmod 700 "$runtime_root" "$runtime_bin"
5869
+ temporary="$(mktemp "$runtime_bin/.devrouter-process.XXXXXX")"
5870
+ trap 'rm -f "$temporary"' EXIT
5871
+ cat > "$temporary"
5872
+ chmod 700 "$temporary"
5873
+ mv -f "$temporary" "${RUNTIME_HELPER_PATH}"
5874
+ trap - EXIT
5875
+ `;
5876
+ }
5877
+ });
5878
+
5763
5879
  // src/core/route-publication.ts
5764
5880
  function routedAppsFromConfig(config) {
5765
5881
  return config.apps.filter((app) => app.kind !== "dependency");
@@ -5833,11 +5949,11 @@ var init_route_publication = __esm({
5833
5949
  // src/core/workspace-ensure.ts
5834
5950
  function assertOverlay(container, repoPath) {
5835
5951
  const workingDir = container.labels["com.docker.compose.project.working_dir"];
5836
- if (!workingDir || !sameWorkspacePath(workingDir, import_node_path15.default.join(repoPath, ".devcontainer"))) {
5952
+ if (!workingDir || !sameWorkspacePath(workingDir, import_node_path16.default.join(repoPath, ".devcontainer"))) {
5837
5953
  throw new Error(`Container '${container.id}' does not belong to the exact worktree.`);
5838
5954
  }
5839
5955
  const configFiles = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").filter(Boolean);
5840
- const expectedOverlay = import_node_path15.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
5956
+ const expectedOverlay = import_node_path16.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
5841
5957
  if (!configFiles.some((configFile) => sameWorkspacePath(configFile, expectedOverlay))) {
5842
5958
  throw new Error(`Container '${container.id}' was not started with ${DEVCONTAINER_OVERLAY}.`);
5843
5959
  }
@@ -5911,7 +6027,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
5911
6027
  target
5912
6028
  });
5913
6029
  if (target.kind === "linked") {
5914
- const workspaceEnv = (0, import_node_child_process11.spawnSync)(
6030
+ const workspaceEnv = (0, import_node_child_process12.spawnSync)(
5915
6031
  "docker",
5916
6032
  ["exec", appContainer.id, "printenv", "WORKSPACE"],
5917
6033
  { encoding: "utf-8" }
@@ -5921,7 +6037,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
5921
6037
  `Workspace app container must expose WORKSPACE='${target.workspace}' (got '${workspaceEnv.stdout.trim() || "(empty)"}').`
5922
6038
  );
5923
6039
  }
5924
- const devrouterWorkspaceEnv = (0, import_node_child_process11.spawnSync)(
6040
+ const devrouterWorkspaceEnv = (0, import_node_child_process12.spawnSync)(
5925
6041
  "docker",
5926
6042
  ["exec", appContainer.id, "printenv", "DEVROUTER_WORKSPACE"],
5927
6043
  { encoding: "utf-8" }
@@ -5932,7 +6048,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
5932
6048
  );
5933
6049
  }
5934
6050
  }
5935
- const gitCheck = (0, import_node_child_process11.spawnSync)(
6051
+ const gitCheck = (0, import_node_child_process12.spawnSync)(
5936
6052
  "docker",
5937
6053
  [
5938
6054
  "exec",
@@ -5948,7 +6064,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
5948
6064
  if (gitCheck.status !== 0 || !sameWorkspacePath(gitCheck.stdout.trim(), appContainer.workspacePath)) {
5949
6065
  throw new Error("Git does not resolve the expected checkout inside the app container.");
5950
6066
  }
5951
- return;
6067
+ return appContainer;
5952
6068
  } catch (error) {
5953
6069
  lastError = error;
5954
6070
  }
@@ -6019,7 +6135,7 @@ function resolvePrimaryTarget(repoPath) {
6019
6135
  }
6020
6136
  function isPrimaryCheckout(repoPath) {
6021
6137
  try {
6022
- return import_node_fs15.default.statSync(import_node_path15.default.join(repoPath, ".git")).isDirectory();
6138
+ return import_node_fs16.default.statSync(import_node_path16.default.join(repoPath, ".git")).isDirectory();
6023
6139
  } catch {
6024
6140
  return false;
6025
6141
  }
@@ -6056,7 +6172,7 @@ function startDevpod(repoPath, target, recreate = false, quiet = false) {
6056
6172
  delete env.DEVROUTER_GIT_COMMON_DIR;
6057
6173
  delete env.DEVCONTAINER_COMPOSE_OVERLAY;
6058
6174
  }
6059
- const result = (0, import_node_child_process11.spawnSync)("devpod", args, {
6175
+ const result = (0, import_node_child_process12.spawnSync)("devpod", args, {
6060
6176
  stdio: quiet ? ["inherit", 2, "inherit"] : "inherit",
6061
6177
  env
6062
6178
  });
@@ -6075,7 +6191,7 @@ function assertDevpodAttachment(repoPath, expectedId) {
6075
6191
  }
6076
6192
  function openUrls(urls) {
6077
6193
  for (const url of urls) {
6078
- const opened = (0, import_node_child_process11.spawnSync)("open", [url], { encoding: "utf-8" });
6194
+ const opened = (0, import_node_child_process12.spawnSync)("open", [url], { encoding: "utf-8" });
6079
6195
  if (opened.status !== 0) {
6080
6196
  process.stderr.write(`Warning: could not open '${url}'.
6081
6197
  `);
@@ -6105,18 +6221,19 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
6105
6221
  const target = linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
6106
6222
  let devpodId = target.devpodId;
6107
6223
  if (target.kind === "linked") {
6108
- const overlayPath = import_node_path15.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
6109
- if (!import_node_fs15.default.existsSync(overlayPath)) {
6224
+ const overlayPath = import_node_path16.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
6225
+ if (!import_node_fs16.default.existsSync(overlayPath)) {
6110
6226
  throw new Error(`Missing required DevPod compose overlay: ${overlayPath}`);
6111
6227
  }
6112
6228
  }
6229
+ const managedPostStart = resolveManagedPostStartPlan(repoPath);
6113
6230
  const runtime = loadRuntimeConfig(
6114
6231
  repoPath,
6115
6232
  target.kind === "primary" ? "" : target.workspace
6116
6233
  );
6117
6234
  const apps = proxyAppsFromConfig(runtime.config);
6118
6235
  const parsedUpstreams = apps.map((app) => parseUpstream(app.upstream));
6119
- const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path15.default.basename(repoPath)) ?? "app";
6236
+ const aliasPrefix = target.kind === "linked" ? target.workspace : wsFromBranch(runtime.config.project?.name ?? import_node_path16.default.basename(repoPath)) ?? "app";
6120
6237
  for (const [index, app] of apps.entries()) {
6121
6238
  if (app.protocol === "tcp" && !parsedUpstreams[index].host.startsWith(`${aliasPrefix}-`)) {
6122
6239
  const owner = target.kind === "linked" ? "workspace" : "checkout";
@@ -6144,36 +6261,38 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
6144
6261
  writeWorkspaceOwnership(repoPath, ownership);
6145
6262
  }
6146
6263
  };
6147
- const recreateAndWait = async () => {
6264
+ const preflight = (timeoutMs) => waitForContainerPreflight(repoPath, currentTarget(), upstreamHosts, timeoutMs);
6265
+ const recreateAndPreflight = async () => {
6148
6266
  startAndProveAttachment(true);
6149
- await waitForContainerPreflight(
6150
- repoPath,
6151
- currentTarget(),
6152
- upstreamHosts,
6153
- options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS
6154
- );
6267
+ return preflight(options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS);
6155
6268
  };
6156
6269
  let recreated = false;
6270
+ let container;
6157
6271
  try {
6158
6272
  startAndProveAttachment();
6159
6273
  } catch (error) {
6160
6274
  if (!target.hadExactDevpod) {
6161
6275
  throw error;
6162
6276
  }
6163
- await recreateAndWait();
6277
+ container = await recreateAndPreflight();
6164
6278
  recreated = true;
6165
6279
  }
6166
- if (!recreated) {
6280
+ if (!container) {
6167
6281
  try {
6168
- await waitForContainerPreflight(repoPath, currentTarget(), upstreamHosts, 0);
6282
+ container = await preflight(0);
6169
6283
  } catch (error) {
6170
6284
  if (!target.hadExactDevpod) {
6171
6285
  throw error;
6172
6286
  }
6173
- await recreateAndWait();
6287
+ container = await recreateAndPreflight();
6174
6288
  recreated = true;
6175
6289
  }
6176
6290
  }
6291
+ runManagedPostStart({
6292
+ plan: managedPostStart,
6293
+ container,
6294
+ quiet: options.quiet
6295
+ });
6177
6296
  const publication = await replacePublishedProxyRoutes(
6178
6297
  repoPath,
6179
6298
  runtime.config,
@@ -6190,7 +6309,12 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
6190
6309
  if (!target.hadExactDevpod || recreated) {
6191
6310
  throw error;
6192
6311
  }
6193
- await recreateAndWait();
6312
+ const recoveredContainer = await recreateAndPreflight();
6313
+ runManagedPostStart({
6314
+ plan: managedPostStart,
6315
+ container: recoveredContainer,
6316
+ quiet: options.quiet
6317
+ });
6194
6318
  recreated = true;
6195
6319
  replaceHostRoutesForRepo(repoPath, publication.routes);
6196
6320
  await waitForHttpRoutes(
@@ -6233,17 +6357,18 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
6233
6357
  }
6234
6358
  });
6235
6359
  }
6236
- var import_node_child_process11, import_node_fs15, import_node_path15, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
6360
+ var import_node_child_process12, import_node_fs16, import_node_path16, DEVCONTAINER_OVERLAY, DEFAULT_READINESS_TIMEOUT_MS, POLL_INTERVAL_MS;
6237
6361
  var init_workspace_ensure = __esm({
6238
6362
  "src/core/workspace-ensure.ts"() {
6239
6363
  "use strict";
6240
- import_node_child_process11 = require("child_process");
6241
- import_node_fs15 = __toESM(require("fs"));
6242
- import_node_path15 = __toESM(require("path"));
6364
+ import_node_child_process12 = require("child_process");
6365
+ import_node_fs16 = __toESM(require("fs"));
6366
+ import_node_path16 = __toESM(require("path"));
6243
6367
  init_devpod_environment();
6244
6368
  init_devpod_workspaces();
6245
6369
  init_host_routes();
6246
6370
  init_http_route_probe();
6371
+ init_managed_post_start();
6247
6372
  init_repo_config();
6248
6373
  init_route_publication();
6249
6374
  init_router();
@@ -6313,20 +6438,20 @@ function warnMissingWorkspaceOwnership(repoPath) {
6313
6438
  );
6314
6439
  }
6315
6440
  function defaultWorktreePath(mainRepo, ws) {
6316
- return import_node_path16.default.join(mainRepo, "trees", ws);
6441
+ return import_node_path17.default.join(mainRepo, "trees", ws);
6317
6442
  }
6318
6443
  function assertDefaultWorktreeRootIgnored(mainRepo) {
6319
- const ignored = (0, import_node_child_process12.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
6444
+ const ignored = (0, import_node_child_process13.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
6320
6445
  encoding: "utf-8"
6321
6446
  });
6322
6447
  if (ignored.status !== 0) {
6323
6448
  throw new Error(
6324
- `Default worktree root '${import_node_path16.default.join(mainRepo, "trees")}' is not ignored. Add 'trees/' to '${import_node_path16.default.join(mainRepo, ".gitignore")}' or use --path.`
6449
+ `Default worktree root '${import_node_path17.default.join(mainRepo, "trees")}' is not ignored. Add 'trees/' to '${import_node_path17.default.join(mainRepo, ".gitignore")}' or use --path.`
6325
6450
  );
6326
6451
  }
6327
6452
  }
6328
6453
  function legacyDefaultWorktreePath(mainRepo, ws) {
6329
- return import_node_path16.default.join(import_node_path16.default.dirname(mainRepo), `${import_node_path16.default.basename(mainRepo)}-${ws}`);
6454
+ return import_node_path17.default.join(import_node_path17.default.dirname(mainRepo), `${import_node_path17.default.basename(mainRepo)}-${ws}`);
6330
6455
  }
6331
6456
  function teardownFallbackPath(mainRepo, workspace) {
6332
6457
  const candidates = [
@@ -6439,13 +6564,13 @@ function assertFullDownPreflight(mainRepo, target) {
6439
6564
  if (sameWorkspacePath(target.worktreePath, mainRepo)) {
6440
6565
  throw new Error("Refusing to remove the primary Git checkout.");
6441
6566
  }
6442
- if (!target.worktree || target.worktree.prunable || !import_node_fs16.default.existsSync(target.worktreePath)) return;
6567
+ if (!target.worktree || target.worktree.prunable || !import_node_fs17.default.existsSync(target.worktreePath)) return;
6443
6568
  if (target.worktree.locked) {
6444
6569
  throw new Error(
6445
6570
  `Worktree '${target.worktreePath}' is locked; unlock it before workspace down.`
6446
6571
  );
6447
6572
  }
6448
- const status = (0, import_node_child_process12.spawnSync)(
6573
+ const status = (0, import_node_child_process13.spawnSync)(
6449
6574
  "git",
6450
6575
  ["-C", target.worktreePath, "status", "--porcelain", "--untracked-files=normal"],
6451
6576
  { encoding: "utf-8" }
@@ -6466,8 +6591,8 @@ async function workspaceUp(branch, opts = {}) {
6466
6591
  if (!ws) {
6467
6592
  throw new Error(`Branch '${branch}' does not yield a valid workspace token.`);
6468
6593
  }
6469
- const worktreePath = opts.path ? import_node_path16.default.resolve(opts.path) : defaultWorktreePath(mainRepo, ws);
6470
- if (import_node_fs16.default.existsSync(worktreePath)) {
6594
+ const worktreePath = opts.path ? import_node_path17.default.resolve(opts.path) : defaultWorktreePath(mainRepo, ws);
6595
+ if (import_node_fs17.default.existsSync(worktreePath)) {
6471
6596
  const registered = listGitWorktrees(mainRepo).find(
6472
6597
  (worktree) => sameWorkspacePath(worktree.path, worktreePath)
6473
6598
  );
@@ -6485,11 +6610,11 @@ async function workspaceUp(branch, opts = {}) {
6485
6610
  if (!opts.path) {
6486
6611
  assertDefaultWorktreeRootIgnored(mainRepo);
6487
6612
  }
6488
- const add = (0, import_node_child_process12.spawnSync)("git", ["-C", mainRepo, "worktree", "add", worktreePath, branch], {
6613
+ const add = (0, import_node_child_process13.spawnSync)("git", ["-C", mainRepo, "worktree", "add", worktreePath, branch], {
6489
6614
  encoding: "utf-8"
6490
6615
  });
6491
6616
  if (add.status !== 0) {
6492
- const addNew = (0, import_node_child_process12.spawnSync)(
6617
+ const addNew = (0, import_node_child_process13.spawnSync)(
6493
6618
  "git",
6494
6619
  ["-C", mainRepo, "worktree", "add", "-b", branch, worktreePath],
6495
6620
  {
@@ -6590,8 +6715,8 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
6590
6715
  }
6591
6716
  const result = mutateWorkspaceRuntime(action2, resolved, worktrees, opts.quiet);
6592
6717
  if (removeWorktree) {
6593
- if (resolved.worktree && !resolved.worktree.prunable && import_node_fs16.default.existsSync(resolved.worktreePath)) {
6594
- const rm = (0, import_node_child_process12.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
6718
+ if (resolved.worktree && !resolved.worktree.prunable && import_node_fs17.default.existsSync(resolved.worktreePath)) {
6719
+ const rm = (0, import_node_child_process13.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
6595
6720
  encoding: "utf-8"
6596
6721
  });
6597
6722
  if (rm.status !== 0) {
@@ -6609,7 +6734,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
6609
6734
  }
6610
6735
  return result;
6611
6736
  };
6612
- return resolved.worktree && !resolved.worktree.prunable && import_node_fs16.default.existsSync(resolved.worktreePath) ? withWorkspaceLifecycleLock(resolved.worktreePath, operation) : operation();
6737
+ return resolved.worktree && !resolved.worktree.prunable && import_node_fs17.default.existsSync(resolved.worktreePath) ? withWorkspaceLifecycleLock(resolved.worktreePath, operation) : operation();
6613
6738
  }
6614
6739
  async function workspaceStopOwnedPath(worktreePath, opts = {}) {
6615
6740
  const mainRepo = resolveRepoPath(opts.repoPath);
@@ -6638,13 +6763,13 @@ async function workspaceStop(target, opts = {}) {
6638
6763
  async function workspaceDown(target, opts = {}) {
6639
6764
  return runWorkspaceLifecycle("down", target, opts);
6640
6765
  }
6641
- var import_node_child_process12, import_node_fs16, import_node_path16;
6766
+ var import_node_child_process13, import_node_fs17, import_node_path17;
6642
6767
  var init_workspace_lifecycle = __esm({
6643
6768
  "src/core/workspace-lifecycle.ts"() {
6644
6769
  "use strict";
6645
- import_node_child_process12 = require("child_process");
6646
- import_node_fs16 = __toESM(require("fs"));
6647
- import_node_path16 = __toESM(require("path"));
6770
+ import_node_child_process13 = require("child_process");
6771
+ import_node_fs17 = __toESM(require("fs"));
6772
+ import_node_path17 = __toESM(require("path"));
6648
6773
  init_devpod_workspaces();
6649
6774
  init_repo_config();
6650
6775
  init_route_state();
@@ -6751,6 +6876,14 @@ var init_stop = __esm({
6751
6876
  });
6752
6877
 
6753
6878
  // src/core/devpod-exec.ts
6879
+ function stripDevpodCompletionDiagnostic(value) {
6880
+ const index = value.indexOf(DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC);
6881
+ if (index < 0) return value;
6882
+ return Buffer.concat([
6883
+ value.subarray(0, index),
6884
+ value.subarray(index + DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC.length)
6885
+ ]);
6886
+ }
6754
6887
  function quotePosixArg(value) {
6755
6888
  return `'${value.replaceAll("'", `'"'"'`)}'`;
6756
6889
  }
@@ -6758,7 +6891,7 @@ function ensureGuidance(repoPath) {
6758
6891
  return `Run 'devrouter ensure ${repoPath}' first.`;
6759
6892
  }
6760
6893
  function assertRunningDevpod(devpodId, repoPath) {
6761
- const result = (0, import_node_child_process13.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
6894
+ const result = (0, import_node_child_process14.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
6762
6895
  encoding: "utf-8"
6763
6896
  });
6764
6897
  if (result.status !== 0) {
@@ -6814,7 +6947,7 @@ async function devpodExec(repoPath, command) {
6814
6947
  wrappedCommand
6815
6948
  ];
6816
6949
  return new Promise((resolve, reject) => {
6817
- const child = (0, import_node_child_process13.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
6950
+ const child = (0, import_node_child_process14.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
6818
6951
  let pending = Buffer.alloc(0);
6819
6952
  let statusBytes = Buffer.alloc(0);
6820
6953
  let readingStatus = false;
@@ -6825,6 +6958,7 @@ async function devpodExec(repoPath, command) {
6825
6958
  child.stderr.on("data", (chunk) => {
6826
6959
  pending = Buffer.concat([pending, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
6827
6960
  for (; ; ) {
6961
+ if (remoteStatus !== void 0) break;
6828
6962
  if (readingStatus) {
6829
6963
  const newline = pending.indexOf(10);
6830
6964
  if (newline < 0) {
@@ -6858,7 +6992,9 @@ async function devpodExec(repoPath, command) {
6858
6992
  reject(new Error(`devpod ssh terminated by signal ${signal ?? "unknown"}.`));
6859
6993
  return;
6860
6994
  }
6861
- if (!readingStatus) forward(pending);
6995
+ if (!readingStatus) {
6996
+ forward(remoteStatus === void 0 ? pending : stripDevpodCompletionDiagnostic(pending));
6997
+ }
6862
6998
  if (remoteStatus === void 0) {
6863
6999
  reject(
6864
7000
  new Error(`DevPod command did not report its exit status (devpod exited ${code}).`)
@@ -6870,15 +7006,18 @@ async function devpodExec(repoPath, command) {
6870
7006
  });
6871
7007
  });
6872
7008
  }
6873
- var import_node_child_process13, import_node_crypto4;
7009
+ var import_node_child_process14, import_node_crypto4, DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC;
6874
7010
  var init_devpod_exec = __esm({
6875
7011
  "src/core/devpod-exec.ts"() {
6876
7012
  "use strict";
6877
- import_node_child_process13 = require("child_process");
7013
+ import_node_child_process14 = require("child_process");
6878
7014
  import_node_crypto4 = require("crypto");
6879
7015
  init_devpod_environment();
6880
7016
  init_devpod_workspaces();
6881
7017
  init_workspace();
7018
+ DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC = Buffer.from(
7019
+ "Error tunneling to container: wait: remote command exited without exit status or exit signal\n"
7020
+ );
6882
7021
  }
6883
7022
  });
6884
7023
 
@@ -7056,7 +7195,7 @@ async function runOpenCommand(name) {
7056
7195
  );
7057
7196
  return;
7058
7197
  }
7059
- const result = (0, import_node_child_process14.spawnSync)("open", [url], { encoding: "utf-8" });
7198
+ const result = (0, import_node_child_process15.spawnSync)("open", [url], { encoding: "utf-8" });
7060
7199
  if (result.status !== 0) {
7061
7200
  const details = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
7062
7201
  throw new Error(`Unable to open '${url}': ${details || "unknown error"}`);
@@ -7064,11 +7203,11 @@ async function runOpenCommand(name) {
7064
7203
  process.stdout.write(`Opened ${url}
7065
7204
  `);
7066
7205
  }
7067
- var import_node_child_process14;
7206
+ var import_node_child_process15;
7068
7207
  var init_open = __esm({
7069
7208
  "src/commands/open.ts"() {
7070
7209
  "use strict";
7071
- import_node_child_process14 = require("child_process");
7210
+ import_node_child_process15 = require("child_process");
7072
7211
  init_docker();
7073
7212
  init_host_routes();
7074
7213
  init_repo_config();
@@ -7091,7 +7230,7 @@ async function runLogsCommand(options) {
7091
7230
  const args = ["logs", "--tail", tail, ROUTER_CONTAINER_NAME];
7092
7231
  if (options.follow) {
7093
7232
  args.splice(1, 0, "-f");
7094
- const child = (0, import_node_child_process15.spawn)("docker", args, { stdio: "inherit" });
7233
+ const child = (0, import_node_child_process16.spawn)("docker", args, { stdio: "inherit" });
7095
7234
  const onSignal = () => {
7096
7235
  child.kill("SIGTERM");
7097
7236
  };
@@ -7105,17 +7244,17 @@ async function runLogsCommand(options) {
7105
7244
  });
7106
7245
  });
7107
7246
  } else {
7108
- const result = (0, import_node_child_process15.spawnSync)("docker", args, { stdio: "inherit" });
7247
+ const result = (0, import_node_child_process16.spawnSync)("docker", args, { stdio: "inherit" });
7109
7248
  if (result.status !== 0) {
7110
7249
  throw new Error("Failed to retrieve router logs.");
7111
7250
  }
7112
7251
  }
7113
7252
  }
7114
- var import_node_child_process15;
7253
+ var import_node_child_process16;
7115
7254
  var init_logs = __esm({
7116
7255
  "src/commands/logs.ts"() {
7117
7256
  "use strict";
7118
- import_node_child_process15 = require("child_process");
7257
+ import_node_child_process16 = require("child_process");
7119
7258
  init_docker();
7120
7259
  init_router();
7121
7260
  }
@@ -7155,17 +7294,17 @@ function asRecord3(value) {
7155
7294
  return value;
7156
7295
  }
7157
7296
  function readJson(filePath) {
7158
- if (!import_node_fs17.default.existsSync(filePath)) {
7297
+ if (!import_node_fs18.default.existsSync(filePath)) {
7159
7298
  return void 0;
7160
7299
  }
7161
7300
  try {
7162
- return JSON.parse(import_node_fs17.default.readFileSync(filePath, "utf-8"));
7301
+ return JSON.parse(import_node_fs18.default.readFileSync(filePath, "utf-8"));
7163
7302
  } catch {
7164
7303
  return void 0;
7165
7304
  }
7166
7305
  }
7167
7306
  function relative(repoPath, filePath) {
7168
- return import_node_path17.default.relative(repoPath, filePath) || ".";
7307
+ return import_node_path18.default.relative(repoPath, filePath) || ".";
7169
7308
  }
7170
7309
  function redactEnvAssignments(value) {
7171
7310
  return value.replace(
@@ -7206,7 +7345,7 @@ function inspectPackageManager(repoPath, pkg) {
7206
7345
  ["bun.lock", "bun"]
7207
7346
  ];
7208
7347
  for (const [fileName, name] of lockfiles) {
7209
- if (import_node_fs17.default.existsSync(import_node_path17.default.join(repoPath, fileName))) {
7348
+ if (import_node_fs18.default.existsSync(import_node_path18.default.join(repoPath, fileName))) {
7210
7349
  return { name, source: fileName };
7211
7350
  }
7212
7351
  }
@@ -7221,9 +7360,9 @@ function inspectNode(repoPath, pkg) {
7221
7360
  if (typeof engines?.node === "string") {
7222
7361
  return { version: engines.node, source: "package.json:engines.node" };
7223
7362
  }
7224
- const nvmrc = import_node_path17.default.join(repoPath, ".nvmrc");
7225
- if (import_node_fs17.default.existsSync(nvmrc)) {
7226
- const version = import_node_fs17.default.readFileSync(nvmrc, "utf-8").trim();
7363
+ const nvmrc = import_node_path18.default.join(repoPath, ".nvmrc");
7364
+ if (import_node_fs18.default.existsSync(nvmrc)) {
7365
+ const version = import_node_fs18.default.readFileSync(nvmrc, "utf-8").trim();
7227
7366
  return { version, source: ".nvmrc" };
7228
7367
  }
7229
7368
  return void 0;
@@ -7284,7 +7423,7 @@ function configuredComposeFiles(repoPath) {
7284
7423
  const files = config.apps.filter(
7285
7424
  (app) => app.runtime === "docker"
7286
7425
  ).flatMap((app) => app.docker.composeFiles).filter(
7287
- (fileName) => !import_node_path17.default.isAbsolute(fileName) && !import_node_path17.default.normalize(fileName).startsWith("..")
7426
+ (fileName) => !import_node_path18.default.isAbsolute(fileName) && !import_node_path18.default.normalize(fileName).startsWith("..")
7288
7427
  );
7289
7428
  return Array.from(new Set(files));
7290
7429
  } catch {
@@ -7302,7 +7441,7 @@ function composeFiles(repoPath) {
7302
7441
  ...configuredComposeFiles(repoPath)
7303
7442
  ];
7304
7443
  return Array.from(new Set(candidates)).filter(
7305
- (fileName) => import_node_fs17.default.existsSync(import_node_path17.default.join(repoPath, fileName))
7444
+ (fileName) => import_node_fs18.default.existsSync(import_node_path18.default.join(repoPath, fileName))
7306
7445
  );
7307
7446
  }
7308
7447
  function stringArray(value) {
@@ -7340,7 +7479,7 @@ function inspectServices(repoPath) {
7340
7479
  const services = [];
7341
7480
  for (const fileName of composeFiles(repoPath)) {
7342
7481
  try {
7343
- const parsed = import_yaml5.default.parse(import_node_fs17.default.readFileSync(import_node_path17.default.join(repoPath, fileName), "utf-8"));
7482
+ const parsed = import_yaml5.default.parse(import_node_fs18.default.readFileSync(import_node_path18.default.join(repoPath, fileName), "utf-8"));
7344
7483
  const serviceMap = asRecord3(asRecord3(parsed)?.services);
7345
7484
  for (const [name, value] of Object.entries(serviceMap ?? {})) {
7346
7485
  const service = asRecord3(value);
@@ -7375,8 +7514,8 @@ function inspectServices(repoPath) {
7375
7514
  return services;
7376
7515
  }
7377
7516
  function inspectEnvFiles(repoPath) {
7378
- const files = import_node_fs17.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
7379
- const content = import_node_fs17.default.readFileSync(import_node_path17.default.join(repoPath, fileName), "utf-8");
7517
+ const files = import_node_fs18.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
7518
+ const content = import_node_fs18.default.readFileSync(import_node_path18.default.join(repoPath, fileName), "utf-8");
7380
7519
  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();
7381
7520
  return { path: fileName, names };
7382
7521
  });
@@ -7390,18 +7529,18 @@ function inspectEnvFiles(repoPath) {
7390
7529
  };
7391
7530
  }
7392
7531
  function inspectDevcontainer(repoPath) {
7393
- const dir = import_node_path17.default.join(repoPath, ".devcontainer");
7394
- if (!import_node_fs17.default.existsSync(dir)) {
7532
+ const dir = import_node_path18.default.join(repoPath, ".devcontainer");
7533
+ if (!import_node_fs18.default.existsSync(dir)) {
7395
7534
  return { exists: false, files: [] };
7396
7535
  }
7397
7536
  return {
7398
7537
  exists: true,
7399
- files: import_node_fs17.default.readdirSync(dir).filter((fileName) => import_node_fs17.default.statSync(import_node_path17.default.join(dir, fileName)).isFile()).sort().map((fileName) => `.devcontainer/${fileName}`)
7538
+ files: import_node_fs18.default.readdirSync(dir).filter((fileName) => import_node_fs18.default.statSync(import_node_path18.default.join(dir, fileName)).isFile()).sort().map((fileName) => `.devcontainer/${fileName}`)
7400
7539
  };
7401
7540
  }
7402
7541
  function inspectDevrouter(repoPath) {
7403
7542
  const configPath = getRepoConfigPath(repoPath);
7404
- if (!import_node_fs17.default.existsSync(configPath)) {
7543
+ if (!import_node_fs18.default.existsSync(configPath)) {
7405
7544
  return {
7406
7545
  exists: false,
7407
7546
  configPath,
@@ -7445,15 +7584,15 @@ function inspectAgentGuidance(repoPath) {
7445
7584
  ["AGENTS.md", "agents"],
7446
7585
  ["CLAUDE.md", "claude"]
7447
7586
  ]) {
7448
- if (import_node_fs17.default.existsSync(import_node_path17.default.join(repoPath, fileName))) {
7587
+ if (import_node_fs18.default.existsSync(import_node_path18.default.join(repoPath, fileName))) {
7449
7588
  results.push({ path: fileName, kind });
7450
7589
  }
7451
7590
  }
7452
- const skillsDir = import_node_path17.default.join(repoPath, ".agents", "skills");
7453
- if (import_node_fs17.default.existsSync(skillsDir)) {
7454
- for (const name of import_node_fs17.default.readdirSync(skillsDir).sort()) {
7455
- const skillPath = import_node_path17.default.join(skillsDir, name, "SKILL.md");
7456
- if (import_node_fs17.default.existsSync(skillPath)) {
7591
+ const skillsDir = import_node_path18.default.join(repoPath, ".agents", "skills");
7592
+ if (import_node_fs18.default.existsSync(skillsDir)) {
7593
+ for (const name of import_node_fs18.default.readdirSync(skillsDir).sort()) {
7594
+ const skillPath = import_node_path18.default.join(skillsDir, name, "SKILL.md");
7595
+ if (import_node_fs18.default.existsSync(skillPath)) {
7457
7596
  results.push({ path: relative(repoPath, skillPath), kind: "skill" });
7458
7597
  }
7459
7598
  }
@@ -7496,7 +7635,7 @@ function buildIssues(report) {
7496
7635
  }
7497
7636
  function inspectRepo(options = {}) {
7498
7637
  const repoPath = resolveRepoPath(options.repo);
7499
- const pkg = readJson(import_node_path17.default.join(repoPath, "package.json"));
7638
+ const pkg = readJson(import_node_path18.default.join(repoPath, "package.json"));
7500
7639
  const scripts = inspectScripts(pkg);
7501
7640
  const reportWithoutIssues = {
7502
7641
  repoPath,
@@ -7515,12 +7654,12 @@ function inspectRepo(options = {}) {
7515
7654
  issues: buildIssues(reportWithoutIssues)
7516
7655
  };
7517
7656
  }
7518
- var import_node_fs17, import_node_path17, import_yaml5;
7657
+ var import_node_fs18, import_node_path18, import_yaml5;
7519
7658
  var init_repo_inspect = __esm({
7520
7659
  "src/core/repo-inspect.ts"() {
7521
7660
  "use strict";
7522
- import_node_fs17 = __toESM(require("fs"));
7523
- import_node_path17 = __toESM(require("path"));
7661
+ import_node_fs18 = __toESM(require("fs"));
7662
+ import_node_path18 = __toESM(require("path"));
7524
7663
  import_yaml5 = __toESM(require("yaml"));
7525
7664
  init_repo_config();
7526
7665
  }
@@ -7625,7 +7764,7 @@ function requiredFileChecks(repoPath) {
7625
7764
  ".devcontainer/docker-compose.yml",
7626
7765
  ".devrouter.yml"
7627
7766
  ];
7628
- const missing = required.filter((fileName) => !import_node_fs18.default.existsSync(import_node_path18.default.join(repoPath, fileName)));
7767
+ const missing = required.filter((fileName) => !import_node_fs19.default.existsSync(import_node_path19.default.join(repoPath, fileName)));
7629
7768
  return {
7630
7769
  id: "repo.devcontainer.verify-files",
7631
7770
  level: missing.length === 0 ? "ok" : "error",
@@ -7844,12 +7983,12 @@ async function verifyDevcontainer(options = {}) {
7844
7983
  nextSteps: collectNextSteps3(checks)
7845
7984
  };
7846
7985
  }
7847
- var import_node_fs18, import_node_path18;
7986
+ var import_node_fs19, import_node_path19;
7848
7987
  var init_devcontainer_verify = __esm({
7849
7988
  "src/core/devcontainer-verify.ts"() {
7850
7989
  "use strict";
7851
- import_node_fs18 = __toESM(require("fs"));
7852
- import_node_path18 = __toESM(require("path"));
7990
+ import_node_fs19 = __toESM(require("fs"));
7991
+ import_node_path19 = __toESM(require("path"));
7853
7992
  init_capabilities();
7854
7993
  init_doctor();
7855
7994
  init_host_routes();
@@ -7887,30 +8026,22 @@ function inferDevProcess(repo) {
7887
8026
  function inferPort2(repo) {
7888
8027
  return repo.apps.find((app) => app.port)?.port ?? 3e3;
7889
8028
  }
7890
- function renderDockerfile(nodeMajor, pnpmVersion, version) {
8029
+ function renderDockerfile(nodeMajor, pnpmVersion) {
7891
8030
  const pnpmPackageSpec = `pnpm@${pnpmVersion}`;
7892
- const devrouterPackageSpec = `@devrouter/cli@${version}`;
7893
- const devrouterTarball = `devrouter-cli-${version}.tgz`;
7894
- return `# ${MANAGED_MARKER}
8031
+ return `# ${MANAGED_MARKER2}
7895
8032
  FROM node:${nodeMajor}-bookworm-slim
7896
8033
 
7897
8034
  RUN apt-get update \\
7898
- && apt-get install -y --no-install-recommends git ca-certificates curl procps openssl tar util-linux \\
8035
+ && apt-get install -y --no-install-recommends git ca-certificates curl procps openssl util-linux \\
7899
8036
  && rm -rf /var/lib/apt/lists/*
7900
8037
 
7901
8038
  RUN npm install -g ${shellSingleQuote(pnpmPackageSpec)}
7902
8039
 
7903
- RUN npm pack --silent ${shellSingleQuote(devrouterPackageSpec)} \\
7904
- && tar -xzf ${shellSingleQuote(devrouterTarball)} --strip-components=2 \\
7905
- -C /usr/local/bin package/bin/devrouter-process \\
7906
- && chmod +x /usr/local/bin/devrouter-process \\
7907
- && rm ${shellSingleQuote(devrouterTarball)}
7908
-
7909
8040
  WORKDIR /workspaces/app
7910
8041
  `;
7911
8042
  }
7912
8043
  function renderCompose(projectName) {
7913
- return `# ${MANAGED_MARKER}
8044
+ return `# ${MANAGED_MARKER2}
7914
8045
  services:
7915
8046
  postgres:
7916
8047
  image: postgres:16-alpine
@@ -7963,12 +8094,12 @@ volumes:
7963
8094
  `;
7964
8095
  }
7965
8096
  function renderDefaultComposeOverlay() {
7966
- return `# ${MANAGED_MARKER}
8097
+ return `# ${MANAGED_MARKER2}
7967
8098
  services: {}
7968
8099
  `;
7969
8100
  }
7970
8101
  function renderDevrouterComposeOverlay() {
7971
- return `# ${MANAGED_MARKER}
8102
+ return `# ${MANAGED_MARKER2}
7972
8103
  # A linked worktree's .git file points at the host repository's common Git
7973
8104
  # directory. workspace ensure sets this absolute path before DevPod starts.
7974
8105
  services:
@@ -7984,7 +8115,7 @@ services:
7984
8115
  }
7985
8116
  function renderInitDb() {
7986
8117
  return `#!/usr/bin/env bash
7987
- # ${MANAGED_MARKER}
8118
+ # ${MANAGED_MARKER2}
7988
8119
  set -euo pipefail
7989
8120
 
7990
8121
  psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<'SQL'
@@ -8003,10 +8134,9 @@ function renderDevcontainerJson(projectName) {
8003
8134
  "service": "app",
8004
8135
  "workspaceFolder": "/workspaces/${projectName}",
8005
8136
  "postCreateCommand": "bash .devcontainer/post-create.sh",
8006
- "postStartCommand": "bash .devcontainer/post-start.sh",
8007
8137
  "customizations": {
8008
8138
  "devrouter": {
8009
- "managed": "${MANAGED_MARKER}"
8139
+ "managed": "${MANAGED_MARKER2}"
8010
8140
  },
8011
8141
  "vscode": {
8012
8142
  "extensions": []
@@ -8016,7 +8146,7 @@ function renderDevcontainerJson(projectName) {
8016
8146
  `;
8017
8147
  }
8018
8148
  function renderEnv(projectName, port) {
8019
- return `# ${MANAGED_MARKER}
8149
+ return `# ${MANAGED_MARKER2}
8020
8150
  WORKSPACE=${projectName}
8021
8151
  HOST=0.0.0.0
8022
8152
  HOSTNAME=0.0.0.0
@@ -8027,7 +8157,7 @@ SHADOW_DATABASE_URL=postgres://prisma:prisma@postgres:5432/shadow
8027
8157
  }
8028
8158
  function renderPostCreate() {
8029
8159
  return `#!/usr/bin/env bash
8030
- # ${MANAGED_MARKER}
8160
+ # ${MANAGED_MARKER2}
8031
8161
  set -euo pipefail
8032
8162
 
8033
8163
  export CI=true
@@ -8043,7 +8173,7 @@ function shellSingleQuote(value) {
8043
8173
  }
8044
8174
  function renderPostStart(devProcess) {
8045
8175
  return `#!/usr/bin/env bash
8046
- # ${MANAGED_MARKER}
8176
+ # ${MANAGED_MARKER2}
8047
8177
  set -euo pipefail
8048
8178
 
8049
8179
  export CI=true
@@ -8052,7 +8182,9 @@ set -a
8052
8182
  . .devcontainer/devcontainer.env
8053
8183
  set +a
8054
8184
 
8055
- devrouter-process ensure \\
8185
+ : "\${DEVROUTER_PROCESS_HELPER:?Run devrouter ensure to start this managed application process.}"
8186
+
8187
+ "$DEVROUTER_PROCESS_HELPER" ensure \\
8056
8188
  --name app \\
8057
8189
  --match ${shellSingleQuote(devProcess.match)} \\
8058
8190
  --log /tmp/devrouter-app.log \\
@@ -8060,7 +8192,7 @@ devrouter-process ensure \\
8060
8192
  `;
8061
8193
  }
8062
8194
  function renderDevrouter(projectName, port, version) {
8063
- return `# ${MANAGED_MARKER}
8195
+ return `# ${MANAGED_MARKER2}
8064
8196
  version: 1
8065
8197
  devrouter:
8066
8198
  version: ${version}
@@ -8082,24 +8214,16 @@ apps:
8082
8214
  `;
8083
8215
  }
8084
8216
  function renderReadme(projectName) {
8085
- return `<!-- ${MANAGED_MARKER} -->
8217
+ return `<!-- ${MANAGED_MARKER2} -->
8086
8218
  # Devcontainer
8087
8219
 
8088
8220
  Use this repo through the devcontainer, with devrouter providing stable local routes.
8089
8221
 
8090
- Primary checkout:
8222
+ Primary or linked checkout:
8091
8223
 
8092
8224
  \`\`\`bash
8093
8225
  devrouter setup --yes
8094
- devpod up .
8095
- devrouter app run app --repo . --yes
8096
- devrouter app run db --repo . --yes
8097
- \`\`\`
8098
-
8099
- Existing linked worktree:
8100
-
8101
- \`\`\`bash
8102
- devrouter workspace ensure .
8226
+ devrouter ensure .
8103
8227
  \`\`\`
8104
8228
 
8105
8229
  - App: https://${projectName}.localhost
@@ -8108,18 +8232,9 @@ devrouter workspace ensure .
8108
8232
  }
8109
8233
  function postWriteNextSteps(repoPath) {
8110
8234
  const quotedRepoPath = shellSingleQuote(repoPath);
8111
- if (isLinkedWorktree(repoPath)) {
8112
- return [
8113
- `Run: devrouter setup --repo ${quotedRepoPath} --yes`,
8114
- `Run: devrouter workspace ensure ${quotedRepoPath}`,
8115
- `Optional: devrouter repo agents --repo ${quotedRepoPath}`
8116
- ];
8117
- }
8118
8235
  return [
8119
8236
  `Run: devrouter setup --repo ${quotedRepoPath} --yes`,
8120
- `Run: cd ${quotedRepoPath} && devpod up .`,
8121
- `Run: devrouter app run app --repo ${quotedRepoPath} --yes`,
8122
- `Run: devrouter app run db --repo ${quotedRepoPath} --yes`,
8237
+ `Run: devrouter ensure ${quotedRepoPath}`,
8123
8238
  `Optional: devrouter repo agents --repo ${quotedRepoPath}`
8124
8239
  ];
8125
8240
  }
@@ -8169,7 +8284,7 @@ function packageManagerIssues(repo) {
8169
8284
  }
8170
8285
  function plannedFiles(repoPath, version) {
8171
8286
  const repo = inspectRepo({ repo: repoPath });
8172
- const projectName = sanitizeProjectName(import_node_path19.default.basename(repo.repoPath));
8287
+ const projectName = sanitizeProjectName(import_node_path20.default.basename(repo.repoPath));
8173
8288
  const nodeMajor = majorVersion(repo.node?.version, "24");
8174
8289
  const pnpmVersion = repo.packageManager?.name === "pnpm" && repo.packageManager.version ? repo.packageManager.version : DEFAULT_PNPM_VERSION;
8175
8290
  const port = inferPort2(repo);
@@ -8181,7 +8296,7 @@ function plannedFiles(repoPath, version) {
8181
8296
  files: [
8182
8297
  {
8183
8298
  relativePath: ".devcontainer/Dockerfile",
8184
- content: renderDockerfile(nodeMajor, pnpmVersion, version)
8299
+ content: renderDockerfile(nodeMajor, pnpmVersion)
8185
8300
  },
8186
8301
  { relativePath: ".devcontainer/docker-compose.yml", content: renderCompose(projectName) },
8187
8302
  {
@@ -8214,8 +8329,8 @@ function plannedFiles(repoPath, version) {
8214
8329
  };
8215
8330
  }
8216
8331
  function classifyFile(repoPath, file) {
8217
- const absolutePath = import_node_path19.default.join(repoPath, file.relativePath);
8218
- if (!import_node_fs19.default.existsSync(absolutePath)) {
8332
+ const absolutePath = import_node_path20.default.join(repoPath, file.relativePath);
8333
+ if (!import_node_fs20.default.existsSync(absolutePath)) {
8219
8334
  return {
8220
8335
  path: file.relativePath,
8221
8336
  action: "create",
@@ -8223,8 +8338,8 @@ function classifyFile(repoPath, file) {
8223
8338
  bytes: Buffer.byteLength(file.content)
8224
8339
  };
8225
8340
  }
8226
- const current = import_node_fs19.default.readFileSync(absolutePath, "utf-8");
8227
- if (!current.includes(MANAGED_MARKER)) {
8341
+ const current = import_node_fs20.default.readFileSync(absolutePath, "utf-8");
8342
+ if (!current.includes(MANAGED_MARKER2)) {
8228
8343
  return {
8229
8344
  path: file.relativePath,
8230
8345
  action: "conflict",
@@ -8280,11 +8395,11 @@ function buildPlan(repoPath, dryRun, version) {
8280
8395
  };
8281
8396
  }
8282
8397
  function writeFile(repoPath, file) {
8283
- const absolutePath = import_node_path19.default.join(repoPath, file.relativePath);
8284
- import_node_fs19.default.mkdirSync(import_node_path19.default.dirname(absolutePath), { recursive: true });
8285
- import_node_fs19.default.writeFileSync(absolutePath, file.content, "utf-8");
8398
+ const absolutePath = import_node_path20.default.join(repoPath, file.relativePath);
8399
+ import_node_fs20.default.mkdirSync(import_node_path20.default.dirname(absolutePath), { recursive: true });
8400
+ import_node_fs20.default.writeFileSync(absolutePath, file.content, "utf-8");
8286
8401
  if (file.executable) {
8287
- import_node_fs19.default.chmodSync(absolutePath, 493);
8402
+ import_node_fs20.default.chmodSync(absolutePath, 493);
8288
8403
  }
8289
8404
  }
8290
8405
  function writeDevcontainer(options = {}) {
@@ -8322,16 +8437,15 @@ function writeDevcontainer(options = {}) {
8322
8437
  nextSteps: postWriteNextSteps(repoPath)
8323
8438
  };
8324
8439
  }
8325
- var import_node_fs19, import_node_path19, MANAGED_MARKER, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
8440
+ var import_node_fs20, import_node_path20, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
8326
8441
  var init_devcontainer_write = __esm({
8327
8442
  "src/core/devcontainer-write.ts"() {
8328
8443
  "use strict";
8329
- import_node_fs19 = __toESM(require("fs"));
8330
- import_node_path19 = __toESM(require("path"));
8444
+ import_node_fs20 = __toESM(require("fs"));
8445
+ import_node_path20 = __toESM(require("path"));
8331
8446
  init_repo_config();
8332
8447
  init_repo_inspect();
8333
- init_workspace();
8334
- MANAGED_MARKER = "devrouter:managed devcontainer";
8448
+ MANAGED_MARKER2 = "devrouter:managed devcontainer";
8335
8449
  DEFAULT_DEVROUTER_VERSION = "0.0.0";
8336
8450
  DEFAULT_PNPM_VERSION = "11.6.0";
8337
8451
  VALID_PACKAGE_VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
@@ -8719,7 +8833,7 @@ function sanitizeRouterId(value) {
8719
8833
  return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
8720
8834
  }
8721
8835
  function repoHash(repoPath) {
8722
- return (0, import_node_crypto5.createHash)("sha1").update(import_node_path20.default.resolve(repoPath)).digest("hex").slice(0, 12);
8836
+ return (0, import_node_crypto5.createHash)("sha1").update(import_node_path21.default.resolve(repoPath)).digest("hex").slice(0, 12);
8723
8837
  }
8724
8838
  function asDockerApp(app) {
8725
8839
  return app.runtime === "docker";
@@ -8798,11 +8912,11 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
8798
8912
  if (dockerApps.length === 0) {
8799
8913
  throw new Error("No docker apps selected to prepare compose overlay.");
8800
8914
  }
8801
- const cachePath = import_node_path20.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
8802
- import_node_fs20.default.mkdirSync(cachePath, { recursive: true });
8803
- const overlayPath = import_node_path20.default.join(cachePath, "compose.devrouter.yml");
8915
+ const cachePath = import_node_path21.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
8916
+ import_node_fs21.default.mkdirSync(cachePath, { recursive: true });
8917
+ const overlayPath = import_node_path21.default.join(cachePath, "compose.devrouter.yml");
8804
8918
  const overlayDocument = buildOverlayDocument(dockerApps, publishTcpPorts);
8805
- import_node_fs20.default.writeFileSync(overlayPath, import_yaml6.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
8919
+ import_node_fs21.default.writeFileSync(overlayPath, import_yaml6.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
8806
8920
  return {
8807
8921
  overlayPath,
8808
8922
  composeFiles: ensureComposeFiles(dockerApps),
@@ -8816,7 +8930,7 @@ function runDockerComposeUp(repoPath, composeFiles2, overlayPath, services) {
8816
8930
  fileArgs.push("-f", resolved);
8817
8931
  }
8818
8932
  const args = ["compose", ...fileArgs, "-f", overlayPath, "up", "-d", "--wait", ...services];
8819
- const result = (0, import_node_child_process16.spawnSync)("docker", args, {
8933
+ const result = (0, import_node_child_process17.spawnSync)("docker", args, {
8820
8934
  encoding: "utf-8",
8821
8935
  cwd: repoPath
8822
8936
  });
@@ -8844,7 +8958,7 @@ function queryRunningComposeServices(repoPath, composeFiles2, overlayPath, servi
8844
8958
  "--services",
8845
8959
  ...services
8846
8960
  ];
8847
- const result = (0, import_node_child_process16.spawnSync)("docker", args, {
8961
+ const result = (0, import_node_child_process17.spawnSync)("docker", args, {
8848
8962
  encoding: "utf-8",
8849
8963
  cwd: repoPath
8850
8964
  });
@@ -8870,7 +8984,7 @@ function runDockerComposeStop(repoPath, composeFiles2, overlayPath, services) {
8870
8984
  fileArgs.push("-f", resolved);
8871
8985
  }
8872
8986
  const args = ["compose", ...fileArgs, "-f", overlayPath, "stop", ...services];
8873
- const result = (0, import_node_child_process16.spawnSync)("docker", args, {
8987
+ const result = (0, import_node_child_process17.spawnSync)("docker", args, {
8874
8988
  encoding: "utf-8",
8875
8989
  cwd: repoPath
8876
8990
  });
@@ -8896,7 +9010,7 @@ function runDockerComposeLogs(repoPath, composeFiles2, overlayPath, services, ta
8896
9010
  String(tail),
8897
9011
  ...services
8898
9012
  ];
8899
- (0, import_node_child_process16.spawnSync)("docker", args, {
9013
+ (0, import_node_child_process17.spawnSync)("docker", args, {
8900
9014
  stdio: "inherit",
8901
9015
  cwd: repoPath
8902
9016
  });
@@ -8908,7 +9022,7 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
8908
9022
  fileArgs.push("-f", resolved);
8909
9023
  }
8910
9024
  const args = ["compose", ...fileArgs, "-f", overlayPath, "port", service, String(internalPort)];
8911
- const result = (0, import_node_child_process16.spawnSync)("docker", args, {
9025
+ const result = (0, import_node_child_process17.spawnSync)("docker", args, {
8912
9026
  encoding: "utf-8",
8913
9027
  cwd: repoPath
8914
9028
  });
@@ -8922,14 +9036,14 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
8922
9036
  const port = Number(match[1]);
8923
9037
  return Number.isInteger(port) && port > 0 ? port : void 0;
8924
9038
  }
8925
- var import_node_child_process16, import_node_crypto5, import_node_fs20, import_node_path20, import_yaml6;
9039
+ var import_node_child_process17, import_node_crypto5, import_node_fs21, import_node_path21, import_yaml6;
8926
9040
  var init_docker_run = __esm({
8927
9041
  "src/core/docker-run.ts"() {
8928
9042
  "use strict";
8929
- import_node_child_process16 = require("child_process");
9043
+ import_node_child_process17 = require("child_process");
8930
9044
  import_node_crypto5 = require("crypto");
8931
- import_node_fs20 = __toESM(require("fs"));
8932
- import_node_path20 = __toESM(require("path"));
9045
+ import_node_fs21 = __toESM(require("fs"));
9046
+ import_node_path21 = __toESM(require("path"));
8933
9047
  import_yaml6 = __toESM(require("yaml"));
8934
9048
  init_docker_error_guidance();
8935
9049
  init_paths();
@@ -9019,7 +9133,7 @@ function isProcessRunning(pid) {
9019
9133
  }
9020
9134
  }
9021
9135
  function readProcessTree(rootPid) {
9022
- const result = (0, import_node_child_process17.spawnSync)("ps", ["-ax", "-o", "pid=,ppid="], { encoding: "utf-8" });
9136
+ const result = (0, import_node_child_process18.spawnSync)("ps", ["-ax", "-o", "pid=,ppid="], { encoding: "utf-8" });
9023
9137
  if (result.status !== 0) {
9024
9138
  return [rootPid];
9025
9139
  }
@@ -9130,12 +9244,12 @@ function detectListeningPorts(pids) {
9130
9244
  if (pids.length === 0) {
9131
9245
  return [];
9132
9246
  }
9133
- const result = (0, import_node_child_process17.spawnSync)("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", pids.join(",")], {
9247
+ const result = (0, import_node_child_process18.spawnSync)("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", pids.join(",")], {
9134
9248
  encoding: "utf-8"
9135
9249
  });
9136
9250
  if (result.error && result.error.code === "ENOENT") {
9137
9251
  if (process.platform === "linux") {
9138
- const ssResult = (0, import_node_child_process17.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
9252
+ const ssResult = (0, import_node_child_process18.spawnSync)("ss", ["-H", "-lntp", "-p"], { encoding: "utf-8" });
9139
9253
  if (ssResult.status === 0 && ssResult.stdout) {
9140
9254
  return parseSsListeningPorts(ssResult.stdout, new Set(pids));
9141
9255
  }
@@ -9194,7 +9308,7 @@ async function runHostApp(repoPath, app, extraEnv = {}, secretManager, env, work
9194
9308
  app.hostRun.command,
9195
9309
  true
9196
9310
  ) : app.hostRun.command;
9197
- const child = (0, import_node_child_process17.spawn)(spawnCommand, {
9311
+ const child = (0, import_node_child_process18.spawn)(spawnCommand, {
9198
9312
  cwd: commandCwd,
9199
9313
  stdio: "inherit",
9200
9314
  shell: true,
@@ -9562,7 +9676,7 @@ async function execWithAppEnv(options) {
9562
9676
  options.command[0],
9563
9677
  true
9564
9678
  );
9565
- child = (0, import_node_child_process17.spawn)(wrapped, {
9679
+ child = (0, import_node_child_process18.spawn)(wrapped, {
9566
9680
  cwd: deps.repoPath,
9567
9681
  stdio: "inherit",
9568
9682
  shell: true,
@@ -9576,7 +9690,7 @@ async function execWithAppEnv(options) {
9576
9690
  false
9577
9691
  );
9578
9692
  const [cmd, ...wrappedArgs] = wrapped;
9579
- child = (0, import_node_child_process17.spawn)(cmd, wrappedArgs, {
9693
+ child = (0, import_node_child_process18.spawn)(cmd, wrappedArgs, {
9580
9694
  cwd: deps.repoPath,
9581
9695
  stdio: "inherit",
9582
9696
  shell: false,
@@ -9584,7 +9698,7 @@ async function execWithAppEnv(options) {
9584
9698
  });
9585
9699
  }
9586
9700
  } else if (options.shell) {
9587
- child = (0, import_node_child_process17.spawn)(options.command[0], {
9701
+ child = (0, import_node_child_process18.spawn)(options.command[0], {
9588
9702
  cwd: deps.repoPath,
9589
9703
  stdio: "inherit",
9590
9704
  shell: true,
@@ -9592,7 +9706,7 @@ async function execWithAppEnv(options) {
9592
9706
  });
9593
9707
  } else {
9594
9708
  const [command, ...args] = options.command;
9595
- child = (0, import_node_child_process17.spawn)(command, args, {
9709
+ child = (0, import_node_child_process18.spawn)(command, args, {
9596
9710
  cwd: deps.repoPath,
9597
9711
  stdio: "inherit",
9598
9712
  shell: false,
@@ -9613,11 +9727,11 @@ async function execWithAppEnv(options) {
9613
9727
  deps.stopDeps();
9614
9728
  }
9615
9729
  }
9616
- var import_node_child_process17, import_node_net, import_node_process, import_promises, POLL_INTERVAL_MS2, DEFAULT_PORT_TIMEOUT_MS, PROCESS_TERMINATION_GRACE_MS;
9730
+ var import_node_child_process18, import_node_net, import_node_process, import_promises, POLL_INTERVAL_MS2, DEFAULT_PORT_TIMEOUT_MS, PROCESS_TERMINATION_GRACE_MS;
9617
9731
  var init_app_run = __esm({
9618
9732
  "src/core/app-run.ts"() {
9619
9733
  "use strict";
9620
- import_node_child_process17 = require("child_process");
9734
+ import_node_child_process18 = require("child_process");
9621
9735
  import_node_net = __toESM(require("net"));
9622
9736
  import_node_process = require("process");
9623
9737
  import_promises = require("readline/promises");
@@ -9878,7 +9992,7 @@ var init_version = __esm({
9878
9992
 
9879
9993
  // src/cli.ts
9880
9994
  var import_commander = require("commander");
9881
- var CLI_VERSION = true ? "0.0.32" : "0.0.0-dev";
9995
+ var CLI_VERSION = true ? "0.0.34" : "0.0.0-dev";
9882
9996
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
9883
9997
  function withErrorHandling(action2) {
9884
9998
  return async (...args) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devrouter/cli",
3
- "version": "0.0.32",
3
+ "version": "0.0.34",
4
4
  "description": "Local dev routing CLI with shared Traefik reverse proxy",
5
5
  "author": "Roland Schlaefli",
6
6
  "homepage": "https://github.com/rschlaefli/devrouter#readme",
@@ -0,0 +1,21 @@
1
+ # Upgrade to devrouter 0.0.33
2
+
3
+ Devrouter now hides a known DevPod transport diagnostic emitted after a one-shot command has completed and devrouter has already recovered its real exit status. Real command stderr and exact non-zero statuses remain unchanged.
4
+
5
+ 1. Install `@devrouter/cli@0.0.33` on the host and bump `.devrouter.yml` to `devrouter.version: 0.0.33`.
6
+ 2. Keep the `0.0.32` lifecycle adoption unchanged: use `ensure` for startup, `stop` for a non-destructive pause, and `exec` for one-shot commands.
7
+ 3. Run `devrouter repo agents --repo <checkout>` only if the repository has not already adopted the `0.0.32` lifecycle guidance.
8
+
9
+ Verification:
10
+
11
+ - Run `devrouter exec <checkout> -- true` and confirm it exits `0` without a transport diagnostic.
12
+ - Run a command that exits `7` and confirm devrouter returns `7` without a transport diagnostic.
13
+ - Write a line to command stderr and confirm that line remains visible.
14
+
15
+ Report template:
16
+
17
+ - CLI/config version: `0.0.33`
18
+ - Successful exec: `<exit status and stderr>`
19
+ - Non-zero exec: `<exit status and stderr>`
20
+ - Real command stderr: `<preserved or failure details>`
21
+ - Remaining blockers: `<none or details>`
@@ -0,0 +1,28 @@
1
+ # Upgrade to devrouter 0.0.34
2
+
3
+ Devrouter now supplies its managed process helper at reconciliation time. Consumer images must not download, install, extract, or version-pin devrouter; `.devrouter.yml` is the only consumer-side devrouter version pin.
4
+
5
+ 1. Install `@devrouter/cli@0.0.34` on the host and bump `.devrouter.yml` to `devrouter.version: 0.0.34`.
6
+ 2. In a devrouter-managed `.devcontainer/post-start.sh`, keep the repository-owned environment and application command, require `DEVROUTER_PROCESS_HELPER`, and replace direct `devrouter-process ensure` with `"$DEVROUTER_PROCESS_HELPER" ensure`.
7
+ 3. Remove `postStartCommand` from `.devcontainer/devcontainer.json`. Canonical `devrouter ensure` delivers the helper and invokes the managed adapter after exact-container proof.
8
+ 4. Remove any `@devrouter/cli` download or `devrouter-process` installation/extraction from the consumer Dockerfile. Retain `procps` and `util-linux`; remove `tar` if the application does not otherwise need it.
9
+ 5. Run `devrouter repo agents --repo <checkout>` to refresh generated guidance.
10
+
11
+ Apply the adapter, devcontainer JSON, and Dockerfile migration atomically. A fully legacy image-plus-hook scaffold remains temporarily supported, but a legacy adapter paired with a helper-free image fails before DevPod startup with migration guidance.
12
+
13
+ Verification:
14
+
15
+ - Run `devrouter repo devcontainer verify --repo <checkout> --json` and confirm `repo.devcontainer.no-devrouter-image-install` is `ok`.
16
+ - Recreate from the helper-free Dockerfile, then run `devrouter ensure <checkout> --json` twice and confirm cold startup plus warm reuse succeed.
17
+ - Run `devrouter exec <checkout> -- sh -c 'test ! -e /usr/local/bin/devrouter-process'` to prove the helper is absent from the image.
18
+ - Run `devrouter exec <checkout> -- sh -c 'test -x /tmp/devrouter/bin/devrouter-process'` to prove the matching helper was delivered only at runtime.
19
+
20
+ Report template:
21
+
22
+ - CLI/config version: `0.0.34`
23
+ - Adapter migration: `<file changed or not applicable>`
24
+ - Image coupling diagnostic: `<ok or details>`
25
+ - Cold/warm ensure: `<DevPod id and route evidence>`
26
+ - Image helper absent: `<passed or failure details>`
27
+ - Runtime helper present: `<passed or failure details>`
28
+ - Remaining blockers: `<none or details>`