@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.
@@ -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 = import_node_fs4.default.readFileSync(TRAEFIK_DYNAMIC_BASE_FILE, "utf-8");
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 = import_node_fs6.default.readFileSync(HOST_ROUTES_STATE_FILE, "utf-8");
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(import_node_fs7.default.readFileSync(statePath, "utf-8"));
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(file, import_node_fs8.default.constants.O_RDONLY | import_node_fs8.default.constants.O_NOFOLLOW);
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 record3 = JSON.parse(import_node_fs8.default.readFileSync(descriptor, "utf8"));
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 = import_node_fs13.default.readFileSync(configPath, "utf-8");
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.59" : "0.0.0-dev";
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 probeHttpReadiness(host, options.readiness, maxTimeSeconds, options.repoPath);
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 probeHttpReadiness(host, readiness, maxTimeSeconds, repoPath) {
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 url = `${tlsEnabled ? "https" : "http"}://${host}${readiness.path}`;
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,