@devrouter/cli 0.0.56 → 0.0.58

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.
@@ -23,7 +23,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  ));
24
24
 
25
25
  // src/core/devpod-exec.ts
26
- var import_node_child_process22 = require("child_process");
26
+ var import_node_child_process23 = require("child_process");
27
27
  var import_node_crypto11 = require("crypto");
28
28
 
29
29
  // src/core/devpod-environment.ts
@@ -1516,7 +1516,7 @@ function selectDevpodWorkspace(workspaces, repoPath) {
1516
1516
  }
1517
1517
 
1518
1518
  // src/core/devsy-exec.ts
1519
- var import_node_child_process21 = require("child_process");
1519
+ var import_node_child_process22 = require("child_process");
1520
1520
 
1521
1521
  // src/core/execution-outcome.ts
1522
1522
  var ExecutionOutcomeError = class extends Error {
@@ -2007,6 +2007,9 @@ var RELIABILITY_MAX_ITEMS = 128;
2007
2007
  function isReliabilityId(value) {
2008
2008
  return typeof value === "string" && /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(value);
2009
2009
  }
2010
+ function isReliabilityProfile(value) {
2011
+ return typeof value === "string" && value.length <= 4096 && value.split(",").every((name) => isReliabilityId(name.trim()));
2012
+ }
2010
2013
  function isReliabilityCounter(value) {
2011
2014
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
2012
2015
  }
@@ -2037,7 +2040,7 @@ function validOperation(value) {
2037
2040
  function assertReliabilityState(value) {
2038
2041
  const intentRevision = record(value) ? value.intentRevision : void 0;
2039
2042
  const valid = record(value) && value.contractVersion === RELIABILITY_CONTRACT_VERSION && oneOf(value.executionPolicy, ["manual", "capacity-managed"]) && (value.executionPolicy === "manual" ? value.admission === "not-applicable" && value.chargeHeld === false : value.admission !== "not-applicable") && boundedArray(value.operationHistory) && uniqueIds(value.operationHistory, "key") && uniqueIds(value.operationHistory, "id") && value.operationHistory.every(
2040
- (entry) => record(entry) && isReliabilityId(entry.profile) && record(entry.consumer) && isReliabilityId(entry.consumer.id) && typeof entry.consumer.pinned === "boolean" && boundedArray(entry.consumer.requiredCapabilities) && entry.consumer.requiredCapabilities.every(isReliabilityId) && new Set(entry.consumer.requiredCapabilities).size === entry.consumer.requiredCapabilities.length && validOperation(entry)
2043
+ (entry) => record(entry) && isReliabilityProfile(entry.profile) && record(entry.consumer) && isReliabilityId(entry.consumer.id) && typeof entry.consumer.pinned === "boolean" && boundedArray(entry.consumer.requiredCapabilities) && entry.consumer.requiredCapabilities.every(isReliabilityId) && new Set(entry.consumer.requiredCapabilities).size === entry.consumer.requiredCapabilities.length && validOperation(entry)
2041
2044
  ) && isReliabilityCounter(value.observationsAfterMs) && isReliabilityId(value.environmentId) && isReliabilityCounter(value.intentRevision) && isReliabilityCounter(value.runtimeGeneration) && isReliabilityCounter(value.controllerEpoch) && oneOf(value.desired, ["running", "parked-for-capacity", "stopped-by-user"]) && oneOf(value.phase, [
2042
2045
  "idle",
2043
2046
  "queued",
@@ -2046,7 +2049,7 @@ function assertReliabilityState(value) {
2046
2049
  "stable",
2047
2050
  "recovering",
2048
2051
  "stopping"
2049
- ]) && (value.profile === null || isReliabilityId(value.profile)) && oneOf(value.admission, [
2052
+ ]) && (value.profile === null || isReliabilityProfile(value.profile)) && oneOf(value.admission, [
2050
2053
  "admitted",
2051
2054
  "waiting",
2052
2055
  "unknown",
@@ -2055,7 +2058,7 @@ function assertReliabilityState(value) {
2055
2058
  ]) && typeof value.chargeHeld === "boolean" && record(value.stopProof) && typeof value.stopProof.workloadsStopped === "boolean" && typeof value.stopProof.routesRemoved === "boolean" && boundedArray(value.consumers) && uniqueIds(value.consumers, "id") && value.consumers.every(
2056
2059
  (consumer) => record(consumer) && typeof consumer.pinned === "boolean" && boundedArray(consumer.requiredCapabilities) && consumer.requiredCapabilities.every(isReliabilityId) && new Set(consumer.requiredCapabilities).size === consumer.requiredCapabilities.length
2057
2060
  ) && boundedArray(value.requests) && uniqueIds(value.requests, "key") && value.requests.every(
2058
- (request) => record(request) && oneOf(request.mode, ["start", "attach"]) && isReliabilityId(request.consumerId) && isReliabilityId(request.operationId) && isReliabilityId(request.profile) && isReliabilityCounter(request.intentRevision) && isReliabilityCounter(intentRevision) && request.intentRevision <= intentRevision
2061
+ (request) => record(request) && oneOf(request.mode, ["start", "attach"]) && isReliabilityId(request.consumerId) && isReliabilityId(request.operationId) && isReliabilityProfile(request.profile) && isReliabilityCounter(request.intentRevision) && isReliabilityCounter(intentRevision) && request.intentRevision <= intentRevision
2059
2062
  ) && boundedArray(value.observations) && uniqueIds(value.observations, "capability") && value.observations.every(
2060
2063
  (observation) => record(observation) && oneOf(observation.infrastructure, ["healthy", "failed", "unknown"]) && oneOf(observation.application, ["verified", "unready", "unverified"]) && isReliabilityCounter(observation.observedAtMs) && isReliabilityCounter(observation.validForMs)
2061
2064
  ) && (value.operation === null || validOperation(value.operation)) && (value.executionPolicy !== "manual" || value.desired !== "parked-for-capacity" && value.incident === null) && (value.incident === null || record(value.incident) && isReliabilityId(value.incident.id) && isReliabilityCounter(value.incident.correctiveActionsTaken) && isReliabilityCounter(value.incident.actionLimit) && value.incident.actionLimit > 0 && value.incident.correctiveActionsTaken <= value.incident.actionLimit);
@@ -2124,10 +2127,10 @@ function assertReliabilityEvent(value) {
2124
2127
  let valid = false;
2125
2128
  switch (value.type) {
2126
2129
  case "request":
2127
- valid = oneOf2(value.mode, ["start", "attach"]) && isReliabilityId(value.key) && isReliabilityId(value.operationId) && isReliabilityId(value.profile) && isConsumer(value.consumer);
2130
+ valid = oneOf2(value.mode, ["start", "attach"]) && isReliabilityId(value.key) && isReliabilityId(value.operationId) && isReliabilityProfile(value.profile) && isConsumer(value.consumer);
2128
2131
  break;
2129
2132
  case "operation-request":
2130
- valid = oneOf2(value.kind, ["ensure", "exec"]) && isReliabilityId(value.key) && isReliabilityId(value.operationId) && isReliabilityId(value.profile) && isConsumer(value.consumer) && typeof value.runtimeRunning === "boolean";
2133
+ valid = oneOf2(value.kind, ["ensure", "exec"]) && isReliabilityId(value.key) && isReliabilityId(value.operationId) && isReliabilityProfile(value.profile) && isConsumer(value.consumer) && typeof value.runtimeRunning === "boolean";
2131
2134
  break;
2132
2135
  case "drained":
2133
2136
  valid = isReliabilityId(value.operationId);
@@ -2866,8 +2869,8 @@ function workerGroupAbsent(pid) {
2866
2869
  }
2867
2870
 
2868
2871
  // src/core/workspace-ensure.ts
2869
- var import_node_child_process20 = require("child_process");
2870
- var import_node_fs21 = __toESM(require("fs"));
2872
+ var import_node_child_process21 = require("child_process");
2873
+ var import_node_fs22 = __toESM(require("fs"));
2871
2874
  var import_node_path23 = __toESM(require("path"));
2872
2875
 
2873
2876
  // src/core/devcontainer-profile.ts
@@ -3576,6 +3579,8 @@ function assertHostNotTemplated(host, label) {
3576
3579
  }
3577
3580
  }
3578
3581
  var MAX_COMMAND_LENGTH = 4096;
3582
+ var MAX_PREPARE_ARGS = 64;
3583
+ var MAX_PREPARE_ARG_BYTES = 4096;
3579
3584
  var DEFAULT_HOST_STRATEGY = {
3580
3585
  type: "auto",
3581
3586
  denyPorts: [80, 443, 5432],
@@ -3996,6 +4001,38 @@ function parseRequiredUniqueStringArray(value, pathLabel) {
3996
4001
  }
3997
4002
  return parseUniqueStringArray(value, pathLabel);
3998
4003
  }
4004
+ function parsePrepareCommand(value, pathLabel) {
4005
+ if (value === void 0) {
4006
+ return void 0;
4007
+ }
4008
+ if (!Array.isArray(value)) {
4009
+ throw new Error(`${pathLabel} must be a non-empty array of strings.`);
4010
+ }
4011
+ if (value.length === 0) {
4012
+ throw new Error(`${pathLabel} must be a non-empty array of strings.`);
4013
+ }
4014
+ if (value.length > MAX_PREPARE_ARGS) {
4015
+ throw new Error(`${pathLabel} exceeds the maximum of ${MAX_PREPARE_ARGS} arguments.`);
4016
+ }
4017
+ return value.map((item, index) => {
4018
+ if (typeof item !== "string") {
4019
+ throw new Error(`${pathLabel}[${index}] must be a string.`);
4020
+ }
4021
+ if (index === 0 && item.length === 0) {
4022
+ throw new Error(`${pathLabel}[0] must be a non-empty executable.`);
4023
+ }
4024
+ if (item.includes("\0")) {
4025
+ throw new Error(`${pathLabel}[${index}] must not contain null bytes.`);
4026
+ }
4027
+ const byteLength = Buffer.byteLength(item, "utf8");
4028
+ if (byteLength > MAX_PREPARE_ARG_BYTES) {
4029
+ throw new Error(
4030
+ `${pathLabel}[${index}] exceeds the maximum of ${MAX_PREPARE_ARG_BYTES} UTF-8 bytes.`
4031
+ );
4032
+ }
4033
+ return item;
4034
+ });
4035
+ }
3999
4036
  function parseManagedRuntime(value, configPath) {
4000
4037
  if (value === void 0) {
4001
4038
  return void 0;
@@ -4008,7 +4045,7 @@ function parseManagedRuntime(value, configPath) {
4008
4045
  );
4009
4046
  ensureAllowedKeys(
4010
4047
  devcontainer,
4011
- ["baseServices", "profileServices"],
4048
+ ["baseServices", "profileServices", "prepareCommand"],
4012
4049
  `${configPath}.managedRuntime.devcontainer`
4013
4050
  );
4014
4051
  const baseServices = parseRequiredUniqueStringArray(
@@ -4019,6 +4056,10 @@ function parseManagedRuntime(value, configPath) {
4019
4056
  devcontainer.profileServices,
4020
4057
  `${configPath}.managedRuntime.devcontainer.profileServices`
4021
4058
  );
4059
+ const prepareCommand = parsePrepareCommand(
4060
+ devcontainer.prepareCommand,
4061
+ `${configPath}.managedRuntime.devcontainer.prepareCommand`
4062
+ );
4022
4063
  const baseSet = new Set(baseServices);
4023
4064
  const overlappingServices = profileServices.filter((service) => baseSet.has(service));
4024
4065
  if (overlappingServices.length > 0) {
@@ -4039,7 +4080,11 @@ function parseManagedRuntime(value, configPath) {
4039
4080
  );
4040
4081
  }
4041
4082
  return {
4042
- devcontainer: { baseServices, profileServices },
4083
+ devcontainer: {
4084
+ baseServices,
4085
+ profileServices,
4086
+ ...prepareCommand ? { prepareCommand } : {}
4087
+ },
4043
4088
  processes
4044
4089
  };
4045
4090
  }
@@ -4460,7 +4505,7 @@ function loadRepoConfig(repoPath) {
4460
4505
  const config = parseConfig(parsed ?? {}, configPath);
4461
4506
  const requiredVersion = config.devrouter?.version;
4462
4507
  if (requiredVersion && !hasWarnedVersionMismatch) {
4463
- const cliVersion = true ? "0.0.56" : "0.0.0-dev";
4508
+ const cliVersion = true ? "0.0.58" : "0.0.0-dev";
4464
4509
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
4465
4510
  hasWarnedVersionMismatch = true;
4466
4511
  process.stderr.write(
@@ -6136,10 +6181,88 @@ function transportFailure() {
6136
6181
  };
6137
6182
  }
6138
6183
 
6139
- // src/core/managed-post-start.ts
6184
+ // src/core/managed-host-preparation.ts
6140
6185
  var import_node_child_process18 = require("child_process");
6141
- var import_node_crypto10 = require("crypto");
6142
6186
  var import_node_fs19 = __toESM(require("fs"));
6187
+ var PREPARATION_TIMEOUT_MS = 6e4;
6188
+ var CONFIG_READ_ERROR = "Managed host preparation could not read .devrouter.yml.";
6189
+ var CONFIG_CHANGED_ERROR = "Managed host preparation changed .devrouter.yml; refusing to continue.";
6190
+ var START_ERROR = "Managed host preparation could not start.";
6191
+ var FAILURE_ERROR = "Managed host preparation failed.";
6192
+ var TIMEOUT_ERROR = "Managed host preparation timed out after 60 seconds.";
6193
+ function runPreparationCommand(argv, cwd) {
6194
+ return new Promise((resolve) => {
6195
+ let child;
6196
+ try {
6197
+ child = (0, import_node_child_process18.spawn)(argv[0], argv.slice(1), {
6198
+ cwd,
6199
+ detached: true,
6200
+ shell: false,
6201
+ stdio: "ignore"
6202
+ });
6203
+ } catch {
6204
+ resolve("start-error");
6205
+ return;
6206
+ }
6207
+ let timedOut = false;
6208
+ const deadline = setTimeout(() => {
6209
+ timedOut = true;
6210
+ if (child.pid !== void 0 && child.exitCode === null && child.signalCode === null) {
6211
+ try {
6212
+ process.kill(-child.pid, "SIGKILL");
6213
+ } catch {
6214
+ }
6215
+ }
6216
+ }, PREPARATION_TIMEOUT_MS);
6217
+ child.once("error", () => {
6218
+ clearTimeout(deadline);
6219
+ resolve("start-error");
6220
+ });
6221
+ child.once("close", (code) => {
6222
+ clearTimeout(deadline);
6223
+ resolve(timedOut ? "timeout" : code === 0 ? "success" : "failure");
6224
+ });
6225
+ });
6226
+ }
6227
+ function readConfigBytes(configPath) {
6228
+ try {
6229
+ return import_node_fs19.default.readFileSync(configPath);
6230
+ } catch {
6231
+ return void 0;
6232
+ }
6233
+ }
6234
+ async function runManagedHostPreparation(repoPath, config) {
6235
+ const prepareCommand = config.managedRuntime?.devcontainer.prepareCommand;
6236
+ if (!prepareCommand) return;
6237
+ if (process.platform === "win32") {
6238
+ throw new Error("Managed host preparation requires POSIX process-group ownership.");
6239
+ }
6240
+ const resolvedRepoPath = resolveRepoPath(repoPath);
6241
+ const configPath = getRepoConfigPath(resolvedRepoPath);
6242
+ const before = readConfigBytes(configPath);
6243
+ if (!before) {
6244
+ throw new Error(CONFIG_READ_ERROR);
6245
+ }
6246
+ const result = await runPreparationCommand(prepareCommand, resolvedRepoPath);
6247
+ const after = readConfigBytes(configPath);
6248
+ if (!after?.equals(before)) {
6249
+ throw new Error(CONFIG_CHANGED_ERROR);
6250
+ }
6251
+ if (result === "start-error") {
6252
+ throw new Error(START_ERROR);
6253
+ }
6254
+ if (result === "timeout") {
6255
+ throw new Error(TIMEOUT_ERROR);
6256
+ }
6257
+ if (result === "failure") {
6258
+ throw new Error(FAILURE_ERROR);
6259
+ }
6260
+ }
6261
+
6262
+ // src/core/managed-post-start.ts
6263
+ var import_node_child_process19 = require("child_process");
6264
+ var import_node_crypto10 = require("crypto");
6265
+ var import_node_fs20 = __toESM(require("fs"));
6143
6266
  var import_node_path21 = __toESM(require("path"));
6144
6267
  var MANAGED_MARKER = "devrouter:managed devcontainer";
6145
6268
  var MANAGED_ADAPTER_PATH = ".devcontainer/post-start.sh";
@@ -6177,7 +6300,7 @@ function commandFailure3(result) {
6177
6300
  return [result.error?.message, result.stderr, result.stdout].filter(Boolean).map((value) => String(value).trim()).filter(Boolean).join("\n");
6178
6301
  }
6179
6302
  function deliverRuntimeFile(containerId, targetPath, contents, label) {
6180
- const result = (0, import_node_child_process18.spawnSync)(
6303
+ const result = (0, import_node_child_process19.spawnSync)(
6181
6304
  "docker",
6182
6305
  ["exec", "-i", containerId, "sh", "-c", renderDeliveryScript(targetPath)],
6183
6306
  { input: contents, encoding: "utf-8" }
@@ -6188,8 +6311,8 @@ function deliverRuntimeFile(containerId, targetPath, contents, label) {
6188
6311
  }
6189
6312
  }
6190
6313
  function readRegularFileBytes(filePath) {
6191
- if (!import_node_fs19.default.existsSync(filePath) || !import_node_fs19.default.lstatSync(filePath).isFile()) return void 0;
6192
- return import_node_fs19.default.readFileSync(filePath);
6314
+ if (!import_node_fs20.default.existsSync(filePath) || !import_node_fs20.default.lstatSync(filePath).isFile()) return void 0;
6315
+ return import_node_fs20.default.readFileSync(filePath);
6193
6316
  }
6194
6317
  function readRegularFile(filePath) {
6195
6318
  return readRegularFileBytes(filePath)?.toString("utf-8");
@@ -6202,7 +6325,7 @@ function resolveProcessHelperPath() {
6202
6325
  import_node_path21.default.resolve(__dirname, "..", "bin", "devrouter-process"),
6203
6326
  import_node_path21.default.resolve(__dirname, "..", "..", "bin", "devrouter-process")
6204
6327
  ];
6205
- const helperPath = candidates.find((candidate) => import_node_fs19.default.existsSync(candidate));
6328
+ const helperPath = candidates.find((candidate) => import_node_fs20.default.existsSync(candidate));
6206
6329
  if (!helperPath) {
6207
6330
  throw new Error("Could not locate the packaged devrouter-process helper.");
6208
6331
  }
@@ -6254,7 +6377,7 @@ function resolveManagedPostStartPlan(repoPath) {
6254
6377
  }
6255
6378
  function runManagedPostStart(options) {
6256
6379
  if (options.plan.kind !== "runtime") return;
6257
- const helper = import_node_fs19.default.readFileSync(resolveProcessHelperPath());
6380
+ const helper = import_node_fs20.default.readFileSync(resolveProcessHelperPath());
6258
6381
  deliverRuntimeFile(
6259
6382
  options.container.id,
6260
6383
  RUNTIME_HELPER_PATH,
@@ -6268,7 +6391,7 @@ function runManagedPostStart(options) {
6268
6391
  options.plan.adapterContents,
6269
6392
  "the managed post-start adapter"
6270
6393
  );
6271
- const started = (0, import_node_child_process18.spawnSync)(
6394
+ const started = (0, import_node_child_process19.spawnSync)(
6272
6395
  "docker",
6273
6396
  [
6274
6397
  "exec",
@@ -6306,7 +6429,7 @@ function assertSafeProcessName(name) {
6306
6429
  }
6307
6430
  function runManagedProcessAction(options) {
6308
6431
  assertSafeProcessName(options.name);
6309
- const result = (0, import_node_child_process18.spawnSync)(
6432
+ const result = (0, import_node_child_process19.spawnSync)(
6310
6433
  "docker",
6311
6434
  [
6312
6435
  "exec",
@@ -6770,8 +6893,8 @@ function collectManagedRuntimeStatus(options) {
6770
6893
  }
6771
6894
 
6772
6895
  // src/core/traefik-route-health.ts
6773
- var import_node_child_process19 = require("child_process");
6774
- var import_node_fs20 = __toESM(require("fs"));
6896
+ var import_node_child_process20 = require("child_process");
6897
+ var import_node_fs21 = __toESM(require("fs"));
6775
6898
  var import_node_path22 = __toESM(require("path"));
6776
6899
  var ROUTER_API_BASE = "http://127.0.0.1:8080/api";
6777
6900
  var ROUTER_RELOAD_LOCK_FILE = import_node_path22.default.join(DEVROUTER_HOME, "router-reload.lock");
@@ -6784,7 +6907,7 @@ function sleep(ms) {
6784
6907
  return new Promise((resolve) => setTimeout(resolve, ms));
6785
6908
  }
6786
6909
  function inspectRouterApi(protocol) {
6787
- const result = (0, import_node_child_process19.spawnSync)(
6910
+ const result = (0, import_node_child_process20.spawnSync)(
6788
6911
  "curl",
6789
6912
  [
6790
6913
  "--silent",
@@ -6827,7 +6950,7 @@ function isRecord3(value) {
6827
6950
  return value !== null && typeof value === "object" && !Array.isArray(value);
6828
6951
  }
6829
6952
  function inspectNamedRouterApi(protocol, resource, name) {
6830
- const result = (0, import_node_child_process19.spawnSync)(
6953
+ const result = (0, import_node_child_process20.spawnSync)(
6831
6954
  "curl",
6832
6955
  [
6833
6956
  "--silent",
@@ -7038,7 +7161,7 @@ async function ensureTraefikRouteExpectation(routes, expectation, options) {
7038
7161
  throw new Error(
7039
7162
  "Traefik routes did not reload; shared router restart is disabled for this operation."
7040
7163
  );
7041
- import_node_fs20.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
7164
+ import_node_fs21.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
7042
7165
  return withFileLock(
7043
7166
  ROUTER_RELOAD_LOCK_FILE,
7044
7167
  {
@@ -7397,7 +7520,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
7397
7520
  target
7398
7521
  });
7399
7522
  if (target.kind === "linked") {
7400
- const workspaceEnv = (0, import_node_child_process20.spawnSync)(
7523
+ const workspaceEnv = (0, import_node_child_process21.spawnSync)(
7401
7524
  "docker",
7402
7525
  ["exec", appContainer.id, "printenv", "WORKSPACE"],
7403
7526
  { encoding: "utf-8" }
@@ -7407,7 +7530,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
7407
7530
  `Workspace app container must expose WORKSPACE='${target.workspace}' (got '${workspaceEnv.stdout.trim() || "(empty)"}').`
7408
7531
  );
7409
7532
  }
7410
- const devrouterWorkspaceEnv = (0, import_node_child_process20.spawnSync)(
7533
+ const devrouterWorkspaceEnv = (0, import_node_child_process21.spawnSync)(
7411
7534
  "docker",
7412
7535
  ["exec", appContainer.id, "printenv", "DEVROUTER_WORKSPACE"],
7413
7536
  { encoding: "utf-8" }
@@ -7418,7 +7541,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
7418
7541
  );
7419
7542
  }
7420
7543
  }
7421
- const gitCheck = (0, import_node_child_process20.spawnSync)(
7544
+ const gitCheck = (0, import_node_child_process21.spawnSync)(
7422
7545
  "docker",
7423
7546
  [
7424
7547
  "exec",
@@ -7629,12 +7752,14 @@ function assertRepairBaseline(options) {
7629
7752
  )?.Destination;
7630
7753
  if (!workspacePath) throw new Error("Repair cannot prove the primary workspace mount.");
7631
7754
  for (const name of primary.state.Running ? config.managedRuntime?.processes ?? [] : []) {
7632
- if (!processes.includes(name) && runManagedProcessAction({
7755
+ if (processes.includes(name) && !options.verifyRetainedProcesses) continue;
7756
+ const processStatus = runManagedProcessAction({
7633
7757
  container: { id: primary.id, workspacePath },
7634
7758
  name,
7635
7759
  action: "status",
7636
7760
  quiet: true
7637
- }) !== "stopped") {
7761
+ });
7762
+ if (processStatus !== "stopped" && !(processes.includes(name) && processStatus === "running")) {
7638
7763
  throw new Error(`Repair refuses unexpected or unproven process '${name}'.`);
7639
7764
  }
7640
7765
  }
@@ -7671,14 +7796,14 @@ function resolvePrimaryTarget(repoPath) {
7671
7796
  }
7672
7797
  function isPrimaryCheckout(repoPath) {
7673
7798
  try {
7674
- return import_node_fs21.default.statSync(import_node_path23.default.join(repoPath, ".git")).isDirectory();
7799
+ return import_node_fs22.default.statSync(import_node_path23.default.join(repoPath, ".git")).isDirectory();
7675
7800
  } catch {
7676
7801
  return false;
7677
7802
  }
7678
7803
  }
7679
7804
  function openUrls(urls) {
7680
7805
  for (const url of urls) {
7681
- const opened = (0, import_node_child_process20.spawnSync)("open", [url], { encoding: "utf-8" });
7806
+ const opened = (0, import_node_child_process21.spawnSync)("open", [url], { encoding: "utf-8" });
7682
7807
  if (opened.status !== 0) {
7683
7808
  process.stderr.write(`Warning: could not open '${url}'.
7684
7809
  `);
@@ -7730,7 +7855,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
7730
7855
  let devpodId = target.devpodId;
7731
7856
  if (target.kind === "linked") {
7732
7857
  const overlayPath = import_node_path23.default.join(repoPath, ".devcontainer", DEVCONTAINER_OVERLAY);
7733
- if (!import_node_fs21.default.existsSync(overlayPath)) {
7858
+ if (!import_node_fs22.default.existsSync(overlayPath)) {
7734
7859
  throw new Error(`Missing required DevPod compose overlay: ${overlayPath}`);
7735
7860
  }
7736
7861
  }
@@ -7884,7 +8009,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
7884
8009
  repairMutationStarted = true;
7885
8010
  transitionPhase = "service-start";
7886
8011
  claimLifecycleEffect();
7887
- const result = (0, import_node_child_process20.spawnSync)("docker", ["start", ...stopped.map((entry) => entry.id)], {
8012
+ const result = (0, import_node_child_process21.spawnSync)("docker", ["start", ...stopped.map((entry) => entry.id)], {
7888
8013
  encoding: "utf-8",
7889
8014
  timeout: DEFAULT_READINESS_TIMEOUT_MS,
7890
8015
  stdio: options.quiet ? ["ignore", 2, "inherit"] : "inherit"
@@ -8211,27 +8336,29 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
8211
8336
  );
8212
8337
  }
8213
8338
  try {
8214
- const previousAllServices = [
8215
- managedPlan.primaryService,
8216
- ...managedPlan.baseServices,
8217
- ...previousServices
8218
- ];
8219
- claimLifecycleEffect();
8220
- startExactManagedServices({
8221
- plan: managedPlan,
8222
- composeProject: managedComposeProject,
8223
- services: previousAllServices,
8224
- quiet: options.quiet,
8225
- workspace: managedWorkspaceEnv
8226
- });
8227
- await waitForManagedServices(
8228
- managedPlan,
8229
- repoPath,
8230
- managedComposeProject,
8231
- previousAllServices,
8232
- managedContainer.id,
8233
- options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS
8234
- );
8339
+ if (previousManagedState?.status !== "degraded") {
8340
+ const previousAllServices = [
8341
+ managedPlan.primaryService,
8342
+ ...managedPlan.baseServices,
8343
+ ...previousServices
8344
+ ];
8345
+ claimLifecycleEffect();
8346
+ startExactManagedServices({
8347
+ plan: managedPlan,
8348
+ composeProject: managedComposeProject,
8349
+ services: previousAllServices,
8350
+ quiet: options.quiet,
8351
+ workspace: managedWorkspaceEnv
8352
+ });
8353
+ await waitForManagedServices(
8354
+ managedPlan,
8355
+ repoPath,
8356
+ managedComposeProject,
8357
+ previousAllServices,
8358
+ managedContainer.id,
8359
+ options.containerTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS
8360
+ );
8361
+ }
8235
8362
  } catch (rollbackError) {
8236
8363
  rollbackErrors.push(
8237
8364
  `services: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
@@ -8241,14 +8368,16 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
8241
8368
  if (!managedPostStartPlan) {
8242
8369
  throw new Error("Managed post-start plan disappeared during rollback.");
8243
8370
  }
8244
- claimLifecycleEffect();
8245
- runManagedPostStart({
8246
- plan: managedPostStartPlan,
8247
- container: managedContainer,
8248
- quiet: options.quiet,
8249
- profile: previousManagedState?.profile ?? "full",
8250
- processes: previousProcesses
8251
- });
8371
+ if (previousManagedState?.status !== "degraded") {
8372
+ claimLifecycleEffect();
8373
+ runManagedPostStart({
8374
+ plan: managedPostStartPlan,
8375
+ container: managedContainer,
8376
+ quiet: options.quiet,
8377
+ profile: previousManagedState?.profile ?? "full",
8378
+ processes: previousProcesses
8379
+ });
8380
+ }
8252
8381
  for (const process2 of managedProcessRegistry) {
8253
8382
  if (previousProcessSet.has(process2)) continue;
8254
8383
  claimLifecycleEffect();
@@ -8317,7 +8446,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
8317
8446
  );
8318
8447
  }
8319
8448
  }
8320
- if (rollbackErrors.length > 0 && previousManagedState) {
8449
+ if (previousManagedState && (rollbackErrors.length > 0 || previousManagedState.status === "degraded")) {
8321
8450
  try {
8322
8451
  markManagedRuntimeDegraded(previousManagedState, failedPhase);
8323
8452
  } catch (stateError) {
@@ -8327,7 +8456,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
8327
8456
  }
8328
8457
  }
8329
8458
  const original = error instanceof Error ? error.message : String(error);
8330
- const suffix = rollbackErrors.length > 0 ? ` Rollback left degraded drift: ${rollbackErrors.join("; ")}.` : " Candidate runtime was rolled back.";
8459
+ const suffix = rollbackErrors.length > 0 ? ` Rollback left degraded drift: ${rollbackErrors.join("; ")}.` : previousManagedState?.status === "degraded" ? " Candidate resources were reconciled; the retained runtime remains degraded." : " Candidate runtime was rolled back.";
8331
8460
  throw new Error(`${original}${suffix}`);
8332
8461
  }
8333
8462
  if (managedPlan) {
@@ -8392,9 +8521,15 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
8392
8521
  }
8393
8522
  };
8394
8523
  return withLifecycleOperationLock(repoPath, async () => {
8524
+ const preparationConfig = loadRepoConfig(repoPath);
8525
+ if (preparationConfig.managedRuntime?.devcontainer.prepareCommand) {
8526
+ claimLifecycleEffect();
8527
+ await runManagedHostPreparation(repoPath, preparationConfig);
8528
+ claimLifecycleEffect();
8529
+ }
8395
8530
  const repairLocked = () => {
8396
8531
  const runtime = resolveWorkspaceRuntimeOrDefault(repoPath);
8397
- import_node_fs21.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
8532
+ import_node_fs22.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
8398
8533
  return withFileLock(
8399
8534
  import_node_path23.default.join(DEVROUTER_HOME, `${runtime}-mutation.lock`),
8400
8535
  {
@@ -8418,6 +8553,35 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
8418
8553
  linked ? workspace : "",
8419
8554
  requestedOptions.profile
8420
8555
  ).profile;
8556
+ if (desiredProfile !== retained.profile) {
8557
+ const target = resolveRepairTarget(repoPath, linked);
8558
+ const baseline = loadRuntimeConfig(repoPath, linked ? workspace : "", retained.profile);
8559
+ const plan = inspectManagedDevcontainerConfig({
8560
+ repoPath,
8561
+ config: baseline.config,
8562
+ profile: baseline.resolvedProfile,
8563
+ linked
8564
+ });
8565
+ if (!baseline.config.managedRuntime)
8566
+ throw new Error("Recovery requires the retained managed configuration.");
8567
+ assertRepairBaseline({
8568
+ repoPath,
8569
+ target,
8570
+ state: retained,
8571
+ plan,
8572
+ config: baseline.config,
8573
+ profile: baseline.profile,
8574
+ processes: desiredManagedProcesses(
8575
+ baseline.config.managedRuntime,
8576
+ baseline.resolvedProfile
8577
+ ),
8578
+ routes: listHostRouteState().filter((route) => sameWorkspacePath(route.repoPath, repoPath)),
8579
+ verifyRetainedProcesses: true,
8580
+ workspace: target.kind === "linked" ? { token: target.workspace, gitCommonDir: target.gitCommonDir } : void 0
8581
+ });
8582
+ claimLifecycleEffect();
8583
+ return ensureLocked();
8584
+ }
8421
8585
  options = { ...requestedOptions, repair: true, profile: retained.profile, open: false };
8422
8586
  let repaired;
8423
8587
  try {
@@ -8679,7 +8843,7 @@ async function devsyExecOutcome(repoPath, command) {
8679
8843
  let child;
8680
8844
  try {
8681
8845
  claimLifecycleEffect();
8682
- child = (0, import_node_child_process21.spawn)("devsy", args, { stdio: "inherit" });
8846
+ child = (0, import_node_child_process22.spawn)("devsy", args, { stdio: "inherit" });
8683
8847
  } catch (error) {
8684
8848
  reject(
8685
8849
  new ExecutionOutcomeError(`devsy exec failed: ${errorMessage(error)}`, {
@@ -8725,7 +8889,7 @@ function ensureGuidance2(repoPath) {
8725
8889
  return `Run 'devrouter ensure ${repoPath}' first.`;
8726
8890
  }
8727
8891
  function assertRunningDevpod(devpodId, repoPath) {
8728
- const result = (0, import_node_child_process22.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
8892
+ const result = (0, import_node_child_process23.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
8729
8893
  encoding: "utf-8"
8730
8894
  });
8731
8895
  if (result.status !== 0) {
@@ -8904,7 +9068,7 @@ async function devpodExecOutcome(repoPath, command) {
8904
9068
  let child;
8905
9069
  try {
8906
9070
  claimLifecycleEffect();
8907
- child = (0, import_node_child_process22.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
9071
+ child = (0, import_node_child_process23.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
8908
9072
  } catch (error) {
8909
9073
  settled = true;
8910
9074
  reject(
@@ -8961,12 +9125,12 @@ async function devpodExecOutcome(repoPath, command) {
8961
9125
  }
8962
9126
 
8963
9127
  // src/core/workspace-lifecycle.ts
8964
- var import_node_child_process23 = require("child_process");
8965
- var import_node_fs23 = __toESM(require("fs"));
9128
+ var import_node_child_process24 = require("child_process");
9129
+ var import_node_fs24 = __toESM(require("fs"));
8966
9130
  var import_node_path25 = __toESM(require("path"));
8967
9131
 
8968
9132
  // src/core/route-state.ts
8969
- var import_node_fs22 = __toESM(require("fs"));
9133
+ var import_node_fs23 = __toESM(require("fs"));
8970
9134
  function removeWorkspaceRoutesForWorktree(workspace, worktreePath) {
8971
9135
  return removeHostRoutesWhere(
8972
9136
  (route) => route.workspace === workspace && sameWorkspacePath(route.repoPath, worktreePath)