@devrouter/cli 0.0.33 → 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.33" : "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.33" : "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();
@@ -6766,7 +6891,7 @@ function ensureGuidance(repoPath) {
6766
6891
  return `Run 'devrouter ensure ${repoPath}' first.`;
6767
6892
  }
6768
6893
  function assertRunningDevpod(devpodId, repoPath) {
6769
- 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"], {
6770
6895
  encoding: "utf-8"
6771
6896
  });
6772
6897
  if (result.status !== 0) {
@@ -6822,7 +6947,7 @@ async function devpodExec(repoPath, command) {
6822
6947
  wrappedCommand
6823
6948
  ];
6824
6949
  return new Promise((resolve, reject) => {
6825
- 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"] });
6826
6951
  let pending = Buffer.alloc(0);
6827
6952
  let statusBytes = Buffer.alloc(0);
6828
6953
  let readingStatus = false;
@@ -6881,11 +7006,11 @@ async function devpodExec(repoPath, command) {
6881
7006
  });
6882
7007
  });
6883
7008
  }
6884
- var import_node_child_process13, import_node_crypto4, DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC;
7009
+ var import_node_child_process14, import_node_crypto4, DEVPOD_MISSING_EXIT_STATUS_DIAGNOSTIC;
6885
7010
  var init_devpod_exec = __esm({
6886
7011
  "src/core/devpod-exec.ts"() {
6887
7012
  "use strict";
6888
- import_node_child_process13 = require("child_process");
7013
+ import_node_child_process14 = require("child_process");
6889
7014
  import_node_crypto4 = require("crypto");
6890
7015
  init_devpod_environment();
6891
7016
  init_devpod_workspaces();
@@ -7070,7 +7195,7 @@ async function runOpenCommand(name) {
7070
7195
  );
7071
7196
  return;
7072
7197
  }
7073
- 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" });
7074
7199
  if (result.status !== 0) {
7075
7200
  const details = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
7076
7201
  throw new Error(`Unable to open '${url}': ${details || "unknown error"}`);
@@ -7078,11 +7203,11 @@ async function runOpenCommand(name) {
7078
7203
  process.stdout.write(`Opened ${url}
7079
7204
  `);
7080
7205
  }
7081
- var import_node_child_process14;
7206
+ var import_node_child_process15;
7082
7207
  var init_open = __esm({
7083
7208
  "src/commands/open.ts"() {
7084
7209
  "use strict";
7085
- import_node_child_process14 = require("child_process");
7210
+ import_node_child_process15 = require("child_process");
7086
7211
  init_docker();
7087
7212
  init_host_routes();
7088
7213
  init_repo_config();
@@ -7105,7 +7230,7 @@ async function runLogsCommand(options) {
7105
7230
  const args = ["logs", "--tail", tail, ROUTER_CONTAINER_NAME];
7106
7231
  if (options.follow) {
7107
7232
  args.splice(1, 0, "-f");
7108
- 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" });
7109
7234
  const onSignal = () => {
7110
7235
  child.kill("SIGTERM");
7111
7236
  };
@@ -7119,17 +7244,17 @@ async function runLogsCommand(options) {
7119
7244
  });
7120
7245
  });
7121
7246
  } else {
7122
- 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" });
7123
7248
  if (result.status !== 0) {
7124
7249
  throw new Error("Failed to retrieve router logs.");
7125
7250
  }
7126
7251
  }
7127
7252
  }
7128
- var import_node_child_process15;
7253
+ var import_node_child_process16;
7129
7254
  var init_logs = __esm({
7130
7255
  "src/commands/logs.ts"() {
7131
7256
  "use strict";
7132
- import_node_child_process15 = require("child_process");
7257
+ import_node_child_process16 = require("child_process");
7133
7258
  init_docker();
7134
7259
  init_router();
7135
7260
  }
@@ -7169,17 +7294,17 @@ function asRecord3(value) {
7169
7294
  return value;
7170
7295
  }
7171
7296
  function readJson(filePath) {
7172
- if (!import_node_fs17.default.existsSync(filePath)) {
7297
+ if (!import_node_fs18.default.existsSync(filePath)) {
7173
7298
  return void 0;
7174
7299
  }
7175
7300
  try {
7176
- return JSON.parse(import_node_fs17.default.readFileSync(filePath, "utf-8"));
7301
+ return JSON.parse(import_node_fs18.default.readFileSync(filePath, "utf-8"));
7177
7302
  } catch {
7178
7303
  return void 0;
7179
7304
  }
7180
7305
  }
7181
7306
  function relative(repoPath, filePath) {
7182
- return import_node_path17.default.relative(repoPath, filePath) || ".";
7307
+ return import_node_path18.default.relative(repoPath, filePath) || ".";
7183
7308
  }
7184
7309
  function redactEnvAssignments(value) {
7185
7310
  return value.replace(
@@ -7220,7 +7345,7 @@ function inspectPackageManager(repoPath, pkg) {
7220
7345
  ["bun.lock", "bun"]
7221
7346
  ];
7222
7347
  for (const [fileName, name] of lockfiles) {
7223
- 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))) {
7224
7349
  return { name, source: fileName };
7225
7350
  }
7226
7351
  }
@@ -7235,9 +7360,9 @@ function inspectNode(repoPath, pkg) {
7235
7360
  if (typeof engines?.node === "string") {
7236
7361
  return { version: engines.node, source: "package.json:engines.node" };
7237
7362
  }
7238
- const nvmrc = import_node_path17.default.join(repoPath, ".nvmrc");
7239
- if (import_node_fs17.default.existsSync(nvmrc)) {
7240
- 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();
7241
7366
  return { version, source: ".nvmrc" };
7242
7367
  }
7243
7368
  return void 0;
@@ -7298,7 +7423,7 @@ function configuredComposeFiles(repoPath) {
7298
7423
  const files = config.apps.filter(
7299
7424
  (app) => app.runtime === "docker"
7300
7425
  ).flatMap((app) => app.docker.composeFiles).filter(
7301
- (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("..")
7302
7427
  );
7303
7428
  return Array.from(new Set(files));
7304
7429
  } catch {
@@ -7316,7 +7441,7 @@ function composeFiles(repoPath) {
7316
7441
  ...configuredComposeFiles(repoPath)
7317
7442
  ];
7318
7443
  return Array.from(new Set(candidates)).filter(
7319
- (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))
7320
7445
  );
7321
7446
  }
7322
7447
  function stringArray(value) {
@@ -7354,7 +7479,7 @@ function inspectServices(repoPath) {
7354
7479
  const services = [];
7355
7480
  for (const fileName of composeFiles(repoPath)) {
7356
7481
  try {
7357
- 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"));
7358
7483
  const serviceMap = asRecord3(asRecord3(parsed)?.services);
7359
7484
  for (const [name, value] of Object.entries(serviceMap ?? {})) {
7360
7485
  const service = asRecord3(value);
@@ -7389,8 +7514,8 @@ function inspectServices(repoPath) {
7389
7514
  return services;
7390
7515
  }
7391
7516
  function inspectEnvFiles(repoPath) {
7392
- const files = import_node_fs17.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
7393
- 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");
7394
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();
7395
7520
  return { path: fileName, names };
7396
7521
  });
@@ -7404,18 +7529,18 @@ function inspectEnvFiles(repoPath) {
7404
7529
  };
7405
7530
  }
7406
7531
  function inspectDevcontainer(repoPath) {
7407
- const dir = import_node_path17.default.join(repoPath, ".devcontainer");
7408
- 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)) {
7409
7534
  return { exists: false, files: [] };
7410
7535
  }
7411
7536
  return {
7412
7537
  exists: true,
7413
- 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}`)
7414
7539
  };
7415
7540
  }
7416
7541
  function inspectDevrouter(repoPath) {
7417
7542
  const configPath = getRepoConfigPath(repoPath);
7418
- if (!import_node_fs17.default.existsSync(configPath)) {
7543
+ if (!import_node_fs18.default.existsSync(configPath)) {
7419
7544
  return {
7420
7545
  exists: false,
7421
7546
  configPath,
@@ -7459,15 +7584,15 @@ function inspectAgentGuidance(repoPath) {
7459
7584
  ["AGENTS.md", "agents"],
7460
7585
  ["CLAUDE.md", "claude"]
7461
7586
  ]) {
7462
- 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))) {
7463
7588
  results.push({ path: fileName, kind });
7464
7589
  }
7465
7590
  }
7466
- const skillsDir = import_node_path17.default.join(repoPath, ".agents", "skills");
7467
- if (import_node_fs17.default.existsSync(skillsDir)) {
7468
- for (const name of import_node_fs17.default.readdirSync(skillsDir).sort()) {
7469
- const skillPath = import_node_path17.default.join(skillsDir, name, "SKILL.md");
7470
- 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)) {
7471
7596
  results.push({ path: relative(repoPath, skillPath), kind: "skill" });
7472
7597
  }
7473
7598
  }
@@ -7510,7 +7635,7 @@ function buildIssues(report) {
7510
7635
  }
7511
7636
  function inspectRepo(options = {}) {
7512
7637
  const repoPath = resolveRepoPath(options.repo);
7513
- const pkg = readJson(import_node_path17.default.join(repoPath, "package.json"));
7638
+ const pkg = readJson(import_node_path18.default.join(repoPath, "package.json"));
7514
7639
  const scripts = inspectScripts(pkg);
7515
7640
  const reportWithoutIssues = {
7516
7641
  repoPath,
@@ -7529,12 +7654,12 @@ function inspectRepo(options = {}) {
7529
7654
  issues: buildIssues(reportWithoutIssues)
7530
7655
  };
7531
7656
  }
7532
- var import_node_fs17, import_node_path17, import_yaml5;
7657
+ var import_node_fs18, import_node_path18, import_yaml5;
7533
7658
  var init_repo_inspect = __esm({
7534
7659
  "src/core/repo-inspect.ts"() {
7535
7660
  "use strict";
7536
- import_node_fs17 = __toESM(require("fs"));
7537
- import_node_path17 = __toESM(require("path"));
7661
+ import_node_fs18 = __toESM(require("fs"));
7662
+ import_node_path18 = __toESM(require("path"));
7538
7663
  import_yaml5 = __toESM(require("yaml"));
7539
7664
  init_repo_config();
7540
7665
  }
@@ -7639,7 +7764,7 @@ function requiredFileChecks(repoPath) {
7639
7764
  ".devcontainer/docker-compose.yml",
7640
7765
  ".devrouter.yml"
7641
7766
  ];
7642
- 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)));
7643
7768
  return {
7644
7769
  id: "repo.devcontainer.verify-files",
7645
7770
  level: missing.length === 0 ? "ok" : "error",
@@ -7858,12 +7983,12 @@ async function verifyDevcontainer(options = {}) {
7858
7983
  nextSteps: collectNextSteps3(checks)
7859
7984
  };
7860
7985
  }
7861
- var import_node_fs18, import_node_path18;
7986
+ var import_node_fs19, import_node_path19;
7862
7987
  var init_devcontainer_verify = __esm({
7863
7988
  "src/core/devcontainer-verify.ts"() {
7864
7989
  "use strict";
7865
- import_node_fs18 = __toESM(require("fs"));
7866
- import_node_path18 = __toESM(require("path"));
7990
+ import_node_fs19 = __toESM(require("fs"));
7991
+ import_node_path19 = __toESM(require("path"));
7867
7992
  init_capabilities();
7868
7993
  init_doctor();
7869
7994
  init_host_routes();
@@ -7901,30 +8026,22 @@ function inferDevProcess(repo) {
7901
8026
  function inferPort2(repo) {
7902
8027
  return repo.apps.find((app) => app.port)?.port ?? 3e3;
7903
8028
  }
7904
- function renderDockerfile(nodeMajor, pnpmVersion, version) {
8029
+ function renderDockerfile(nodeMajor, pnpmVersion) {
7905
8030
  const pnpmPackageSpec = `pnpm@${pnpmVersion}`;
7906
- const devrouterPackageSpec = `@devrouter/cli@${version}`;
7907
- const devrouterTarball = `devrouter-cli-${version}.tgz`;
7908
- return `# ${MANAGED_MARKER}
8031
+ return `# ${MANAGED_MARKER2}
7909
8032
  FROM node:${nodeMajor}-bookworm-slim
7910
8033
 
7911
8034
  RUN apt-get update \\
7912
- && 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 \\
7913
8036
  && rm -rf /var/lib/apt/lists/*
7914
8037
 
7915
8038
  RUN npm install -g ${shellSingleQuote(pnpmPackageSpec)}
7916
8039
 
7917
- RUN npm pack --silent ${shellSingleQuote(devrouterPackageSpec)} \\
7918
- && tar -xzf ${shellSingleQuote(devrouterTarball)} --strip-components=2 \\
7919
- -C /usr/local/bin package/bin/devrouter-process \\
7920
- && chmod +x /usr/local/bin/devrouter-process \\
7921
- && rm ${shellSingleQuote(devrouterTarball)}
7922
-
7923
8040
  WORKDIR /workspaces/app
7924
8041
  `;
7925
8042
  }
7926
8043
  function renderCompose(projectName) {
7927
- return `# ${MANAGED_MARKER}
8044
+ return `# ${MANAGED_MARKER2}
7928
8045
  services:
7929
8046
  postgres:
7930
8047
  image: postgres:16-alpine
@@ -7977,12 +8094,12 @@ volumes:
7977
8094
  `;
7978
8095
  }
7979
8096
  function renderDefaultComposeOverlay() {
7980
- return `# ${MANAGED_MARKER}
8097
+ return `# ${MANAGED_MARKER2}
7981
8098
  services: {}
7982
8099
  `;
7983
8100
  }
7984
8101
  function renderDevrouterComposeOverlay() {
7985
- return `# ${MANAGED_MARKER}
8102
+ return `# ${MANAGED_MARKER2}
7986
8103
  # A linked worktree's .git file points at the host repository's common Git
7987
8104
  # directory. workspace ensure sets this absolute path before DevPod starts.
7988
8105
  services:
@@ -7998,7 +8115,7 @@ services:
7998
8115
  }
7999
8116
  function renderInitDb() {
8000
8117
  return `#!/usr/bin/env bash
8001
- # ${MANAGED_MARKER}
8118
+ # ${MANAGED_MARKER2}
8002
8119
  set -euo pipefail
8003
8120
 
8004
8121
  psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<'SQL'
@@ -8017,10 +8134,9 @@ function renderDevcontainerJson(projectName) {
8017
8134
  "service": "app",
8018
8135
  "workspaceFolder": "/workspaces/${projectName}",
8019
8136
  "postCreateCommand": "bash .devcontainer/post-create.sh",
8020
- "postStartCommand": "bash .devcontainer/post-start.sh",
8021
8137
  "customizations": {
8022
8138
  "devrouter": {
8023
- "managed": "${MANAGED_MARKER}"
8139
+ "managed": "${MANAGED_MARKER2}"
8024
8140
  },
8025
8141
  "vscode": {
8026
8142
  "extensions": []
@@ -8030,7 +8146,7 @@ function renderDevcontainerJson(projectName) {
8030
8146
  `;
8031
8147
  }
8032
8148
  function renderEnv(projectName, port) {
8033
- return `# ${MANAGED_MARKER}
8149
+ return `# ${MANAGED_MARKER2}
8034
8150
  WORKSPACE=${projectName}
8035
8151
  HOST=0.0.0.0
8036
8152
  HOSTNAME=0.0.0.0
@@ -8041,7 +8157,7 @@ SHADOW_DATABASE_URL=postgres://prisma:prisma@postgres:5432/shadow
8041
8157
  }
8042
8158
  function renderPostCreate() {
8043
8159
  return `#!/usr/bin/env bash
8044
- # ${MANAGED_MARKER}
8160
+ # ${MANAGED_MARKER2}
8045
8161
  set -euo pipefail
8046
8162
 
8047
8163
  export CI=true
@@ -8057,7 +8173,7 @@ function shellSingleQuote(value) {
8057
8173
  }
8058
8174
  function renderPostStart(devProcess) {
8059
8175
  return `#!/usr/bin/env bash
8060
- # ${MANAGED_MARKER}
8176
+ # ${MANAGED_MARKER2}
8061
8177
  set -euo pipefail
8062
8178
 
8063
8179
  export CI=true
@@ -8066,7 +8182,9 @@ set -a
8066
8182
  . .devcontainer/devcontainer.env
8067
8183
  set +a
8068
8184
 
8069
- devrouter-process ensure \\
8185
+ : "\${DEVROUTER_PROCESS_HELPER:?Run devrouter ensure to start this managed application process.}"
8186
+
8187
+ "$DEVROUTER_PROCESS_HELPER" ensure \\
8070
8188
  --name app \\
8071
8189
  --match ${shellSingleQuote(devProcess.match)} \\
8072
8190
  --log /tmp/devrouter-app.log \\
@@ -8074,7 +8192,7 @@ devrouter-process ensure \\
8074
8192
  `;
8075
8193
  }
8076
8194
  function renderDevrouter(projectName, port, version) {
8077
- return `# ${MANAGED_MARKER}
8195
+ return `# ${MANAGED_MARKER2}
8078
8196
  version: 1
8079
8197
  devrouter:
8080
8198
  version: ${version}
@@ -8096,24 +8214,16 @@ apps:
8096
8214
  `;
8097
8215
  }
8098
8216
  function renderReadme(projectName) {
8099
- return `<!-- ${MANAGED_MARKER} -->
8217
+ return `<!-- ${MANAGED_MARKER2} -->
8100
8218
  # Devcontainer
8101
8219
 
8102
8220
  Use this repo through the devcontainer, with devrouter providing stable local routes.
8103
8221
 
8104
- Primary checkout:
8222
+ Primary or linked checkout:
8105
8223
 
8106
8224
  \`\`\`bash
8107
8225
  devrouter setup --yes
8108
- devpod up .
8109
- devrouter app run app --repo . --yes
8110
- devrouter app run db --repo . --yes
8111
- \`\`\`
8112
-
8113
- Existing linked worktree:
8114
-
8115
- \`\`\`bash
8116
- devrouter workspace ensure .
8226
+ devrouter ensure .
8117
8227
  \`\`\`
8118
8228
 
8119
8229
  - App: https://${projectName}.localhost
@@ -8122,18 +8232,9 @@ devrouter workspace ensure .
8122
8232
  }
8123
8233
  function postWriteNextSteps(repoPath) {
8124
8234
  const quotedRepoPath = shellSingleQuote(repoPath);
8125
- if (isLinkedWorktree(repoPath)) {
8126
- return [
8127
- `Run: devrouter setup --repo ${quotedRepoPath} --yes`,
8128
- `Run: devrouter workspace ensure ${quotedRepoPath}`,
8129
- `Optional: devrouter repo agents --repo ${quotedRepoPath}`
8130
- ];
8131
- }
8132
8235
  return [
8133
8236
  `Run: devrouter setup --repo ${quotedRepoPath} --yes`,
8134
- `Run: cd ${quotedRepoPath} && devpod up .`,
8135
- `Run: devrouter app run app --repo ${quotedRepoPath} --yes`,
8136
- `Run: devrouter app run db --repo ${quotedRepoPath} --yes`,
8237
+ `Run: devrouter ensure ${quotedRepoPath}`,
8137
8238
  `Optional: devrouter repo agents --repo ${quotedRepoPath}`
8138
8239
  ];
8139
8240
  }
@@ -8183,7 +8284,7 @@ function packageManagerIssues(repo) {
8183
8284
  }
8184
8285
  function plannedFiles(repoPath, version) {
8185
8286
  const repo = inspectRepo({ repo: repoPath });
8186
- const projectName = sanitizeProjectName(import_node_path19.default.basename(repo.repoPath));
8287
+ const projectName = sanitizeProjectName(import_node_path20.default.basename(repo.repoPath));
8187
8288
  const nodeMajor = majorVersion(repo.node?.version, "24");
8188
8289
  const pnpmVersion = repo.packageManager?.name === "pnpm" && repo.packageManager.version ? repo.packageManager.version : DEFAULT_PNPM_VERSION;
8189
8290
  const port = inferPort2(repo);
@@ -8195,7 +8296,7 @@ function plannedFiles(repoPath, version) {
8195
8296
  files: [
8196
8297
  {
8197
8298
  relativePath: ".devcontainer/Dockerfile",
8198
- content: renderDockerfile(nodeMajor, pnpmVersion, version)
8299
+ content: renderDockerfile(nodeMajor, pnpmVersion)
8199
8300
  },
8200
8301
  { relativePath: ".devcontainer/docker-compose.yml", content: renderCompose(projectName) },
8201
8302
  {
@@ -8228,8 +8329,8 @@ function plannedFiles(repoPath, version) {
8228
8329
  };
8229
8330
  }
8230
8331
  function classifyFile(repoPath, file) {
8231
- const absolutePath = import_node_path19.default.join(repoPath, file.relativePath);
8232
- 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)) {
8233
8334
  return {
8234
8335
  path: file.relativePath,
8235
8336
  action: "create",
@@ -8237,8 +8338,8 @@ function classifyFile(repoPath, file) {
8237
8338
  bytes: Buffer.byteLength(file.content)
8238
8339
  };
8239
8340
  }
8240
- const current = import_node_fs19.default.readFileSync(absolutePath, "utf-8");
8241
- if (!current.includes(MANAGED_MARKER)) {
8341
+ const current = import_node_fs20.default.readFileSync(absolutePath, "utf-8");
8342
+ if (!current.includes(MANAGED_MARKER2)) {
8242
8343
  return {
8243
8344
  path: file.relativePath,
8244
8345
  action: "conflict",
@@ -8294,11 +8395,11 @@ function buildPlan(repoPath, dryRun, version) {
8294
8395
  };
8295
8396
  }
8296
8397
  function writeFile(repoPath, file) {
8297
- const absolutePath = import_node_path19.default.join(repoPath, file.relativePath);
8298
- import_node_fs19.default.mkdirSync(import_node_path19.default.dirname(absolutePath), { recursive: true });
8299
- 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");
8300
8401
  if (file.executable) {
8301
- import_node_fs19.default.chmodSync(absolutePath, 493);
8402
+ import_node_fs20.default.chmodSync(absolutePath, 493);
8302
8403
  }
8303
8404
  }
8304
8405
  function writeDevcontainer(options = {}) {
@@ -8336,16 +8437,15 @@ function writeDevcontainer(options = {}) {
8336
8437
  nextSteps: postWriteNextSteps(repoPath)
8337
8438
  };
8338
8439
  }
8339
- 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;
8340
8441
  var init_devcontainer_write = __esm({
8341
8442
  "src/core/devcontainer-write.ts"() {
8342
8443
  "use strict";
8343
- import_node_fs19 = __toESM(require("fs"));
8344
- import_node_path19 = __toESM(require("path"));
8444
+ import_node_fs20 = __toESM(require("fs"));
8445
+ import_node_path20 = __toESM(require("path"));
8345
8446
  init_repo_config();
8346
8447
  init_repo_inspect();
8347
- init_workspace();
8348
- MANAGED_MARKER = "devrouter:managed devcontainer";
8448
+ MANAGED_MARKER2 = "devrouter:managed devcontainer";
8349
8449
  DEFAULT_DEVROUTER_VERSION = "0.0.0";
8350
8450
  DEFAULT_PNPM_VERSION = "11.6.0";
8351
8451
  VALID_PACKAGE_VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
@@ -8733,7 +8833,7 @@ function sanitizeRouterId(value) {
8733
8833
  return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
8734
8834
  }
8735
8835
  function repoHash(repoPath) {
8736
- 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);
8737
8837
  }
8738
8838
  function asDockerApp(app) {
8739
8839
  return app.runtime === "docker";
@@ -8812,11 +8912,11 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
8812
8912
  if (dockerApps.length === 0) {
8813
8913
  throw new Error("No docker apps selected to prepare compose overlay.");
8814
8914
  }
8815
- const cachePath = import_node_path20.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
8816
- import_node_fs20.default.mkdirSync(cachePath, { recursive: true });
8817
- 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");
8818
8918
  const overlayDocument = buildOverlayDocument(dockerApps, publishTcpPorts);
8819
- 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");
8820
8920
  return {
8821
8921
  overlayPath,
8822
8922
  composeFiles: ensureComposeFiles(dockerApps),
@@ -8830,7 +8930,7 @@ function runDockerComposeUp(repoPath, composeFiles2, overlayPath, services) {
8830
8930
  fileArgs.push("-f", resolved);
8831
8931
  }
8832
8932
  const args = ["compose", ...fileArgs, "-f", overlayPath, "up", "-d", "--wait", ...services];
8833
- const result = (0, import_node_child_process16.spawnSync)("docker", args, {
8933
+ const result = (0, import_node_child_process17.spawnSync)("docker", args, {
8834
8934
  encoding: "utf-8",
8835
8935
  cwd: repoPath
8836
8936
  });
@@ -8858,7 +8958,7 @@ function queryRunningComposeServices(repoPath, composeFiles2, overlayPath, servi
8858
8958
  "--services",
8859
8959
  ...services
8860
8960
  ];
8861
- const result = (0, import_node_child_process16.spawnSync)("docker", args, {
8961
+ const result = (0, import_node_child_process17.spawnSync)("docker", args, {
8862
8962
  encoding: "utf-8",
8863
8963
  cwd: repoPath
8864
8964
  });
@@ -8884,7 +8984,7 @@ function runDockerComposeStop(repoPath, composeFiles2, overlayPath, services) {
8884
8984
  fileArgs.push("-f", resolved);
8885
8985
  }
8886
8986
  const args = ["compose", ...fileArgs, "-f", overlayPath, "stop", ...services];
8887
- const result = (0, import_node_child_process16.spawnSync)("docker", args, {
8987
+ const result = (0, import_node_child_process17.spawnSync)("docker", args, {
8888
8988
  encoding: "utf-8",
8889
8989
  cwd: repoPath
8890
8990
  });
@@ -8910,7 +9010,7 @@ function runDockerComposeLogs(repoPath, composeFiles2, overlayPath, services, ta
8910
9010
  String(tail),
8911
9011
  ...services
8912
9012
  ];
8913
- (0, import_node_child_process16.spawnSync)("docker", args, {
9013
+ (0, import_node_child_process17.spawnSync)("docker", args, {
8914
9014
  stdio: "inherit",
8915
9015
  cwd: repoPath
8916
9016
  });
@@ -8922,7 +9022,7 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
8922
9022
  fileArgs.push("-f", resolved);
8923
9023
  }
8924
9024
  const args = ["compose", ...fileArgs, "-f", overlayPath, "port", service, String(internalPort)];
8925
- const result = (0, import_node_child_process16.spawnSync)("docker", args, {
9025
+ const result = (0, import_node_child_process17.spawnSync)("docker", args, {
8926
9026
  encoding: "utf-8",
8927
9027
  cwd: repoPath
8928
9028
  });
@@ -8936,14 +9036,14 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
8936
9036
  const port = Number(match[1]);
8937
9037
  return Number.isInteger(port) && port > 0 ? port : void 0;
8938
9038
  }
8939
- 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;
8940
9040
  var init_docker_run = __esm({
8941
9041
  "src/core/docker-run.ts"() {
8942
9042
  "use strict";
8943
- import_node_child_process16 = require("child_process");
9043
+ import_node_child_process17 = require("child_process");
8944
9044
  import_node_crypto5 = require("crypto");
8945
- import_node_fs20 = __toESM(require("fs"));
8946
- import_node_path20 = __toESM(require("path"));
9045
+ import_node_fs21 = __toESM(require("fs"));
9046
+ import_node_path21 = __toESM(require("path"));
8947
9047
  import_yaml6 = __toESM(require("yaml"));
8948
9048
  init_docker_error_guidance();
8949
9049
  init_paths();
@@ -9033,7 +9133,7 @@ function isProcessRunning(pid) {
9033
9133
  }
9034
9134
  }
9035
9135
  function readProcessTree(rootPid) {
9036
- 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" });
9037
9137
  if (result.status !== 0) {
9038
9138
  return [rootPid];
9039
9139
  }
@@ -9144,12 +9244,12 @@ function detectListeningPorts(pids) {
9144
9244
  if (pids.length === 0) {
9145
9245
  return [];
9146
9246
  }
9147
- 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(",")], {
9148
9248
  encoding: "utf-8"
9149
9249
  });
9150
9250
  if (result.error && result.error.code === "ENOENT") {
9151
9251
  if (process.platform === "linux") {
9152
- 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" });
9153
9253
  if (ssResult.status === 0 && ssResult.stdout) {
9154
9254
  return parseSsListeningPorts(ssResult.stdout, new Set(pids));
9155
9255
  }
@@ -9208,7 +9308,7 @@ async function runHostApp(repoPath, app, extraEnv = {}, secretManager, env, work
9208
9308
  app.hostRun.command,
9209
9309
  true
9210
9310
  ) : app.hostRun.command;
9211
- const child = (0, import_node_child_process17.spawn)(spawnCommand, {
9311
+ const child = (0, import_node_child_process18.spawn)(spawnCommand, {
9212
9312
  cwd: commandCwd,
9213
9313
  stdio: "inherit",
9214
9314
  shell: true,
@@ -9576,7 +9676,7 @@ async function execWithAppEnv(options) {
9576
9676
  options.command[0],
9577
9677
  true
9578
9678
  );
9579
- child = (0, import_node_child_process17.spawn)(wrapped, {
9679
+ child = (0, import_node_child_process18.spawn)(wrapped, {
9580
9680
  cwd: deps.repoPath,
9581
9681
  stdio: "inherit",
9582
9682
  shell: true,
@@ -9590,7 +9690,7 @@ async function execWithAppEnv(options) {
9590
9690
  false
9591
9691
  );
9592
9692
  const [cmd, ...wrappedArgs] = wrapped;
9593
- child = (0, import_node_child_process17.spawn)(cmd, wrappedArgs, {
9693
+ child = (0, import_node_child_process18.spawn)(cmd, wrappedArgs, {
9594
9694
  cwd: deps.repoPath,
9595
9695
  stdio: "inherit",
9596
9696
  shell: false,
@@ -9598,7 +9698,7 @@ async function execWithAppEnv(options) {
9598
9698
  });
9599
9699
  }
9600
9700
  } else if (options.shell) {
9601
- child = (0, import_node_child_process17.spawn)(options.command[0], {
9701
+ child = (0, import_node_child_process18.spawn)(options.command[0], {
9602
9702
  cwd: deps.repoPath,
9603
9703
  stdio: "inherit",
9604
9704
  shell: true,
@@ -9606,7 +9706,7 @@ async function execWithAppEnv(options) {
9606
9706
  });
9607
9707
  } else {
9608
9708
  const [command, ...args] = options.command;
9609
- child = (0, import_node_child_process17.spawn)(command, args, {
9709
+ child = (0, import_node_child_process18.spawn)(command, args, {
9610
9710
  cwd: deps.repoPath,
9611
9711
  stdio: "inherit",
9612
9712
  shell: false,
@@ -9627,11 +9727,11 @@ async function execWithAppEnv(options) {
9627
9727
  deps.stopDeps();
9628
9728
  }
9629
9729
  }
9630
- 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;
9631
9731
  var init_app_run = __esm({
9632
9732
  "src/core/app-run.ts"() {
9633
9733
  "use strict";
9634
- import_node_child_process17 = require("child_process");
9734
+ import_node_child_process18 = require("child_process");
9635
9735
  import_node_net = __toESM(require("net"));
9636
9736
  import_node_process = require("process");
9637
9737
  import_promises = require("readline/promises");
@@ -9892,7 +9992,7 @@ var init_version = __esm({
9892
9992
 
9893
9993
  // src/cli.ts
9894
9994
  var import_commander = require("commander");
9895
- var CLI_VERSION = true ? "0.0.33" : "0.0.0-dev";
9995
+ var CLI_VERSION = true ? "0.0.34" : "0.0.0-dev";
9896
9996
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
9897
9997
  function withErrorHandling(action2) {
9898
9998
  return async (...args) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devrouter/cli",
3
- "version": "0.0.33",
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,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>`