@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 +74 -24
- package/package.json +1 -1
- package/upgrade-prompts/0.0.50.md +26 -0
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
9784
|
+
mismatched.push(...expected);
|
|
9785
9785
|
continue;
|
|
9786
9786
|
}
|
|
9787
|
-
const
|
|
9788
|
-
|
|
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
|
-
|
|
9796
|
+
mismatched.push(...unexpected);
|
|
9797
|
+
if (expectation === "removed" && unexpected.length === 0 && result.reachedPageLimit) {
|
|
9798
|
+
mismatched.push(...expected);
|
|
9799
|
+
}
|
|
9794
9800
|
}
|
|
9795
|
-
return
|
|
9801
|
+
return mismatched.length === 0 ? { ok: true } : {
|
|
9796
9802
|
ok: false,
|
|
9797
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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.
|
|
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
|
@@ -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>`
|