@devrouter/cli 0.0.48 → 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.48" : "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.48" : "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, {
@@ -9741,7 +9741,7 @@ function inspectRouterApi(protocol) {
9741
9741
  "--fail",
9742
9742
  "--max-time",
9743
9743
  "2",
9744
- `${ROUTER_API_BASE}/${protocol}/routers`
9744
+ `${ROUTER_API_BASE}/${protocol}/routers?per_page=${ROUTER_API_PAGE_SIZE}`
9745
9745
  ],
9746
9746
  { encoding: "utf-8" }
9747
9747
  );
@@ -9762,6 +9762,7 @@ function inspectRouterApi(protocol) {
9762
9762
  }
9763
9763
  return {
9764
9764
  ok: true,
9765
+ reachedPageLimit: value.length >= ROUTER_API_PAGE_SIZE,
9765
9766
  names: new Set(
9766
9767
  value.flatMap((item) => {
9767
9768
  if (!item || typeof item !== "object") return [];
@@ -9771,8 +9772,8 @@ function inspectRouterApi(protocol) {
9771
9772
  )
9772
9773
  };
9773
9774
  }
9774
- function inspectExpectedRoutes(routes) {
9775
- const missing = [];
9775
+ function inspectExpectedRoutes(routes, expectation) {
9776
+ const mismatched = [];
9776
9777
  const failures = [];
9777
9778
  for (const protocol of ["http", "tcp"]) {
9778
9779
  const expected = routes.filter((route) => (route.protocol ?? "http") === protocol).map((route) => buildHostRouteRouterName(route.repoPath, route.name));
@@ -9780,22 +9781,34 @@ function inspectExpectedRoutes(routes) {
9780
9781
  const result = inspectRouterApi(protocol);
9781
9782
  if (!result.ok) {
9782
9783
  failures.push(result.details);
9783
- missing.push(...expected);
9784
+ mismatched.push(...expected);
9784
9785
  continue;
9785
9786
  }
9786
- missing.push(...expected.filter((name) => !result.names.has(name)));
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) {
9792
+ failures.push(
9793
+ `${protocol.toUpperCase()} router API reached the ${ROUTER_API_PAGE_SIZE}-entry safety limit`
9794
+ );
9795
+ }
9796
+ mismatched.push(...unexpected);
9797
+ if (expectation === "removed" && unexpected.length === 0 && result.reachedPageLimit) {
9798
+ mismatched.push(...expected);
9799
+ }
9787
9800
  }
9788
- return missing.length === 0 ? { ok: true } : {
9801
+ return mismatched.length === 0 ? { ok: true } : {
9789
9802
  ok: false,
9790
- missing,
9791
- 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"
9792
9805
  };
9793
9806
  }
9794
- async function waitForExpectedRoutes(routes, timeoutMs, pollIntervalMs) {
9807
+ async function waitForExpectedRoutes(routes, expectation, timeoutMs, pollIntervalMs) {
9795
9808
  const deadline = Date.now() + timeoutMs;
9796
9809
  let result;
9797
9810
  do {
9798
- result = inspectExpectedRoutes(routes);
9811
+ result = inspectExpectedRoutes(routes, expectation);
9799
9812
  if (result.ok) return result;
9800
9813
  if (Date.now() < deadline) {
9801
9814
  await sleep(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())));
@@ -9803,12 +9816,17 @@ async function waitForExpectedRoutes(routes, timeoutMs, pollIntervalMs) {
9803
9816
  } while (Date.now() < deadline);
9804
9817
  return result;
9805
9818
  }
9806
- async function ensureTraefikRoutesLoaded(routes, options = {}) {
9819
+ async function ensureTraefikRouteExpectation(routes, expectation, options) {
9807
9820
  if (routes.length === 0) return { restarted: false };
9808
9821
  const initialTimeoutMs = options.initialTimeoutMs ?? DEFAULT_INITIAL_TIMEOUT_MS;
9809
9822
  const recoveryTimeoutMs = options.recoveryTimeoutMs ?? DEFAULT_RECOVERY_TIMEOUT_MS;
9810
9823
  const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
9811
- const initial = await waitForExpectedRoutes(routes, initialTimeoutMs, pollIntervalMs);
9824
+ const initial = await waitForExpectedRoutes(
9825
+ routes,
9826
+ expectation,
9827
+ initialTimeoutMs,
9828
+ pollIntervalMs
9829
+ );
9812
9830
  if (initial.ok) return { restarted: false };
9813
9831
  import_node_fs24.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
9814
9832
  return withFileLock(
@@ -9820,18 +9838,30 @@ async function ensureTraefikRoutesLoaded(routes, options = {}) {
9820
9838
  onWait: createStderrWaitReporter("Traefik route reload recovery", "shared router")
9821
9839
  },
9822
9840
  async () => {
9823
- const recheck = await waitForExpectedRoutes(routes, 0, pollIntervalMs);
9841
+ const recheck = await waitForExpectedRoutes(routes, expectation, 0, pollIntervalMs);
9824
9842
  if (recheck.ok) return { restarted: false };
9825
9843
  restartRouterStack();
9826
- const recovered = await waitForExpectedRoutes(routes, recoveryTimeoutMs, pollIntervalMs);
9844
+ const recovered = await waitForExpectedRoutes(
9845
+ routes,
9846
+ expectation,
9847
+ recoveryTimeoutMs,
9848
+ pollIntervalMs
9849
+ );
9827
9850
  if (recovered.ok) return { restarted: true };
9851
+ const action2 = expectation === "loaded" ? "load" : "remove";
9828
9852
  throw new Error(
9829
- `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`
9830
9854
  );
9831
9855
  }
9832
9856
  );
9833
9857
  }
9834
- 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;
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
+ }
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;
9835
9865
  var init_traefik_route_health = __esm({
9836
9866
  "src/core/traefik-route-health.ts"() {
9837
9867
  "use strict";
@@ -9846,6 +9876,7 @@ var init_traefik_route_health = __esm({
9846
9876
  DEFAULT_INITIAL_TIMEOUT_MS = 3e3;
9847
9877
  DEFAULT_RECOVERY_TIMEOUT_MS = 1e4;
9848
9878
  DEFAULT_POLL_INTERVAL_MS = 250;
9879
+ ROUTER_API_PAGE_SIZE = 1e3;
9849
9880
  }
9850
9881
  });
9851
9882
 
@@ -9987,6 +10018,13 @@ function routeInputFromState(route) {
9987
10018
  workspace: route.workspace
9988
10019
  };
9989
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
+ }
9990
10028
  function isWildcard2(values) {
9991
10029
  return values?.length === 1 && values[0] === "*";
9992
10030
  }
@@ -10298,6 +10336,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
10298
10336
  let managedComposeProject;
10299
10337
  let managedContainer;
10300
10338
  let previousRoutes = [];
10339
+ let candidateRoutes = [];
10301
10340
  let candidateRoutesPublished = false;
10302
10341
  let transitionPhase = "validation";
10303
10342
  let managedProcessRegistry = [];
@@ -10327,6 +10366,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
10327
10366
  options.profile
10328
10367
  );
10329
10368
  const managedRuntime = runtime.config.managedRuntime;
10369
+ previousRoutes = listHostRouteState().filter(
10370
+ (route) => sameWorkspacePath(route.repoPath, repoPath)
10371
+ );
10330
10372
  managedRuntimeConfig = managedRuntime ? runtime.config : void 0;
10331
10373
  managedProcessRegistry = managedRuntime?.processes ?? [];
10332
10374
  const desiredProcesses = managedRuntime ? desiredManagedProcesses(managedRuntime, runtime.resolvedProfile) : [];
@@ -10355,9 +10397,6 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
10355
10397
  )) {
10356
10398
  throw new Error("Managed runtime state contains an unregistered process marker.");
10357
10399
  }
10358
- previousRoutes = listHostRouteState().filter(
10359
- (route) => sameWorkspacePath(route.repoPath, repoPath)
10360
- );
10361
10400
  if (managedPostStart.kind !== "runtime") {
10362
10401
  throw new Error(
10363
10402
  "managedRuntime requires the runtime managed post-start adapter for exact process reconciliation."
@@ -10548,8 +10587,13 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
10548
10587
  runtime.config,
10549
10588
  target.workspace
10550
10589
  );
10590
+ candidateRoutes = publication.routes;
10551
10591
  candidateRoutesPublished = true;
10552
10592
  await ensureTraefikRoutesLoaded(publication.routes, routeLoadOptions);
10593
+ await ensureTraefikRoutesRemoved(
10594
+ removedRoutesForReplacement(previousRoutes.map(routeInputFromState), publication.routes),
10595
+ routeLoadOptions
10596
+ );
10553
10597
  try {
10554
10598
  await waitForHttpRoutes(
10555
10599
  repoPath,
@@ -10561,6 +10605,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
10561
10605
  throw error;
10562
10606
  }
10563
10607
  replaceHostRoutesForRepo(repoPath, []);
10608
+ await ensureTraefikRoutesRemoved(candidateRoutes, routeLoadOptions);
10564
10609
  if (!target.hadExactDevpod || recreated) {
10565
10610
  throw error;
10566
10611
  }
@@ -10750,6 +10795,10 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
10750
10795
  const rollbackRoutes = previousRoutes.map(routeInputFromState);
10751
10796
  replaceHostRoutesForRepo(repoPath, rollbackRoutes);
10752
10797
  await ensureTraefikRoutesLoaded(rollbackRoutes, routeLoadOptions);
10798
+ await ensureTraefikRoutesRemoved(
10799
+ removedRoutesForReplacement(candidateRoutes, rollbackRoutes),
10800
+ routeLoadOptions
10801
+ );
10753
10802
  } catch (rollbackError) {
10754
10803
  rollbackErrors.push(
10755
10804
  `routes: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
@@ -10795,6 +10844,10 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
10795
10844
  const rollbackRoutes = previousRoutes.map(routeInputFromState);
10796
10845
  replaceHostRoutesForRepo(repoPath, rollbackRoutes);
10797
10846
  await ensureTraefikRoutesLoaded(rollbackRoutes, routeLoadOptions);
10847
+ await ensureTraefikRoutesRemoved(
10848
+ removedRoutesForReplacement(candidateRoutes, rollbackRoutes),
10849
+ routeLoadOptions
10850
+ );
10798
10851
  } catch (rollbackError) {
10799
10852
  rollbackErrors.push(
10800
10853
  `routes: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
@@ -10813,6 +10866,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
10813
10866
  if (environmentStarted) {
10814
10867
  try {
10815
10868
  replaceHostRoutesForRepo(repoPath, []);
10869
+ await ensureTraefikRoutesRemoved(candidateRoutes, routeLoadOptions);
10816
10870
  } catch (cleanupError) {
10817
10871
  const original = error instanceof Error ? error.message : String(error);
10818
10872
  const cleanup = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
@@ -11181,12 +11235,13 @@ function workspaceLs(repoPath) {
11181
11235
  };
11182
11236
  });
11183
11237
  }
11184
- function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = false) {
11238
+ async function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = false) {
11185
11239
  const devpods = listDevpodWorkspaces(resolved.worktreePath);
11186
11240
  assertDevpodTargetSafe(resolved, worktrees, devpods);
11187
11241
  const devpodId = resolved.record?.devpodId ?? resolved.workspace;
11188
11242
  const mutation = action2 === "stop" ? stopOwnedDevpodWorkspace(devpodId, resolved.worktreePath) : deleteOwnedDevpodWorkspace(devpodId, resolved.worktreePath);
11189
11243
  const routes = removeWorkspaceRoutesForWorktree(resolved.workspace, resolved.worktreePath);
11244
+ await ensureTraefikRoutesRemoved(routes);
11190
11245
  if (!quiet) {
11191
11246
  process.stdout.write(
11192
11247
  `Freed ${routes.length} route(s) for workspace '${resolved.workspace}'.
@@ -11210,7 +11265,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
11210
11265
  if (removeWorktree) {
11211
11266
  assertFullDownPreflight(mainRepo, resolved);
11212
11267
  }
11213
- const result = mutateWorkspaceRuntime(
11268
+ const result = await mutateWorkspaceRuntime(
11214
11269
  action2 === "stop" ? "stop" : "delete",
11215
11270
  resolved,
11216
11271
  worktrees,
@@ -11282,6 +11337,7 @@ var init_workspace_lifecycle = __esm({
11282
11337
  init_devpod_workspaces();
11283
11338
  init_repo_config();
11284
11339
  init_route_state();
11340
+ init_traefik_route_health();
11285
11341
  init_workspace();
11286
11342
  init_workspace_ensure();
11287
11343
  init_workspace_ownership();
@@ -11333,6 +11389,7 @@ async function environmentStop(repoPath, options = {}) {
11333
11389
  const removedRoutes = removeHostRoutesWhere(
11334
11390
  (route) => sameWorkspacePath(route.repoPath, repoPath)
11335
11391
  );
11392
+ await ensureTraefikRoutesRemoved(removedRoutes);
11336
11393
  return {
11337
11394
  kind: linked ? "linked" : "primary",
11338
11395
  repoPath,
@@ -11350,6 +11407,7 @@ var init_environment_stop = __esm({
11350
11407
  init_devpod_mutation();
11351
11408
  init_devpod_workspaces();
11352
11409
  init_host_routes();
11410
+ init_traefik_route_health();
11353
11411
  init_workspace();
11354
11412
  init_workspace_lifecycle();
11355
11413
  init_workspace_ownership();
@@ -15502,7 +15560,7 @@ var init_version = __esm({
15502
15560
 
15503
15561
  // src/cli.ts
15504
15562
  var import_commander = require("commander");
15505
- var CLI_VERSION = true ? "0.0.48" : "0.0.0-dev";
15563
+ var CLI_VERSION = true ? "0.0.50" : "0.0.0-dev";
15506
15564
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
15507
15565
  function withErrorHandling(action2) {
15508
15566
  return async (...args) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devrouter/cli",
3
- "version": "0.0.48",
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.49
2
+
3
+ Devrouter now checks a bounded 1,000-router view from both Traefik router APIs
4
+ when proving managed routes. This prevents Traefik's default 100-entry page
5
+ from hiding a valid route in a large shared installation.
6
+
7
+ 1. Install `@devrouter/cli@0.0.49` on the host and bump `.devrouter.yml` to
8
+ `devrouter.version: 0.0.49`.
9
+ 2. Keep using `devrouter ensure <checkout>`. Route naming, publication, and
10
+ restart-once recovery are unchanged.
11
+ 3. If an expected router is absent from a full 1,000-entry API response,
12
+ Devrouter intentionally fails closed because the API view may be incomplete.
13
+
14
+ Verification:
15
+
16
+ - Run `devrouter -V --repo <checkout>` and confirm the installed CLI and local
17
+ repository report `0.0.49`.
18
+ - Start one safe representative managed profile with `devrouter ensure`, verify
19
+ its expected routes and zero profile drift, then stop that exact checkout and
20
+ require zero remaining routes.
21
+
22
+ Report template:
23
+
24
+ - CLI/config version: `0.0.49`
25
+ - Representative route proof and stop: `<passed, deferred, or details>`
26
+ - Remaining blockers: `<none or details>`
@@ -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>`