@devrouter/cli 0.0.59 → 0.0.60
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-lifecycle-worker.js +148 -34
- package/dist/devrouter.js +9117 -7155
- package/package.json +2 -1
- package/upgrade-prompts/0.0.60.md +40 -0
|
@@ -1272,18 +1272,18 @@ function setTLSEnabled(enabled) {
|
|
|
1272
1272
|
ensureRouterFiles();
|
|
1273
1273
|
import_node_fs4.default.writeFileSync(TRAEFIK_DYNAMIC_BASE_FILE, renderTraefikBaseDynamicYml(enabled), "utf-8");
|
|
1274
1274
|
}
|
|
1275
|
-
function isTLSConfigured() {
|
|
1275
|
+
function isTLSConfigured(read = (file) => import_node_fs4.default.readFileSync(file, "utf-8")) {
|
|
1276
1276
|
if (!import_node_fs4.default.existsSync(TRAEFIK_DYNAMIC_BASE_FILE)) {
|
|
1277
1277
|
return false;
|
|
1278
1278
|
}
|
|
1279
|
-
const content =
|
|
1279
|
+
const content = read(TRAEFIK_DYNAMIC_BASE_FILE);
|
|
1280
1280
|
return content.includes("certificates:") && content.includes("/certs/localhost.pem");
|
|
1281
1281
|
}
|
|
1282
1282
|
function areTLSCertsPresent() {
|
|
1283
1283
|
return import_node_fs4.default.existsSync(CERT_FILE) && import_node_fs4.default.existsSync(CERT_KEY_FILE);
|
|
1284
1284
|
}
|
|
1285
|
-
function isTLSEnabled() {
|
|
1286
|
-
return areTLSCertsPresent() && isTLSConfigured();
|
|
1285
|
+
function isTLSEnabled(read) {
|
|
1286
|
+
return areTLSCertsPresent() && isTLSConfigured(read);
|
|
1287
1287
|
}
|
|
1288
1288
|
function runDockerCompose(args) {
|
|
1289
1289
|
const result = (0, import_node_child_process6.spawnSync)("docker", ["compose", "-f", COMPOSE_FILE, ...args], {
|
|
@@ -1717,10 +1717,10 @@ function writeRouteGeneration(routes, tlsEnabled) {
|
|
|
1717
1717
|
writeFileAtomically(HOST_ROUTES_STATE_FILE, renderCompatibilityState(routes));
|
|
1718
1718
|
writeFileAtomically(TRAEFIK_HOST_ROUTES_FILE, renderCanonicalState(routes, tlsEnabled));
|
|
1719
1719
|
}
|
|
1720
|
-
function readCompatibilityState() {
|
|
1720
|
+
function readCompatibilityState(read = (file) => import_node_fs6.default.readFileSync(file, "utf-8")) {
|
|
1721
1721
|
let raw;
|
|
1722
1722
|
try {
|
|
1723
|
-
raw =
|
|
1723
|
+
raw = read(HOST_ROUTES_STATE_FILE);
|
|
1724
1724
|
} catch (error) {
|
|
1725
1725
|
throw new Error(
|
|
1726
1726
|
`Could not read the compatibility host-route state: ${error.message}`
|
|
@@ -1963,12 +1963,12 @@ function validateState(value, repoPath, workspace) {
|
|
|
1963
1963
|
updatedAt: candidate.updatedAt
|
|
1964
1964
|
};
|
|
1965
1965
|
}
|
|
1966
|
-
function readManagedRuntimeState(repoPath, workspace) {
|
|
1966
|
+
function readManagedRuntimeState(repoPath, workspace, read = (file) => import_node_fs7.default.readFileSync(file, "utf-8")) {
|
|
1967
1967
|
const statePath = managedRuntimeStatePath(repoPath, workspace);
|
|
1968
1968
|
if (!import_node_fs7.default.existsSync(statePath)) return void 0;
|
|
1969
1969
|
let parsed;
|
|
1970
1970
|
try {
|
|
1971
|
-
parsed = JSON.parse(
|
|
1971
|
+
parsed = JSON.parse(read(statePath));
|
|
1972
1972
|
} catch (error) {
|
|
1973
1973
|
throw new Error(`Could not parse managed runtime state: ${String(error)}`);
|
|
1974
1974
|
}
|
|
@@ -2785,7 +2785,10 @@ function readReliabilityOperation(identity) {
|
|
|
2785
2785
|
const file = reliabilityOperationPath(identity);
|
|
2786
2786
|
let descriptor;
|
|
2787
2787
|
try {
|
|
2788
|
-
descriptor = import_node_fs8.default.openSync(
|
|
2788
|
+
descriptor = import_node_fs8.default.openSync(
|
|
2789
|
+
file,
|
|
2790
|
+
import_node_fs8.default.constants.O_RDONLY | import_node_fs8.default.constants.O_NOFOLLOW | import_node_fs8.default.constants.O_NONBLOCK
|
|
2791
|
+
);
|
|
2789
2792
|
} catch (error) {
|
|
2790
2793
|
if (error.code === "ENOENT") return void 0;
|
|
2791
2794
|
throw error;
|
|
@@ -2795,7 +2798,12 @@ function readReliabilityOperation(identity) {
|
|
|
2795
2798
|
if (!stat.isFile() || stat.size > MAX_RECORD_BYTES || (stat.mode & 63) !== 0) {
|
|
2796
2799
|
throw new Error("Reliability record is not a bounded private file.");
|
|
2797
2800
|
}
|
|
2798
|
-
const
|
|
2801
|
+
const bytes = Buffer.alloc(MAX_RECORD_BYTES + 1);
|
|
2802
|
+
const count = import_node_fs8.default.readSync(descriptor, bytes, 0, bytes.length, 0);
|
|
2803
|
+
if (count > MAX_RECORD_BYTES) throw new Error("Reliability record exceeds its byte limit.");
|
|
2804
|
+
const record3 = JSON.parse(
|
|
2805
|
+
bytes.subarray(0, count).toString("utf8")
|
|
2806
|
+
);
|
|
2799
2807
|
validate(record3, identity);
|
|
2800
2808
|
return record3;
|
|
2801
2809
|
} finally {
|
|
@@ -4492,7 +4500,7 @@ function resolveRepoPath(repoPath) {
|
|
|
4492
4500
|
function getRepoConfigPath(repoPath) {
|
|
4493
4501
|
return import_node_path15.default.join(resolveRepoPath(repoPath), CONFIG_FILE_NAME);
|
|
4494
4502
|
}
|
|
4495
|
-
function loadRepoConfig(repoPath) {
|
|
4503
|
+
function loadRepoConfig(repoPath, read = (file) => import_node_fs13.default.readFileSync(file, "utf-8")) {
|
|
4496
4504
|
const resolvedRepoPath = resolveRepoPath(repoPath);
|
|
4497
4505
|
const configPath = getRepoConfigPath(resolvedRepoPath);
|
|
4498
4506
|
if (!import_node_fs13.default.existsSync(configPath)) {
|
|
@@ -4500,12 +4508,12 @@ function loadRepoConfig(repoPath) {
|
|
|
4500
4508
|
`Missing ${CONFIG_FILE_NAME} in ${resolvedRepoPath}. Run 'dev repo init --repo ${resolvedRepoPath}' first.`
|
|
4501
4509
|
);
|
|
4502
4510
|
}
|
|
4503
|
-
const raw =
|
|
4511
|
+
const raw = read(configPath);
|
|
4504
4512
|
const parsed = import_yaml3.default.parse(raw);
|
|
4505
4513
|
const config = parseConfig(parsed ?? {}, configPath);
|
|
4506
4514
|
const requiredVersion = config.devrouter?.version;
|
|
4507
4515
|
if (requiredVersion && !hasWarnedVersionMismatch) {
|
|
4508
|
-
const cliVersion = true ? "0.0.
|
|
4516
|
+
const cliVersion = true ? "0.0.60" : "0.0.0-dev";
|
|
4509
4517
|
if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
|
|
4510
4518
|
hasWarnedVersionMismatch = true;
|
|
4511
4519
|
process.stderr.write(
|
|
@@ -6067,7 +6075,7 @@ function probeHttpRoute(host, options = {}) {
|
|
|
6067
6075
|
throw new Error("maxTimeSeconds must be a positive finite number.");
|
|
6068
6076
|
}
|
|
6069
6077
|
if (options.readiness !== void 0) {
|
|
6070
|
-
return
|
|
6078
|
+
return probeHttpReadinessSync(host, options.readiness, maxTimeSeconds, options.repoPath);
|
|
6071
6079
|
}
|
|
6072
6080
|
const tlsEnabled = isTLSEnabled();
|
|
6073
6081
|
const url = `${tlsEnabled ? "https" : "http"}://${host}`;
|
|
@@ -6093,33 +6101,16 @@ function probeHttpRoute(host, options = {}) {
|
|
|
6093
6101
|
const details = ok ? `HTTP ${status}` : [status === void 0 ? void 0 : `HTTP ${status}`, result.stderr?.trim()].filter(Boolean).join(": ") || `curl exited with status ${result.status ?? "unknown"}`;
|
|
6094
6102
|
return { ok, status, details };
|
|
6095
6103
|
}
|
|
6096
|
-
function
|
|
6104
|
+
function probeHttpReadinessSync(host, readiness, maxTimeSeconds, repoPath) {
|
|
6097
6105
|
const parentTimeoutMs = maxTimeSeconds * 1e3 + PARENT_TIMEOUT_PADDING_MS;
|
|
6098
6106
|
if (!Number.isFinite(parentTimeoutMs)) {
|
|
6099
6107
|
throw new Error("maxTimeSeconds is too large for a bounded probe timeout.");
|
|
6100
6108
|
}
|
|
6101
6109
|
const tlsEnabled = isTLSEnabled();
|
|
6102
|
-
const
|
|
6103
|
-
const args = [
|
|
6104
|
-
"--disable",
|
|
6105
|
-
"--globoff",
|
|
6106
|
-
"--silent",
|
|
6107
|
-
"--show-error",
|
|
6108
|
-
"--no-location",
|
|
6109
|
-
"--noproxy",
|
|
6110
|
-
"*",
|
|
6111
|
-
"--output",
|
|
6112
|
-
"/dev/null",
|
|
6113
|
-
"--write-out",
|
|
6114
|
-
"%{http_code} %{content_type}",
|
|
6115
|
-
"--max-time",
|
|
6116
|
-
String(maxTimeSeconds)
|
|
6117
|
-
];
|
|
6110
|
+
const args = buildHttpReadinessCurlArgs(host, readiness, maxTimeSeconds, tlsEnabled);
|
|
6118
6111
|
if (tlsEnabled) {
|
|
6119
6112
|
getMkcertRootCAPath({ repoPath });
|
|
6120
|
-
args.push("--cacert", CERT_FILE);
|
|
6121
6113
|
}
|
|
6122
|
-
args.push(url);
|
|
6123
6114
|
let result;
|
|
6124
6115
|
try {
|
|
6125
6116
|
result = (0, import_node_child_process17.spawnSync)("curl", args, {
|
|
@@ -6134,6 +6125,30 @@ function probeHttpReadiness(host, readiness, maxTimeSeconds, repoPath) {
|
|
|
6134
6125
|
return transportFailure();
|
|
6135
6126
|
}
|
|
6136
6127
|
const metadata = typeof result.stdout === "string" ? result.stdout : "";
|
|
6128
|
+
return parseHttpReadinessMetadata(metadata, readiness);
|
|
6129
|
+
}
|
|
6130
|
+
function buildHttpReadinessCurlArgs(host, readiness, maxTimeSeconds, tlsEnabled) {
|
|
6131
|
+
const url = `${tlsEnabled ? "https" : "http"}://${host}${readiness.path}`;
|
|
6132
|
+
const args = [
|
|
6133
|
+
"--disable",
|
|
6134
|
+
"--globoff",
|
|
6135
|
+
"--silent",
|
|
6136
|
+
"--show-error",
|
|
6137
|
+
"--no-location",
|
|
6138
|
+
"--noproxy",
|
|
6139
|
+
"*",
|
|
6140
|
+
"--output",
|
|
6141
|
+
"/dev/null",
|
|
6142
|
+
"--write-out",
|
|
6143
|
+
"%{http_code} %{content_type}",
|
|
6144
|
+
"--max-time",
|
|
6145
|
+
String(maxTimeSeconds)
|
|
6146
|
+
];
|
|
6147
|
+
if (tlsEnabled) args.push("--cacert", CERT_FILE);
|
|
6148
|
+
args.push(url);
|
|
6149
|
+
return args;
|
|
6150
|
+
}
|
|
6151
|
+
function parseHttpReadinessMetadata(metadata, readiness) {
|
|
6137
6152
|
if (metadata.length > CONTRACT_MAX_BUFFER) {
|
|
6138
6153
|
return transportFailure();
|
|
6139
6154
|
}
|
|
@@ -7845,6 +7860,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
7845
7860
|
let managedConfigWritten = false;
|
|
7846
7861
|
let managedWorkspaceEnv;
|
|
7847
7862
|
let firstTransitionBaseline;
|
|
7863
|
+
let detachedState;
|
|
7864
|
+
let resetCandidateState;
|
|
7848
7865
|
const routeLoadOptions = {
|
|
7849
7866
|
initialTimeoutMs: Math.min(options.httpTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS, 3e3),
|
|
7850
7867
|
recoveryTimeoutMs: Math.min(options.httpTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS, 1e4),
|
|
@@ -7880,6 +7897,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
7880
7897
|
if (managedRuntime) {
|
|
7881
7898
|
previousManagedState = readManagedRuntimeState(repoPath, target.workspace);
|
|
7882
7899
|
if (!options.repair && previousManagedState && !hasExactManagedComposeProject(repoPath, previousManagedState)) {
|
|
7900
|
+
detachedState = previousManagedState;
|
|
7883
7901
|
previousManagedState = void 0;
|
|
7884
7902
|
}
|
|
7885
7903
|
if (previousManagedState && previousManagedState.devpodId !== target.devpodId) {
|
|
@@ -8097,6 +8115,25 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
8097
8115
|
);
|
|
8098
8116
|
container = await preflight(options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS);
|
|
8099
8117
|
managedContainer = container;
|
|
8118
|
+
if (detachedState && devpodId) {
|
|
8119
|
+
resetCandidateState = {
|
|
8120
|
+
version: 1,
|
|
8121
|
+
repoPath,
|
|
8122
|
+
...target.workspace !== void 0 ? { workspace: target.workspace } : {},
|
|
8123
|
+
devpodId,
|
|
8124
|
+
composeProject: managedComposeProject,
|
|
8125
|
+
profile: runtime.profile,
|
|
8126
|
+
desired: {
|
|
8127
|
+
apps: apps.map((app) => app.name).sort(),
|
|
8128
|
+
services: [...managedPlan.desiredProfileServices].sort(),
|
|
8129
|
+
processes: [...desiredProcesses].sort()
|
|
8130
|
+
},
|
|
8131
|
+
sourceConfigSha256: managedPlan.sourceConfigSha256,
|
|
8132
|
+
effectiveConfigSha256: managedPlan.effectiveConfigSha256,
|
|
8133
|
+
status: "degraded",
|
|
8134
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8135
|
+
};
|
|
8136
|
+
}
|
|
8100
8137
|
}
|
|
8101
8138
|
if (options.repair && managedPlan)
|
|
8102
8139
|
assertRetainedRepairContainers(repoPath, managedPlan, retainedRepairContainers);
|
|
@@ -8272,6 +8309,83 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
8272
8309
|
...managedRuntimeStatus ? { managedRuntime: managedRuntimeStatus } : {}
|
|
8273
8310
|
};
|
|
8274
8311
|
} catch (error) {
|
|
8312
|
+
if (detachedState) {
|
|
8313
|
+
const failures = [];
|
|
8314
|
+
const resetWorkspace = detachedState.workspace;
|
|
8315
|
+
try {
|
|
8316
|
+
const routes = listHostRouteState().filter(
|
|
8317
|
+
(route) => sameWorkspacePath(route.repoPath, repoPath)
|
|
8318
|
+
);
|
|
8319
|
+
if (routes.some((route) => route.workspace !== resetWorkspace)) {
|
|
8320
|
+
throw new Error("Reset recovery found a foreign workspace route.");
|
|
8321
|
+
}
|
|
8322
|
+
claimLifecycleEffect();
|
|
8323
|
+
replaceHostRoutesForRepo(repoPath, []);
|
|
8324
|
+
await ensureTraefikRoutesRemoved([...routes, ...candidateRoutes], {
|
|
8325
|
+
...routeLoadOptions,
|
|
8326
|
+
allowRestart: false
|
|
8327
|
+
});
|
|
8328
|
+
} catch (cleanupError) {
|
|
8329
|
+
failures.push(
|
|
8330
|
+
`routes: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
8331
|
+
);
|
|
8332
|
+
}
|
|
8333
|
+
if (managedContainer) {
|
|
8334
|
+
for (const name of managedProcessRegistry) {
|
|
8335
|
+
try {
|
|
8336
|
+
claimLifecycleEffect();
|
|
8337
|
+
runManagedProcessAction({
|
|
8338
|
+
container: managedContainer,
|
|
8339
|
+
name,
|
|
8340
|
+
action: "stop",
|
|
8341
|
+
quiet: options.quiet
|
|
8342
|
+
});
|
|
8343
|
+
} catch (cleanupError) {
|
|
8344
|
+
failures.push(
|
|
8345
|
+
`process '${name}': ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
8346
|
+
);
|
|
8347
|
+
}
|
|
8348
|
+
}
|
|
8349
|
+
}
|
|
8350
|
+
if (resetCandidateState && managedPlan && managedContainer && managedComposeProject) {
|
|
8351
|
+
try {
|
|
8352
|
+
await waitForManagedServices(
|
|
8353
|
+
managedPlan,
|
|
8354
|
+
repoPath,
|
|
8355
|
+
managedComposeProject,
|
|
8356
|
+
managedPlan.desiredServices,
|
|
8357
|
+
managedContainer.id,
|
|
8358
|
+
0
|
|
8359
|
+
);
|
|
8360
|
+
assertManagedContainerConfigUnchanged({
|
|
8361
|
+
plan: managedPlan,
|
|
8362
|
+
containers: inspectWorkspaceContainers({ composeProject: managedComposeProject }),
|
|
8363
|
+
workspace: managedWorkspaceEnv
|
|
8364
|
+
});
|
|
8365
|
+
if (inspectManagedDevcontainerGeneratedConfig(managedPlan).status !== "valid") {
|
|
8366
|
+
throw new Error("Reset candidate configuration changed before recovery persistence.");
|
|
8367
|
+
}
|
|
8368
|
+
claimLifecycleEffect();
|
|
8369
|
+
writeManagedRuntimeState({
|
|
8370
|
+
...resetCandidateState,
|
|
8371
|
+
transitionPhase,
|
|
8372
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8373
|
+
});
|
|
8374
|
+
} catch (stateError) {
|
|
8375
|
+
failures.push(
|
|
8376
|
+
`state: ${stateError instanceof Error ? stateError.message : String(stateError)}`
|
|
8377
|
+
);
|
|
8378
|
+
}
|
|
8379
|
+
} else {
|
|
8380
|
+
failures.push(
|
|
8381
|
+
"candidate ownership and complete service population were not proved; recovery state was not replaced"
|
|
8382
|
+
);
|
|
8383
|
+
}
|
|
8384
|
+
const original = error instanceof Error ? error.message : String(error);
|
|
8385
|
+
throw new Error(
|
|
8386
|
+
`${original} Reset candidate retained for recovery.${failures.length ? ` Recovery incomplete: ${failures.join("; ")}.` : " State is degraded."}`
|
|
8387
|
+
);
|
|
8388
|
+
}
|
|
8275
8389
|
if (options.repair && !repairMutationStarted) throw error;
|
|
8276
8390
|
if (options.repair && repairMutationStarted && previousManagedState && managedPlan) {
|
|
8277
8391
|
const failures = [];
|
|
@@ -8368,7 +8482,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
8368
8482
|
if (!managedPostStartPlan) {
|
|
8369
8483
|
throw new Error("Managed post-start plan disappeared during rollback.");
|
|
8370
8484
|
}
|
|
8371
|
-
if (previousManagedState?.status !== "degraded") {
|
|
8485
|
+
if (previousProcesses.length > 0 && previousManagedState?.status !== "degraded") {
|
|
8372
8486
|
claimLifecycleEffect();
|
|
8373
8487
|
runManagedPostStart({
|
|
8374
8488
|
plan: managedPostStartPlan,
|