@devrouter/cli 0.0.49 → 0.0.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/devrouter.js CHANGED
@@ -2668,7 +2668,7 @@ function loadRepoConfig(repoPath) {
2668
2668
  const config = parseConfig(parsed ?? {}, configPath);
2669
2669
  const requiredVersion = config.devrouter?.version;
2670
2670
  if (requiredVersion && !hasWarnedVersionMismatch) {
2671
- const cliVersion = true ? "0.0.49" : "0.0.0-dev";
2671
+ const cliVersion = true ? "0.0.50" : "0.0.0-dev";
2672
2672
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
2673
2673
  hasWarnedVersionMismatch = true;
2674
2674
  process.stderr.write(
@@ -8986,7 +8986,7 @@ async function buildDoctorReport(options = {}) {
8986
8986
  const config = runtimeConfig.config;
8987
8987
  loadedConfig = config;
8988
8988
  loadedWorkspace = runtimeConfig.workspace;
8989
- const cliVersion = true ? "0.0.49" : "0.0.0-dev";
8989
+ const cliVersion = true ? "0.0.50" : "0.0.0-dev";
8990
8990
  const configVersion = config.devrouter?.version;
8991
8991
  if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
8992
8992
  addCheck(checks, {
@@ -9772,8 +9772,8 @@ function inspectRouterApi(protocol) {
9772
9772
  )
9773
9773
  };
9774
9774
  }
9775
- function inspectExpectedRoutes(routes) {
9776
- const missing = [];
9775
+ function inspectExpectedRoutes(routes, expectation) {
9776
+ const mismatched = [];
9777
9777
  const failures = [];
9778
9778
  for (const protocol of ["http", "tcp"]) {
9779
9779
  const expected = routes.filter((route) => (route.protocol ?? "http") === protocol).map((route) => buildHostRouteRouterName(route.repoPath, route.name));
@@ -9781,28 +9781,34 @@ function inspectExpectedRoutes(routes) {
9781
9781
  const result = inspectRouterApi(protocol);
9782
9782
  if (!result.ok) {
9783
9783
  failures.push(result.details);
9784
- missing.push(...expected);
9784
+ mismatched.push(...expected);
9785
9785
  continue;
9786
9786
  }
9787
- const absent = expected.filter((name) => !result.names.has(name));
9788
- if (absent.length > 0 && result.reachedPageLimit) {
9787
+ const unexpected = expected.filter(
9788
+ (name) => expectation === "loaded" ? !result.names.has(name) : result.names.has(name)
9789
+ );
9790
+ const boundedAbsenceIsUncertain = result.reachedPageLimit && (expectation === "loaded" ? unexpected.length > 0 : unexpected.length === 0);
9791
+ if (boundedAbsenceIsUncertain) {
9789
9792
  failures.push(
9790
9793
  `${protocol.toUpperCase()} router API reached the ${ROUTER_API_PAGE_SIZE}-entry safety limit`
9791
9794
  );
9792
9795
  }
9793
- missing.push(...absent);
9796
+ mismatched.push(...unexpected);
9797
+ if (expectation === "removed" && unexpected.length === 0 && result.reachedPageLimit) {
9798
+ mismatched.push(...expected);
9799
+ }
9794
9800
  }
9795
- return missing.length === 0 ? { ok: true } : {
9801
+ return mismatched.length === 0 ? { ok: true } : {
9796
9802
  ok: false,
9797
- missing,
9798
- details: failures.length > 0 ? failures.join("; ") : "router names are absent"
9803
+ routes: mismatched,
9804
+ details: failures.length > 0 ? failures.join("; ") : expectation === "loaded" ? "router names are absent" : "router names are still present"
9799
9805
  };
9800
9806
  }
9801
- async function waitForExpectedRoutes(routes, timeoutMs, pollIntervalMs) {
9807
+ async function waitForExpectedRoutes(routes, expectation, timeoutMs, pollIntervalMs) {
9802
9808
  const deadline = Date.now() + timeoutMs;
9803
9809
  let result;
9804
9810
  do {
9805
- result = inspectExpectedRoutes(routes);
9811
+ result = inspectExpectedRoutes(routes, expectation);
9806
9812
  if (result.ok) return result;
9807
9813
  if (Date.now() < deadline) {
9808
9814
  await sleep(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())));
@@ -9810,12 +9816,17 @@ async function waitForExpectedRoutes(routes, timeoutMs, pollIntervalMs) {
9810
9816
  } while (Date.now() < deadline);
9811
9817
  return result;
9812
9818
  }
9813
- async function ensureTraefikRoutesLoaded(routes, options = {}) {
9819
+ async function ensureTraefikRouteExpectation(routes, expectation, options) {
9814
9820
  if (routes.length === 0) return { restarted: false };
9815
9821
  const initialTimeoutMs = options.initialTimeoutMs ?? DEFAULT_INITIAL_TIMEOUT_MS;
9816
9822
  const recoveryTimeoutMs = options.recoveryTimeoutMs ?? DEFAULT_RECOVERY_TIMEOUT_MS;
9817
9823
  const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
9818
- const initial = await waitForExpectedRoutes(routes, initialTimeoutMs, pollIntervalMs);
9824
+ const initial = await waitForExpectedRoutes(
9825
+ routes,
9826
+ expectation,
9827
+ initialTimeoutMs,
9828
+ pollIntervalMs
9829
+ );
9819
9830
  if (initial.ok) return { restarted: false };
9820
9831
  import_node_fs24.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
9821
9832
  return withFileLock(
@@ -9827,17 +9838,29 @@ async function ensureTraefikRoutesLoaded(routes, options = {}) {
9827
9838
  onWait: createStderrWaitReporter("Traefik route reload recovery", "shared router")
9828
9839
  },
9829
9840
  async () => {
9830
- const recheck = await waitForExpectedRoutes(routes, 0, pollIntervalMs);
9841
+ const recheck = await waitForExpectedRoutes(routes, expectation, 0, pollIntervalMs);
9831
9842
  if (recheck.ok) return { restarted: false };
9832
9843
  restartRouterStack();
9833
- const recovered = await waitForExpectedRoutes(routes, recoveryTimeoutMs, pollIntervalMs);
9844
+ const recovered = await waitForExpectedRoutes(
9845
+ routes,
9846
+ expectation,
9847
+ recoveryTimeoutMs,
9848
+ pollIntervalMs
9849
+ );
9834
9850
  if (recovered.ok) return { restarted: true };
9851
+ const action2 = expectation === "loaded" ? "load" : "remove";
9835
9852
  throw new Error(
9836
- `Traefik did not load file-provider routes after one restart: ${recovered.missing.join(", ")} (${recovered.details}). Inspect: devrouter logs --tail 100`
9853
+ `Traefik did not ${action2} file-provider routes after one restart: ${recovered.routes.join(", ")} (${recovered.details}). Inspect: devrouter logs --tail 100`
9837
9854
  );
9838
9855
  }
9839
9856
  );
9840
9857
  }
9858
+ async function ensureTraefikRoutesLoaded(routes, options = {}) {
9859
+ return ensureTraefikRouteExpectation(routes, "loaded", options);
9860
+ }
9861
+ async function ensureTraefikRoutesRemoved(routes, options = {}) {
9862
+ return ensureTraefikRouteExpectation(routes, "removed", options);
9863
+ }
9841
9864
  var import_node_child_process20, import_node_fs24, import_node_path25, ROUTER_API_BASE, ROUTER_RELOAD_LOCK_FILE, DEFAULT_INITIAL_TIMEOUT_MS, DEFAULT_RECOVERY_TIMEOUT_MS, DEFAULT_POLL_INTERVAL_MS, ROUTER_API_PAGE_SIZE;
9842
9865
  var init_traefik_route_health = __esm({
9843
9866
  "src/core/traefik-route-health.ts"() {
@@ -9995,6 +10018,13 @@ function routeInputFromState(route) {
9995
10018
  workspace: route.workspace
9996
10019
  };
9997
10020
  }
10021
+ function routeReferenceKey(route) {
10022
+ return `${route.repoPath}\0${route.name}\0${route.protocol ?? "http"}`;
10023
+ }
10024
+ function removedRoutesForReplacement(previousRoutes, nextRoutes) {
10025
+ const nextKeys = new Set(nextRoutes.map(routeReferenceKey));
10026
+ return previousRoutes.filter((route) => !nextKeys.has(routeReferenceKey(route)));
10027
+ }
9998
10028
  function isWildcard2(values) {
9999
10029
  return values?.length === 1 && values[0] === "*";
10000
10030
  }
@@ -10306,6 +10336,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
10306
10336
  let managedComposeProject;
10307
10337
  let managedContainer;
10308
10338
  let previousRoutes = [];
10339
+ let candidateRoutes = [];
10309
10340
  let candidateRoutesPublished = false;
10310
10341
  let transitionPhase = "validation";
10311
10342
  let managedProcessRegistry = [];
@@ -10335,6 +10366,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
10335
10366
  options.profile
10336
10367
  );
10337
10368
  const managedRuntime = runtime.config.managedRuntime;
10369
+ previousRoutes = listHostRouteState().filter(
10370
+ (route) => sameWorkspacePath(route.repoPath, repoPath)
10371
+ );
10338
10372
  managedRuntimeConfig = managedRuntime ? runtime.config : void 0;
10339
10373
  managedProcessRegistry = managedRuntime?.processes ?? [];
10340
10374
  const desiredProcesses = managedRuntime ? desiredManagedProcesses(managedRuntime, runtime.resolvedProfile) : [];
@@ -10363,9 +10397,6 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
10363
10397
  )) {
10364
10398
  throw new Error("Managed runtime state contains an unregistered process marker.");
10365
10399
  }
10366
- previousRoutes = listHostRouteState().filter(
10367
- (route) => sameWorkspacePath(route.repoPath, repoPath)
10368
- );
10369
10400
  if (managedPostStart.kind !== "runtime") {
10370
10401
  throw new Error(
10371
10402
  "managedRuntime requires the runtime managed post-start adapter for exact process reconciliation."
@@ -10556,8 +10587,13 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
10556
10587
  runtime.config,
10557
10588
  target.workspace
10558
10589
  );
10590
+ candidateRoutes = publication.routes;
10559
10591
  candidateRoutesPublished = true;
10560
10592
  await ensureTraefikRoutesLoaded(publication.routes, routeLoadOptions);
10593
+ await ensureTraefikRoutesRemoved(
10594
+ removedRoutesForReplacement(previousRoutes.map(routeInputFromState), publication.routes),
10595
+ routeLoadOptions
10596
+ );
10561
10597
  try {
10562
10598
  await waitForHttpRoutes(
10563
10599
  repoPath,
@@ -10569,6 +10605,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
10569
10605
  throw error;
10570
10606
  }
10571
10607
  replaceHostRoutesForRepo(repoPath, []);
10608
+ await ensureTraefikRoutesRemoved(candidateRoutes, routeLoadOptions);
10572
10609
  if (!target.hadExactDevpod || recreated) {
10573
10610
  throw error;
10574
10611
  }
@@ -10758,6 +10795,10 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
10758
10795
  const rollbackRoutes = previousRoutes.map(routeInputFromState);
10759
10796
  replaceHostRoutesForRepo(repoPath, rollbackRoutes);
10760
10797
  await ensureTraefikRoutesLoaded(rollbackRoutes, routeLoadOptions);
10798
+ await ensureTraefikRoutesRemoved(
10799
+ removedRoutesForReplacement(candidateRoutes, rollbackRoutes),
10800
+ routeLoadOptions
10801
+ );
10761
10802
  } catch (rollbackError) {
10762
10803
  rollbackErrors.push(
10763
10804
  `routes: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
@@ -10803,6 +10844,10 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
10803
10844
  const rollbackRoutes = previousRoutes.map(routeInputFromState);
10804
10845
  replaceHostRoutesForRepo(repoPath, rollbackRoutes);
10805
10846
  await ensureTraefikRoutesLoaded(rollbackRoutes, routeLoadOptions);
10847
+ await ensureTraefikRoutesRemoved(
10848
+ removedRoutesForReplacement(candidateRoutes, rollbackRoutes),
10849
+ routeLoadOptions
10850
+ );
10806
10851
  } catch (rollbackError) {
10807
10852
  rollbackErrors.push(
10808
10853
  `routes: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
@@ -10821,6 +10866,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
10821
10866
  if (environmentStarted) {
10822
10867
  try {
10823
10868
  replaceHostRoutesForRepo(repoPath, []);
10869
+ await ensureTraefikRoutesRemoved(candidateRoutes, routeLoadOptions);
10824
10870
  } catch (cleanupError) {
10825
10871
  const original = error instanceof Error ? error.message : String(error);
10826
10872
  const cleanup = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
@@ -11189,12 +11235,13 @@ function workspaceLs(repoPath) {
11189
11235
  };
11190
11236
  });
11191
11237
  }
11192
- function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = false) {
11238
+ async function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = false) {
11193
11239
  const devpods = listDevpodWorkspaces(resolved.worktreePath);
11194
11240
  assertDevpodTargetSafe(resolved, worktrees, devpods);
11195
11241
  const devpodId = resolved.record?.devpodId ?? resolved.workspace;
11196
11242
  const mutation = action2 === "stop" ? stopOwnedDevpodWorkspace(devpodId, resolved.worktreePath) : deleteOwnedDevpodWorkspace(devpodId, resolved.worktreePath);
11197
11243
  const routes = removeWorkspaceRoutesForWorktree(resolved.workspace, resolved.worktreePath);
11244
+ await ensureTraefikRoutesRemoved(routes);
11198
11245
  if (!quiet) {
11199
11246
  process.stdout.write(
11200
11247
  `Freed ${routes.length} route(s) for workspace '${resolved.workspace}'.
@@ -11218,7 +11265,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
11218
11265
  if (removeWorktree) {
11219
11266
  assertFullDownPreflight(mainRepo, resolved);
11220
11267
  }
11221
- const result = mutateWorkspaceRuntime(
11268
+ const result = await mutateWorkspaceRuntime(
11222
11269
  action2 === "stop" ? "stop" : "delete",
11223
11270
  resolved,
11224
11271
  worktrees,
@@ -11290,6 +11337,7 @@ var init_workspace_lifecycle = __esm({
11290
11337
  init_devpod_workspaces();
11291
11338
  init_repo_config();
11292
11339
  init_route_state();
11340
+ init_traefik_route_health();
11293
11341
  init_workspace();
11294
11342
  init_workspace_ensure();
11295
11343
  init_workspace_ownership();
@@ -11341,6 +11389,7 @@ async function environmentStop(repoPath, options = {}) {
11341
11389
  const removedRoutes = removeHostRoutesWhere(
11342
11390
  (route) => sameWorkspacePath(route.repoPath, repoPath)
11343
11391
  );
11392
+ await ensureTraefikRoutesRemoved(removedRoutes);
11344
11393
  return {
11345
11394
  kind: linked ? "linked" : "primary",
11346
11395
  repoPath,
@@ -11358,6 +11407,7 @@ var init_environment_stop = __esm({
11358
11407
  init_devpod_mutation();
11359
11408
  init_devpod_workspaces();
11360
11409
  init_host_routes();
11410
+ init_traefik_route_health();
11361
11411
  init_workspace();
11362
11412
  init_workspace_lifecycle();
11363
11413
  init_workspace_ownership();
@@ -15510,7 +15560,7 @@ var init_version = __esm({
15510
15560
 
15511
15561
  // src/cli.ts
15512
15562
  var import_commander = require("commander");
15513
- var CLI_VERSION = true ? "0.0.49" : "0.0.0-dev";
15563
+ var CLI_VERSION = true ? "0.0.50" : "0.0.0-dev";
15514
15564
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
15515
15565
  function withErrorHandling(action2) {
15516
15566
  return async (...args) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devrouter/cli",
3
- "version": "0.0.49",
3
+ "version": "0.0.50",
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,26 @@
1
+ # Upgrade to devrouter 0.0.50
2
+
3
+ Devrouter now proves that Traefik unloaded exact HTTP and TCP routers removed
4
+ by stop, down, managed profile replacement, or rollback. A stale router gets
5
+ the same serialized Traefik-only restart used for publication recovery.
6
+
7
+ 1. Install `@devrouter/cli@0.0.50` on the host and bump `.devrouter.yml` to
8
+ `devrouter.version: 0.0.50`.
9
+ 2. Keep using the existing `devrouter ensure`, `devrouter stop`, and managed
10
+ profile commands. Route naming and configuration are unchanged.
11
+ 3. Treat a cleanup error after one restart as fail-closed evidence that
12
+ Traefik's live router set did not match the canonical route generation.
13
+
14
+ Verification:
15
+
16
+ - Run `devrouter -V --repo <checkout>` and confirm the installed CLI and local
17
+ repository report `0.0.50`.
18
+ - Start one safe representative managed profile, verify its exact routes, then
19
+ stop that exact checkout and require both canonical route count and matching
20
+ Traefik API routers to be zero.
21
+
22
+ Report template:
23
+
24
+ - CLI/config version: `0.0.50`
25
+ - Representative route publication and removal proof: `<passed, deferred, or details>`
26
+ - Remaining blockers: `<none or details>`