boxdown 1.2.0 → 1.3.0

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.
Files changed (53) hide show
  1. package/README.md +10 -0
  2. package/assets/devcontainer/README.md +29 -6
  3. package/assets/devcontainer/devcontainer.json +4 -9
  4. package/assets/devcontainer/hooks/initialize.sh +49 -27
  5. package/assets/devcontainer/hooks/post-create.sh +23 -1
  6. package/assets/devcontainer/hooks/post-start.sh +0 -10
  7. package/assets/devcontainer/utils/git-signing-bootstrap.sh +58 -5
  8. package/assets/devcontainer/utils/secret-env-bootstrap.sh +22 -0
  9. package/assets/devcontainer/utils/ssh-agent-proxy-bootstrap.sh +27 -0
  10. package/assets/devcontainer/utils/ssh-agent-proxy.mjs +39 -0
  11. package/dist/bin/cli.cjs +1 -1
  12. package/dist/bin/cli.mjs +1 -1
  13. package/dist/{main-Df4E8ARj.cjs → main-CT2n9qcb.cjs} +907 -266
  14. package/dist/{main-XMBsKjIK.mjs → main-O__JaiXe.mjs} +891 -268
  15. package/dist/main-O__JaiXe.mjs.map +1 -0
  16. package/dist/main.cjs +4 -1
  17. package/dist/main.d.cts +98 -26
  18. package/dist/main.d.cts.map +1 -1
  19. package/dist/main.d.mts +98 -26
  20. package/dist/main.d.mts.map +1 -1
  21. package/dist/main.mjs +2 -2
  22. package/docs/features/commit-signing.md +71 -0
  23. package/docs/features/generated-config-and-state.md +19 -4
  24. package/docs/features/github-auth-refresh.md +3 -2
  25. package/docs/features/lifecycle.md +19 -0
  26. package/docs/features/setup.md +5 -0
  27. package/docs/features/start-and-shell.md +10 -0
  28. package/docs/superpowers/plans/2026-07-17-node-ssh-agent-proxy-signing.md +132 -0
  29. package/docs/superpowers/plans/2026-07-17-runtime-secret-environment.md +174 -0
  30. package/docs/superpowers/plans/2026-07-18-architecture-aware-1password-installer.md +163 -0
  31. package/docs/superpowers/plans/2026-07-18-devcontainer-node-image-digest.md +341 -0
  32. package/docs/superpowers/plans/2026-07-19-container-runtime-readiness.md +1536 -0
  33. package/docs/superpowers/specs/2026-07-11-default-commit-signing-design.md +20 -0
  34. package/docs/superpowers/specs/2026-07-17-node-ssh-agent-proxy-design.md +63 -0
  35. package/docs/superpowers/specs/2026-07-17-runtime-secret-environment-design.md +194 -0
  36. package/docs/superpowers/specs/2026-07-18-architecture-aware-1password-installer-design.md +63 -0
  37. package/docs/superpowers/specs/2026-07-18-devcontainer-node-image-digest-design.md +108 -0
  38. package/docs/superpowers/specs/2026-07-19-container-runtime-readiness-design.md +353 -0
  39. package/package.json +1 -1
  40. package/src/config.ts +14 -2
  41. package/src/constants.ts +7 -0
  42. package/src/container-runtime.ts +295 -0
  43. package/src/devcontainer.ts +57 -30
  44. package/src/doctor.ts +169 -46
  45. package/src/git-signing.ts +205 -25
  46. package/src/logging.ts +11 -1
  47. package/src/main.ts +97 -12
  48. package/src/paths.ts +21 -1
  49. package/src/process.ts +50 -10
  50. package/src/progress.ts +63 -8
  51. package/src/purge.ts +8 -0
  52. package/src/shell.ts +6 -0
  53. package/dist/main-XMBsKjIK.mjs.map +0 -1
@@ -1,8 +1,9 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { appendFileSync, chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
- import { homedir } from "node:os";
4
+ import { homedir, tmpdir } from "node:os";
5
5
  import { basename, delimiter, dirname, isAbsolute, join, parse, relative, resolve } from "node:path";
6
+ import { performance } from "node:perf_hooks";
6
7
  import { spawn } from "node:child_process";
7
8
  import { createInterface } from "node:readline";
8
9
  import { fileURLToPath } from "node:url";
@@ -10,7 +11,7 @@ import { fileURLToPath } from "node:url";
10
11
  //#region src/claude-app-config.ts
11
12
  const CLAUDE_APP_SUPPORT_DIRNAME = "Claude";
12
13
  const CLAUDE_SSH_CONFIGS_FILENAME = "ssh_configs.json";
13
- function isRecord$2(value) {
14
+ function isRecord$1(value) {
14
15
  return value !== null && typeof value === "object" && !Array.isArray(value);
15
16
  }
16
17
  function parseRequiredString(value, key) {
@@ -43,7 +44,7 @@ function parseStringArray(value, key) {
43
44
  });
44
45
  }
45
46
  function parseClaudeSshConfigEntry(value) {
46
- if (!isRecord$2(value)) {
47
+ if (!isRecord$1(value)) {
47
48
  throw new Error("Invalid Claude SSH config: configs entries must be objects");
48
49
  }
49
50
  const source = parseOptionalString(value.source, "source");
@@ -61,7 +62,7 @@ function parseClaudeSshConfigEntry(value) {
61
62
  return parsed;
62
63
  }
63
64
  function parseClaudeSshConfigs(value) {
64
- if (!isRecord$2(value)) {
65
+ if (!isRecord$1(value)) {
65
66
  throw new Error("Invalid Claude SSH config: top-level value must be an object");
66
67
  }
67
68
  const configs = value.configs;
@@ -217,7 +218,7 @@ const CODEX_APP_CONFIG_VERSION = 1;
217
218
  const CODEX_APP_CONFIG_DIRNAME = "codex-app";
218
219
  const CODEX_APP_CONFIG_FILENAME = "config.json";
219
220
  const CODEX_GLOBAL_STATE_FILENAME = ".codex-global-state.json";
220
- function isRecord$1(value) {
221
+ function isRecord(value) {
221
222
  return value !== null && typeof value === "object" && !Array.isArray(value);
222
223
  }
223
224
  function parseOptionalNonnegativeInteger(value, key) {
@@ -239,7 +240,7 @@ function parseOptionalLabel(value) {
239
240
  return value;
240
241
  }
241
242
  function parseProjectConfig(value) {
242
- if (!isRecord$1(value)) {
243
+ if (!isRecord(value)) {
243
244
  throw new Error("Invalid Codex app config: project entries must be objects");
244
245
  }
245
246
  const remotePath = value.remotePath;
@@ -253,7 +254,7 @@ function parseProjectConfig(value) {
253
254
  };
254
255
  }
255
256
  function parseRemoteConnectionConfig(value) {
256
- if (!isRecord$1(value)) {
257
+ if (!isRecord(value)) {
257
258
  throw new Error("Invalid Codex app config: remoteConnections entries must be objects");
258
259
  }
259
260
  const sshAlias = value.sshAlias;
@@ -270,7 +271,7 @@ function parseRemoteConnectionConfig(value) {
270
271
  };
271
272
  }
272
273
  function parseCodexAppConfig(value) {
273
- if (!isRecord$1(value)) {
274
+ if (!isRecord(value)) {
274
275
  throw new Error("Invalid Codex app config: top-level value must be an object");
275
276
  }
276
277
  const version = value.version;
@@ -397,7 +398,7 @@ function cloneJsonObject(value) {
397
398
  return JSON.parse(JSON.stringify(value));
398
399
  }
399
400
  function removeObjectKey(value, key) {
400
- if (isRecord$1(value)) {
401
+ if (isRecord(value)) {
401
402
  delete value[key];
402
403
  }
403
404
  }
@@ -417,7 +418,7 @@ function cleanupCodexStateContainer(container, hostId, remotePaths) {
417
418
  const remoteProjects = container["remote-projects"];
418
419
  if (Array.isArray(remoteProjects)) {
419
420
  container["remote-projects"] = remoteProjects.filter((project) => {
420
- if (!isRecord$1(project)) {
421
+ if (!isRecord(project)) {
421
422
  return true;
422
423
  }
423
424
  const matches = project.hostId === hostId && typeof project.remotePath === "string" && remotePaths.has(normalizeRemotePath(project.remotePath));
@@ -429,7 +430,7 @@ function cleanupCodexStateContainer(container, hostId, remotePaths) {
429
430
  }
430
431
  const managedConnections = container["codex-managed-remote-connections"];
431
432
  if (Array.isArray(managedConnections)) {
432
- container["codex-managed-remote-connections"] = managedConnections.filter((connection) => !isRecord$1(connection) || connection.hostId !== hostId);
433
+ container["codex-managed-remote-connections"] = managedConnections.filter((connection) => !isRecord(connection) || connection.hostId !== hostId);
433
434
  }
434
435
  for (const key of [
435
436
  "remote-connection-analytics-id-by-host-id",
@@ -453,7 +454,7 @@ function cleanupProjectReferences(state, atomState, removedProjectIds) {
453
454
  for (const projectId of removedProjectIds) {
454
455
  removeObjectKey(state["sidebar-collapsed-groups"], projectId);
455
456
  }
456
- if (isRecord$1(atomState)) {
457
+ if (isRecord(atomState)) {
457
458
  cleanupStringArrayField(atomState, "project-order", removedProjectIds);
458
459
  for (const projectId of removedProjectIds) {
459
460
  removeObjectKey(atomState["sidebar-collapsed-groups"], projectId);
@@ -466,7 +467,7 @@ function removeCodexGlobalStateProject(state, entry, options = {}) {
466
467
  const nextState = cloneJsonObject(state);
467
468
  const removedProjectIds = cleanupCodexStateContainer(nextState, hostId, remotePaths);
468
469
  const atomState = nextState["electron-persisted-atom-state"];
469
- if (isRecord$1(atomState)) {
470
+ if (isRecord(atomState)) {
470
471
  for (const projectId of cleanupCodexStateContainer(atomState, hostId, remotePaths)) {
471
472
  removedProjectIds.add(projectId);
472
473
  }
@@ -482,7 +483,7 @@ function normalizeCodexStateContainer(container, hostId, canonicalRemotePath, le
482
483
  }
483
484
  let matchedProject = false;
484
485
  container["remote-projects"] = remoteProjects.flatMap((project) => {
485
- if (!isRecord$1(project) || project.hostId !== hostId || typeof project.remotePath !== "string") {
486
+ if (!isRecord(project) || project.hostId !== hostId || typeof project.remotePath !== "string") {
486
487
  return [project];
487
488
  }
488
489
  const remotePath = normalizeRemotePath(project.remotePath);
@@ -510,7 +511,7 @@ function normalizeCodexGlobalStateProject(state, entry, options = {}) {
510
511
  const nextState = cloneJsonObject(state);
511
512
  const removedProjectIds = normalizeCodexStateContainer(nextState, hostId, canonicalRemotePath, legacyRemotePaths);
512
513
  const atomState = nextState["electron-persisted-atom-state"];
513
- if (isRecord$1(atomState)) {
514
+ if (isRecord(atomState)) {
514
515
  for (const projectId of normalizeCodexStateContainer(atomState, hostId, canonicalRemotePath, legacyRemotePaths)) {
515
516
  removedProjectIds.add(projectId);
516
517
  }
@@ -608,7 +609,7 @@ function uninstallCodexGlobalStateProject(entry, options = {}) {
608
609
  }
609
610
  const existingJson = readFileSync(statePath, "utf8");
610
611
  const existingState = JSON.parse(existingJson);
611
- if (!isRecord$1(existingState)) {
612
+ if (!isRecord(existingState)) {
612
613
  throw new Error(`Invalid Codex global state JSON: ${statePath}`);
613
614
  }
614
615
  const nextState = removeCodexGlobalStateProject(existingState, entry, { additionalRemotePaths: options.additionalRemotePaths });
@@ -638,7 +639,7 @@ function installCodexGlobalStateProject(entry, options = {}) {
638
639
  }
639
640
  const existingJson = readFileSync(statePath, "utf8");
640
641
  const existingState = JSON.parse(existingJson);
641
- if (!isRecord$1(existingState)) {
642
+ if (!isRecord(existingState)) {
642
643
  throw new Error(`Invalid Codex global state JSON: ${statePath}`);
643
644
  }
644
645
  const nextState = normalizeCodexGlobalStateProject(existingState, entry, { legacyRemotePaths: options.legacyRemotePaths });
@@ -691,6 +692,13 @@ const BOXDOWN_CONTAINER_AGENTS_DIR = "/home/node/.agents";
691
692
  const BOXDOWN_CONTAINER_GITCONFIG_PATH = "/home/node/.gitconfig";
692
693
  const BOXDOWN_CONTAINER_CODEX_DIR = "/home/node/.codex";
693
694
  const BOXDOWN_CONTAINER_CODEX_AUTH_PATH = `${BOXDOWN_CONTAINER_CODEX_DIR}/auth.json`;
695
+ const BOXDOWN_CONTAINER_SECRET_ENV_DIR = "/run/boxdown/secrets";
696
+ const BOXDOWN_CONTAINER_SECRET_ENV_BOOTSTRAP = `${BOXDOWN_CONTAINER_DEVCONTAINER_DIR}/utils/secret-env-bootstrap.sh`;
697
+ const BOXDOWN_SECRET_ENV_NAMES = [
698
+ "ANTHROPIC_API_KEY",
699
+ "SNYK_TOKEN",
700
+ "OP_SERVICE_ACCOUNT_TOKEN"
701
+ ];
694
702
  const PACKAGE_NAME = "boxdown";
695
703
 
696
704
  //#endregion
@@ -844,10 +852,14 @@ function interactiveTermSetupScript() {
844
852
  function interactiveAgentPathScript() {
845
853
  return ["codex_home=\"${CODEX_HOME:-${HOME}/.codex}\"", "export PATH=\"${HOME}/.local/bin:${HOME}/.opencode/bin:${codex_home}/packages/standalone/current/bin:${PATH}\""].join("\n");
846
854
  }
855
+ function runtimeSecretEnvironmentScript() {
856
+ return "source \"${BASH_ENV:-/opt/boxdown/devcontainer/utils/secret-env-bootstrap.sh}\"";
857
+ }
847
858
  function interactiveShellScript() {
848
859
  return [
849
860
  interactiveTermSetupScript(),
850
861
  interactiveTtySetupScript(),
862
+ runtimeSecretEnvironmentScript(),
851
863
  "exec bash -i"
852
864
  ].join("\n");
853
865
  }
@@ -856,6 +868,7 @@ function interactiveCommandScript() {
856
868
  interactiveTermSetupScript(),
857
869
  interactiveTtySetupScript(),
858
870
  interactiveAgentPathScript(),
871
+ runtimeSecretEnvironmentScript(),
859
872
  "exec \"$@\""
860
873
  ].join("\n");
861
874
  }
@@ -892,7 +905,8 @@ function buildGeneratedDevcontainerConfig(context, signing) {
892
905
  const boxdownMounts = [
893
906
  `type=bind,source=${context.assetsDevcontainerDir},target=${BOXDOWN_CONTAINER_DEVCONTAINER_DIR},readonly`,
894
907
  `type=bind,source=${context.sshPublicKeyRuntimeDir},target=${BOXDOWN_CONTAINER_SSH_DIR},readonly`,
895
- `type=bind,source=${context.hostGitconfigSnapshotDir},target=${BOXDOWN_CONTAINER_HOST_GITCONFIG_DIR},readonly`
908
+ `type=bind,source=${context.hostGitconfigSnapshotDir},target=${BOXDOWN_CONTAINER_HOST_GITCONFIG_DIR},readonly`,
909
+ `type=bind,source=${context.workspaceSecretEnvDir},target=${BOXDOWN_CONTAINER_SECRET_ENV_DIR},readonly`
896
910
  ];
897
911
  if (directoryExists(context.hostAgentsDir) && !hasMountTarget(mounts, "/home/node/.agents")) {
898
912
  boxdownMounts.push(`type=bind,source=${context.hostAgentsDir},target=${BOXDOWN_CONTAINER_AGENTS_DIR},readonly`);
@@ -912,6 +926,7 @@ function buildGeneratedDevcontainerConfig(context, signing) {
912
926
  `BOXDOWN_WORKSPACE_FOLDER=${shellQuote(context.workspaceFolder)}`,
913
927
  `BOXDOWN_HOST_GITCONFIG_PATH=${shellQuote(context.hostGitconfigPath)}`,
914
928
  `BOXDOWN_HOST_GITCONFIG_SNAPSHOT_PATH=${shellQuote(context.hostGitconfigSnapshotPath)}`,
929
+ `BOXDOWN_SECRET_ENV_DIR=${shellQuote(context.workspaceSecretEnvDir)}`,
915
930
  `BOXDOWN_PROGRESS=${shellQuote("${localEnv:BOXDOWN_PROGRESS}")}`,
916
931
  `BOXDOWN_VERBOSE=${shellQuote("${localEnv:BOXDOWN_VERBOSE}")}`,
917
932
  "bash",
@@ -933,10 +948,16 @@ function buildGeneratedDevcontainerConfig(context, signing) {
933
948
  ...baseConfig.containerEnv ?? {},
934
949
  BOXDOWN_CONTAINER_WORKSPACE_FOLDER: "/workspaces/${localWorkspaceFolderBasename}",
935
950
  BOXDOWN_WORKSPACE_BASENAME: "${localWorkspaceFolderBasename}",
951
+ BOXDOWN_SECRET_ENV_DIR: BOXDOWN_CONTAINER_SECRET_ENV_DIR,
952
+ BASH_ENV: BOXDOWN_CONTAINER_SECRET_ENV_BOOTSTRAP,
936
953
  DEVCONTAINER_SSH_PUBLIC_KEY_FILE: BOXDOWN_CONTAINER_SSH_PUBLIC_KEY_PATH,
937
954
  BOXDOWN_GIT_SIGNING_ENABLED: signing?.enabled === true ? "1" : "0",
938
955
  BOXDOWN_GIT_SIGNING_KEY_PATH: "/opt/boxdown/state/git-signing/signing-key.pub",
939
- ...signing?.enabled === true ? { SSH_AUTH_SOCK: "/run/boxdown/ssh-agent.sock" } : {}
956
+ ...signing?.enabled === false && signing.reason !== undefined ? { BOXDOWN_GIT_SIGNING_REASON: signing.reason } : {},
957
+ ...signing?.enabled === true ? {
958
+ BOXDOWN_GIT_SIGNING_SOURCE_SOCKET: "/run/boxdown/ssh-agent.sock",
959
+ SSH_AUTH_SOCK: "/run/boxdown/ssh-agent-node.sock"
960
+ } : {}
940
961
  }
941
962
  };
942
963
  }
@@ -950,41 +971,12 @@ function publishContainerPortFromConfig(config) {
950
971
  return config.runArgs?.find((arg) => /^[0-9.]+::[0-9]+$/.test(arg))?.split("::")[1];
951
972
  }
952
973
 
953
- //#endregion
954
- //#region src/devcontainer-cli.ts
955
- function devcontainerBinFromPackageJson(packageJson) {
956
- return typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.devcontainer;
957
- }
958
- function resolveDevcontainerCli(context) {
959
- const requireFromBoxdown = createRequire(join(context.packageRoot, "package.json"));
960
- let packageJsonPath;
961
- try {
962
- packageJsonPath = requireFromBoxdown.resolve("@devcontainers/cli/package.json");
963
- } catch (error) {
964
- throw new Error(`Boxdown's packaged @devcontainers/cli dependency is missing. Reinstall boxdown so @devcontainers/cli@${DEVCONTAINER_CLI_VERSION} is available.`, { cause: error });
965
- }
966
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
967
- const bin = devcontainerBinFromPackageJson(packageJson);
968
- if (packageJson.version !== "0.84.1") {
969
- throw new Error(`Boxdown expected @devcontainers/cli@${DEVCONTAINER_CLI_VERSION} but resolved ${packageJson.version ?? "an unknown version"}.`);
970
- }
971
- if (bin === undefined) {
972
- throw new Error("Boxdown resolved @devcontainers/cli, but the package does not expose a devcontainer binary.");
973
- }
974
- const cliPath = resolve(dirname(packageJsonPath), bin);
975
- if (!existsSync(cliPath)) {
976
- throw new Error(`Boxdown resolved @devcontainers/cli, but its devcontainer binary is missing: ${cliPath}`);
977
- }
978
- return {
979
- command: process.execPath,
980
- argsPrefix: [cliPath],
981
- path: cliPath,
982
- version: packageJson.version
983
- };
984
- }
985
-
986
974
  //#endregion
987
975
  //#region src/process.ts
976
+ const realTimeoutControl = {
977
+ schedule: (callback, milliseconds) => setTimeout(callback, milliseconds),
978
+ cancel: (handle) => clearTimeout(handle)
979
+ };
988
980
  function mergedEnv(env) {
989
981
  const baseEnv = {
990
982
  ...process.env,
@@ -1037,6 +1029,9 @@ function writeChunk(target, chunk) {
1037
1029
  }
1038
1030
  }
1039
1031
  function runBuffered(command, args, options = {}) {
1032
+ if (options.timeoutMs !== undefined && (!Number.isFinite(options.timeoutMs) || options.timeoutMs < 0)) {
1033
+ throw new Error("timeoutMs must be a finite non-negative number");
1034
+ }
1040
1035
  return new Promise((resolve) => {
1041
1036
  const loggedCommand = options.logger?.startCommand(command, args, { cwd: options.cwd });
1042
1037
  const child = spawn(command, args, {
@@ -1050,24 +1045,28 @@ function runBuffered(command, args, options = {}) {
1050
1045
  });
1051
1046
  const stdoutChunks = [];
1052
1047
  const stderrChunks = [];
1048
+ let settled = false;
1049
+ let timeoutHandle;
1050
+ let timeoutScheduled = false;
1051
+ const timeoutControl = options.timeoutControl ?? realTimeoutControl;
1053
1052
  child.stdout.on("data", (chunk) => {
1053
+ if (settled) return;
1054
1054
  stdoutChunks.push(chunk);
1055
1055
  loggedCommand?.stream("stdout", chunk);
1056
1056
  options.onStdout?.(chunk);
1057
1057
  writeChunk(options.mirrorStdout ?? "stdout", chunk);
1058
1058
  });
1059
1059
  child.stderr.on("data", (chunk) => {
1060
+ if (settled) return;
1060
1061
  stderrChunks.push(chunk);
1061
1062
  loggedCommand?.stream("stderr", chunk);
1062
1063
  options.onStderr?.(chunk);
1063
1064
  writeChunk(options.mirrorStderr ?? "stderr", chunk);
1064
1065
  });
1065
- let resolved = false;
1066
1066
  child.on("error", (error) => {
1067
- if (resolved) {
1068
- return;
1069
- }
1070
- resolved = true;
1067
+ if (settled) return;
1068
+ settled = true;
1069
+ if (timeoutScheduled) timeoutControl.cancel(timeoutHandle);
1071
1070
  loggedCommand?.error(error);
1072
1071
  loggedCommand?.finish(127);
1073
1072
  resolve({
@@ -1077,10 +1076,9 @@ function runBuffered(command, args, options = {}) {
1077
1076
  });
1078
1077
  });
1079
1078
  child.on("close", (code) => {
1080
- if (resolved) {
1081
- return;
1082
- }
1083
- resolved = true;
1079
+ if (settled) return;
1080
+ settled = true;
1081
+ if (timeoutScheduled) timeoutControl.cancel(timeoutHandle);
1084
1082
  loggedCommand?.finish(code ?? 1);
1085
1083
  resolve({
1086
1084
  code: code ?? 1,
@@ -1088,6 +1086,25 @@ function runBuffered(command, args, options = {}) {
1088
1086
  stderr: Buffer.concat(stderrChunks).toString("utf8")
1089
1087
  });
1090
1088
  });
1089
+ if (options.timeoutMs !== undefined) {
1090
+ timeoutHandle = timeoutControl.schedule(() => {
1091
+ if (settled) return;
1092
+ settled = true;
1093
+ const message = `Command timed out after ${options.timeoutMs} milliseconds.`;
1094
+ const stdout = Buffer.concat(stdoutChunks).toString("utf8");
1095
+ const stderr = Buffer.concat(stderrChunks).toString("utf8");
1096
+ loggedCommand?.error(new Error(message));
1097
+ loggedCommand?.finish(124);
1098
+ child.kill("SIGKILL");
1099
+ resolve({
1100
+ code: 124,
1101
+ stdout,
1102
+ stderr: `${stderr}${stderr.length > 0 && !stderr.endsWith("\n") ? "\n" : ""}${message}\n`,
1103
+ timedOut: true
1104
+ });
1105
+ }, options.timeoutMs);
1106
+ timeoutScheduled = true;
1107
+ }
1091
1108
  if (options.input !== undefined) {
1092
1109
  child.stdin.end(options.input);
1093
1110
  } else {
@@ -1116,6 +1133,490 @@ function runInteractive(command, args, options = {}) {
1116
1133
  });
1117
1134
  }
1118
1135
 
1136
+ //#endregion
1137
+ //#region src/container-runtime.ts
1138
+ const BUILDX_FALLBACK_WARNING = "Docker Buildx is unavailable; the Dev Containers CLI will use its classic-build fallback.";
1139
+ async function runContainerRuntimeCommand(command, args, timeoutMs) {
1140
+ return runBuffered(command, args, {
1141
+ mirrorStdout: false,
1142
+ mirrorStderr: false,
1143
+ timeoutMs
1144
+ });
1145
+ }
1146
+ function compactCommandOutput(result) {
1147
+ const output = result.stderr.trim().length > 0 ? result.stderr : result.stdout;
1148
+ const compact = output.trim().replace(/\s+/gu, " ").slice(0, 500);
1149
+ return compact.length > 0 ? compact : `Command exited with code ${result.code}`;
1150
+ }
1151
+ function failure(reason, command, result) {
1152
+ return {
1153
+ reason,
1154
+ command,
1155
+ detail: compactCommandOutput(result),
1156
+ ...result.timedOut === true ? { timedOut: true } : {}
1157
+ };
1158
+ }
1159
+ async function probeContainerRuntime(runCommand = runContainerRuntimeCommand) {
1160
+ const dockerVersionCommand = ["docker", "--version"];
1161
+ const dockerVersion = await runCommand(dockerVersionCommand[0], dockerVersionCommand.slice(1));
1162
+ if (dockerVersion.code !== 0) {
1163
+ return {
1164
+ state: "failed",
1165
+ failure: failure("docker-cli-unavailable", dockerVersionCommand, dockerVersion)
1166
+ };
1167
+ }
1168
+ const dockerInfoCommand = ["docker", "info"];
1169
+ const dockerInfo = await runCommand(dockerInfoCommand[0], dockerInfoCommand.slice(1));
1170
+ if (dockerInfo.code !== 0) {
1171
+ return {
1172
+ state: "waiting",
1173
+ failure: failure("docker-daemon-unavailable", dockerInfoCommand, dockerInfo)
1174
+ };
1175
+ }
1176
+ const buildxVersionCommand = [
1177
+ "docker",
1178
+ "buildx",
1179
+ "version"
1180
+ ];
1181
+ const buildxVersion = await runCommand(buildxVersionCommand[0], buildxVersionCommand.slice(1));
1182
+ if (buildxVersion.timedOut === true) {
1183
+ return {
1184
+ state: "waiting",
1185
+ failure: failure("buildx-builder-unavailable", buildxVersionCommand, buildxVersion)
1186
+ };
1187
+ }
1188
+ if (buildxVersion.code !== 0) {
1189
+ return {
1190
+ state: "ready",
1191
+ mode: "fallback",
1192
+ warnings: [BUILDX_FALLBACK_WARNING]
1193
+ };
1194
+ }
1195
+ const buildxInspectCommand = [
1196
+ "docker",
1197
+ "buildx",
1198
+ "inspect",
1199
+ "--bootstrap"
1200
+ ];
1201
+ const buildxInspect = await runCommand(buildxInspectCommand[0], buildxInspectCommand.slice(1));
1202
+ if (buildxInspect.code !== 0) {
1203
+ return {
1204
+ state: "waiting",
1205
+ failure: failure("buildx-builder-unavailable", buildxInspectCommand, buildxInspect)
1206
+ };
1207
+ }
1208
+ return {
1209
+ state: "ready",
1210
+ mode: "buildx",
1211
+ warnings: []
1212
+ };
1213
+ }
1214
+ function defaultSleep(milliseconds) {
1215
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
1216
+ }
1217
+ function transitionKey(probe) {
1218
+ return probe.state === "ready" ? "ready" : `${probe.state}:${probe.failure.reason}`;
1219
+ }
1220
+ var ContainerRuntimeDeadlineError = class extends Error {
1221
+ command;
1222
+ constructor(command) {
1223
+ super(`Container runtime deadline expired before ${command.join(" ")}`);
1224
+ this.command = command;
1225
+ }
1226
+ };
1227
+ function deadlineFailure(command) {
1228
+ const reason = command[1] === "info" ? "docker-daemon-unavailable" : command[1] === "buildx" ? "buildx-builder-unavailable" : "docker-cli-unavailable";
1229
+ return {
1230
+ reason,
1231
+ command,
1232
+ detail: `Readiness deadline expired before ${command.join(" ")} could start.`,
1233
+ timedOut: true
1234
+ };
1235
+ }
1236
+ /** @internal */
1237
+ async function waitForContainerRuntimeInternal(options = {}) {
1238
+ const timeoutMs = options.timeoutMs ?? 6e4;
1239
+ const pollIntervalMs = options.pollIntervalMs ?? 1e3;
1240
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0 || timeoutMs > 6e4) {
1241
+ throw new Error("timeoutMs must be a finite number between 0 and 60000 milliseconds");
1242
+ }
1243
+ if (pollIntervalMs !== 1e3) {
1244
+ throw new Error("pollIntervalMs must be exactly 1000 milliseconds");
1245
+ }
1246
+ const now = options.now ?? Date.now;
1247
+ const sleep = options.sleep ?? defaultSleep;
1248
+ const deadline = now() + timeoutMs;
1249
+ let lastTransition;
1250
+ let lastWaitingFailure;
1251
+ while (true) {
1252
+ if (lastWaitingFailure !== undefined && deadline - now() <= 0) {
1253
+ return {
1254
+ state: "failed",
1255
+ failure: lastWaitingFailure,
1256
+ timedOut: true,
1257
+ timeoutMs
1258
+ };
1259
+ }
1260
+ const runCommand = options.runCommand ?? runContainerRuntimeCommand;
1261
+ let lastStartedCommand;
1262
+ let probe;
1263
+ try {
1264
+ probe = await probeContainerRuntime(async (command, args) => {
1265
+ const commandArgs = [command, ...args];
1266
+ const remainingMs = Math.floor(deadline - now());
1267
+ if (remainingMs <= 0) throw new ContainerRuntimeDeadlineError(commandArgs);
1268
+ lastStartedCommand = commandArgs;
1269
+ return runCommand(command, args, remainingMs);
1270
+ });
1271
+ } catch (error) {
1272
+ if (!(error instanceof ContainerRuntimeDeadlineError)) throw error;
1273
+ return {
1274
+ state: "failed",
1275
+ failure: lastWaitingFailure ?? deadlineFailure(error.command),
1276
+ timedOut: true,
1277
+ timeoutMs
1278
+ };
1279
+ }
1280
+ if (deadline - now() <= 0) {
1281
+ const currentFailure = probe.state === "ready" ? deadlineFailure(lastStartedCommand ?? ["docker", "--version"]) : probe.failure;
1282
+ return {
1283
+ state: "failed",
1284
+ failure: lastWaitingFailure ?? currentFailure,
1285
+ timedOut: true,
1286
+ timeoutMs
1287
+ };
1288
+ }
1289
+ if (probe.state === "ready") return probe;
1290
+ const currentTransition = transitionKey(probe);
1291
+ if (currentTransition !== lastTransition) {
1292
+ options.onTransition?.(probe);
1293
+ lastTransition = currentTransition;
1294
+ }
1295
+ if (probe.state === "failed") {
1296
+ return {
1297
+ state: "failed",
1298
+ failure: probe.failure,
1299
+ timedOut: probe.failure.timedOut === true,
1300
+ timeoutMs
1301
+ };
1302
+ }
1303
+ lastWaitingFailure = probe.failure;
1304
+ const remainingMs = deadline - now();
1305
+ if (remainingMs <= 0) {
1306
+ return {
1307
+ state: "failed",
1308
+ failure: probe.failure,
1309
+ timedOut: true,
1310
+ timeoutMs
1311
+ };
1312
+ }
1313
+ await sleep(Math.min(pollIntervalMs, remainingMs));
1314
+ }
1315
+ }
1316
+ async function waitForContainerRuntime(options = {}) {
1317
+ return waitForContainerRuntimeInternal({
1318
+ runCommand: options.runCommand,
1319
+ onTransition: options.onTransition,
1320
+ timeoutMs: 6e4,
1321
+ pollIntervalMs: 1e3,
1322
+ now: () => performance.now(),
1323
+ sleep: defaultSleep
1324
+ });
1325
+ }
1326
+ function commandText(command) {
1327
+ return command.join(" ");
1328
+ }
1329
+ function formatContainerRuntimeFailure(result, options = {}) {
1330
+ const seconds = result.timeoutMs / 1e3;
1331
+ const descriptions = {
1332
+ "docker-cli-unavailable": "Docker CLI is required but was not available.",
1333
+ "docker-daemon-unavailable": result.timedOut ? `Docker daemon did not become ready within ${seconds} seconds.` : "Docker daemon is required but was not reachable.",
1334
+ "buildx-builder-unavailable": result.timedOut ? `Docker Buildx builder did not become ready within ${seconds} seconds.` : "Docker Buildx builder was not operational."
1335
+ };
1336
+ const manualCheck = result.failure.reason === "buildx-builder-unavailable" ? "docker buildx inspect" : result.failure.reason === "docker-daemon-unavailable" ? "docker info" : "docker --version";
1337
+ const lines = [
1338
+ descriptions[result.failure.reason],
1339
+ `Last check: ${commandText(result.failure.command)}`,
1340
+ `Detail: ${result.failure.detail}`,
1341
+ `Check ${result.failure.reason === "buildx-builder-unavailable" ? "Buildx" : "Docker"} with: ${manualCheck}`
1342
+ ];
1343
+ if (options.logPath !== undefined) lines.push(`Command log: ${options.logPath}`);
1344
+ return lines.join("\n");
1345
+ }
1346
+
1347
+ //#endregion
1348
+ //#region src/devcontainer-cli.ts
1349
+ function devcontainerBinFromPackageJson(packageJson) {
1350
+ return typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.devcontainer;
1351
+ }
1352
+ function resolveDevcontainerCli(context) {
1353
+ const requireFromBoxdown = createRequire(join(context.packageRoot, "package.json"));
1354
+ let packageJsonPath;
1355
+ try {
1356
+ packageJsonPath = requireFromBoxdown.resolve("@devcontainers/cli/package.json");
1357
+ } catch (error) {
1358
+ throw new Error(`Boxdown's packaged @devcontainers/cli dependency is missing. Reinstall boxdown so @devcontainers/cli@${DEVCONTAINER_CLI_VERSION} is available.`, { cause: error });
1359
+ }
1360
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
1361
+ const bin = devcontainerBinFromPackageJson(packageJson);
1362
+ if (packageJson.version !== "0.84.1") {
1363
+ throw new Error(`Boxdown expected @devcontainers/cli@${DEVCONTAINER_CLI_VERSION} but resolved ${packageJson.version ?? "an unknown version"}.`);
1364
+ }
1365
+ if (bin === undefined) {
1366
+ throw new Error("Boxdown resolved @devcontainers/cli, but the package does not expose a devcontainer binary.");
1367
+ }
1368
+ const cliPath = resolve(dirname(packageJsonPath), bin);
1369
+ if (!existsSync(cliPath)) {
1370
+ throw new Error(`Boxdown resolved @devcontainers/cli, but its devcontainer binary is missing: ${cliPath}`);
1371
+ }
1372
+ return {
1373
+ command: process.execPath,
1374
+ argsPrefix: [cliPath],
1375
+ path: cliPath,
1376
+ version: packageJson.version
1377
+ };
1378
+ }
1379
+
1380
+ //#endregion
1381
+ //#region src/git-signing.ts
1382
+ const GIT_SIGNING_REASON_MESSAGES = {
1383
+ "agent-unavailable": "the host SSH agent is unavailable",
1384
+ "no-identities": "the host SSH agent has no loaded identities",
1385
+ "ambiguous-identities": "multiple SSH identities are loaded and no signing key could be selected safely",
1386
+ "configured-key-unreadable": "the configured SSH signing-key file could not be read",
1387
+ "configured-key-invalid": "the configured SSH signing key is not a valid public key",
1388
+ "configured-key-not-loaded": "the configured SSH signing key is not loaded in the agent",
1389
+ "agent-socket-unavailable": "the host SSH-agent socket is unavailable",
1390
+ "docker-probe-image-unavailable": "no local Docker image is available to probe the SSH-agent mount",
1391
+ "agent-mount-unavailable": "Docker could not mount the host SSH-agent socket"
1392
+ };
1393
+ function compactDiagnosticDetail(detail) {
1394
+ return detail.trim().replace(/\s+/gu, " ").slice(0, 2e3).replace(/-----BEGIN [^-]*PRIVATE KEY-----.*?(?:-----END [^-]*PRIVATE KEY-----|$)/giu, "[redacted-private-key]").replace(/\b(?:ssh-[A-Za-z0-9-]+|ecdsa-sha2-[A-Za-z0-9-]+|sk-(?:ssh-[A-Za-z0-9-]+|ecdsa-sha2-[A-Za-z0-9-]+))\s+[A-Za-z0-9+/=]{16,}/gu, "[redacted-ssh-key]").replace(/\b(?:github_pat_|gh[pousr]_|glpat-|xox[baprs]-)[A-Za-z0-9_-]{8,}/gu, "[redacted-token]").replace(/\b(?:Bearer\s+|token[=:]\s*)[A-Za-z0-9._~+/-]{8,}/giu, "[redacted-token]").slice(0, 300);
1395
+ }
1396
+ function reportGitSigningPlan(plan, options = {}) {
1397
+ if (plan.enabled) return;
1398
+ const reason = plan.reason ?? "agent-unavailable";
1399
+ const detail = plan.detail === undefined ? "" : ` detail=${compactDiagnosticDetail(plan.detail)}`;
1400
+ options.logger?.boxdown(`git-signing: enabled=false reason=${reason}${detail}\n`);
1401
+ if (options.quiet !== true) {
1402
+ const writeWarning = options.writeWarning ?? ((message) => process.stderr.write(message));
1403
+ writeWarning(`boxdown: commit signing disabled: ${GIT_SIGNING_REASON_MESSAGES[reason]}; commits will remain unsigned.\n`);
1404
+ }
1405
+ }
1406
+ function parseSshPublicKey(value) {
1407
+ const [algorithm, key] = value.trim().split(/\s+/, 3);
1408
+ if (algorithm === undefined || key === undefined || !/^ssh-[A-Za-z0-9-]+$/.test(algorithm) || key.length === 0) {
1409
+ return undefined;
1410
+ }
1411
+ return `${algorithm} ${key}`;
1412
+ }
1413
+ function resolveConfiguredSshSigningKey(value, options) {
1414
+ const inlineValue = value.startsWith("key::") ? value.slice("key::".length) : value;
1415
+ const inlineKey = parseSshPublicKey(inlineValue);
1416
+ if (inlineKey !== undefined) {
1417
+ return { key: inlineKey };
1418
+ }
1419
+ if (value.startsWith("key::")) {
1420
+ return {
1421
+ reason: "configured-key-invalid",
1422
+ detail: "configured inline value is not a valid SSH public key"
1423
+ };
1424
+ }
1425
+ let keyPath;
1426
+ if (value.startsWith("~/")) {
1427
+ if (options.homeDir === undefined) {
1428
+ return {
1429
+ reason: "configured-key-unreadable",
1430
+ detail: "configured public-key file could not be read"
1431
+ };
1432
+ }
1433
+ keyPath = join(options.homeDir, value.slice(2));
1434
+ } else {
1435
+ keyPath = isAbsolute(value) ? value : resolve(options.workspaceFolder, value);
1436
+ }
1437
+ let publicKeyText;
1438
+ try {
1439
+ publicKeyText = readFileSync(keyPath, "utf8");
1440
+ } catch {
1441
+ return {
1442
+ reason: "configured-key-unreadable",
1443
+ detail: "configured public-key file could not be read"
1444
+ };
1445
+ }
1446
+ const publicKey = parseSshPublicKey(publicKeyText.split(/\r?\n/u).find((line) => line.trim().length > 0) ?? "");
1447
+ return publicKey === undefined ? {
1448
+ reason: "configured-key-invalid",
1449
+ detail: "configured public-key file does not contain a valid SSH public key"
1450
+ } : { key: publicKey };
1451
+ }
1452
+ function selectGitSigningKey(identities, configuredKey, githubKeys) {
1453
+ const keys = identities.map(parseSshPublicKey).filter((key) => key !== undefined);
1454
+ if (keys.length === 0) return { reason: "no-identities" };
1455
+ const configured = configuredKey === undefined ? undefined : parseSshPublicKey(configuredKey);
1456
+ if (configuredKey !== undefined && configured === undefined) {
1457
+ return { reason: "configured-key-invalid" };
1458
+ }
1459
+ if (configured !== undefined) {
1460
+ return keys.includes(configured) ? { key: configured } : { reason: "configured-key-not-loaded" };
1461
+ }
1462
+ const github = new Set((githubKeys ?? []).map(parseSshPublicKey).filter((key) => key !== undefined));
1463
+ const matches = keys.filter((key) => github.has(key));
1464
+ if (matches.length === 1) return { key: matches[0] };
1465
+ if (keys.length === 1) return { key: keys[0] };
1466
+ return { reason: "ambiguous-identities" };
1467
+ }
1468
+ function writeGitSigningPublicKey(context, key) {
1469
+ mkdirSync(context.gitSigningStateDir, {
1470
+ recursive: true,
1471
+ mode: 448
1472
+ });
1473
+ writeFileSync(join(context.gitSigningStateDir, "signing-key.pub"), `${parseSshPublicKey(key) ?? key}\n`, { mode: 420 });
1474
+ }
1475
+ async function runGitSigningCommand(command, args) {
1476
+ return runBuffered(command, args, {
1477
+ mirrorStdout: false,
1478
+ mirrorStderr: false
1479
+ });
1480
+ }
1481
+ function failedCommandDetail(label, result) {
1482
+ const stderr = compactDiagnosticDetail(result.stderr);
1483
+ return stderr.length === 0 ? `${label} failed with exit code ${result.code}` : `${label} failed with exit code ${result.code}: ${stderr}`;
1484
+ }
1485
+ async function probeDockerAgentMount(source, runCommand) {
1486
+ const images = await runCommand("docker", [
1487
+ "image",
1488
+ "ls",
1489
+ "--format",
1490
+ "{{.Repository}}:{{.Tag}}"
1491
+ ]);
1492
+ const image = images.stdout.split(/\r?\n/).map((value) => value.trim()).find((value) => value.length > 0 && value !== "<none>:<none>");
1493
+ if (images.code !== 0) {
1494
+ return {
1495
+ ok: false,
1496
+ reason: "agent-mount-unavailable",
1497
+ detail: failedCommandDetail("Docker image listing", images)
1498
+ };
1499
+ }
1500
+ if (image === undefined) {
1501
+ return {
1502
+ ok: false,
1503
+ reason: "docker-probe-image-unavailable",
1504
+ detail: "no tagged local Docker image was found"
1505
+ };
1506
+ }
1507
+ const created = await runCommand("docker", [
1508
+ "create",
1509
+ "--pull=never",
1510
+ "--entrypoint",
1511
+ "/bin/true",
1512
+ "--mount",
1513
+ `type=bind,source=${source},target=/run/boxdown/ssh-agent.sock`,
1514
+ image
1515
+ ]);
1516
+ const containerId = created.stdout.trim().split(/\r?\n/)[0];
1517
+ if (created.code !== 0 || containerId === undefined || containerId.length === 0) {
1518
+ return {
1519
+ ok: false,
1520
+ reason: "agent-mount-unavailable",
1521
+ detail: failedCommandDetail("Docker SSH-agent mount probe", created)
1522
+ };
1523
+ }
1524
+ await runCommand("docker", [
1525
+ "rm",
1526
+ "-f",
1527
+ containerId
1528
+ ]);
1529
+ return { ok: true };
1530
+ }
1531
+ async function resolveGitSigningPlan(context, options = {}) {
1532
+ const runCommand = options.runCommand ?? runGitSigningCommand;
1533
+ const result = await runCommand("ssh-add", ["-L"]);
1534
+ if (result.code !== 0) {
1535
+ return {
1536
+ enabled: false,
1537
+ reason: "agent-unavailable",
1538
+ detail: failedCommandDetail("ssh-add -L", result)
1539
+ };
1540
+ }
1541
+ const identities = result.stdout.split(/\r?\n/);
1542
+ const format = await runCommand("git", [
1543
+ "config",
1544
+ "--global",
1545
+ "--get",
1546
+ "gpg.format"
1547
+ ]);
1548
+ let configuredKey;
1549
+ if (format.code === 0 && format.stdout.trim() === "ssh") {
1550
+ const signingKey = await runCommand("git", [
1551
+ "config",
1552
+ "--global",
1553
+ "--get",
1554
+ "user.signingkey"
1555
+ ]);
1556
+ if (signingKey.code === 0 && signingKey.stdout.trim().length > 0) {
1557
+ const resolvedKey = resolveConfiguredSshSigningKey(signingKey.stdout.trim(), {
1558
+ homeDir: options.env?.HOME ?? process.env.HOME,
1559
+ workspaceFolder: context.workspaceFolder
1560
+ });
1561
+ if (resolvedKey.key === undefined) {
1562
+ return {
1563
+ enabled: false,
1564
+ reason: resolvedKey.reason ?? "configured-key-invalid",
1565
+ detail: resolvedKey.detail
1566
+ };
1567
+ }
1568
+ configuredKey = resolvedKey.key;
1569
+ }
1570
+ }
1571
+ let githubKeys;
1572
+ if (configuredKey === undefined && identities.filter((key) => parseSshPublicKey(key) !== undefined).length > 1) {
1573
+ const user = await runCommand("gh", [
1574
+ "api",
1575
+ "user",
1576
+ "--jq",
1577
+ ".login"
1578
+ ]);
1579
+ const login = user.stdout.trim();
1580
+ if (user.code === 0 && login.length > 0) {
1581
+ const github = await runCommand("gh", [
1582
+ "api",
1583
+ `users/${login}/keys`,
1584
+ "--paginate",
1585
+ "--jq",
1586
+ ".[].key"
1587
+ ]);
1588
+ if (github.code === 0) githubKeys = github.stdout.split(/\r?\n/);
1589
+ }
1590
+ }
1591
+ const selected = selectGitSigningKey(identities, configuredKey, githubKeys);
1592
+ if (selected.key === undefined) return {
1593
+ enabled: false,
1594
+ reason: selected.reason
1595
+ };
1596
+ const agentSocketSource = (options.platform ?? process.platform) === "darwin" ? "/run/host-services/ssh-auth.sock" : options.env?.SSH_AUTH_SOCK ?? process.env.SSH_AUTH_SOCK;
1597
+ if (agentSocketSource === undefined || agentSocketSource.length === 0) {
1598
+ return {
1599
+ enabled: false,
1600
+ reason: "agent-socket-unavailable",
1601
+ detail: "SSH_AUTH_SOCK is not set"
1602
+ };
1603
+ }
1604
+ const mountProbe = await probeDockerAgentMount(agentSocketSource, runCommand);
1605
+ if (!mountProbe.ok) {
1606
+ return {
1607
+ enabled: false,
1608
+ reason: mountProbe.reason,
1609
+ detail: mountProbe.detail
1610
+ };
1611
+ }
1612
+ writeGitSigningPublicKey(context, selected.key);
1613
+ return {
1614
+ enabled: true,
1615
+ publicKey: selected.key,
1616
+ agentSocketSource
1617
+ };
1618
+ }
1619
+
1119
1620
  //#endregion
1120
1621
  //#region src/doctor.ts
1121
1622
  function nodeVersionPasses(version) {
@@ -1129,6 +1630,26 @@ function check(name, pass, okMessage, failMessage) {
1129
1630
  message: pass ? okMessage : failMessage
1130
1631
  };
1131
1632
  }
1633
+ function secretEnvironmentConfigCheck(context) {
1634
+ let config;
1635
+ try {
1636
+ config = existsSync(context.generatedConfigPath) ? JSON.parse(readFileSync(context.generatedConfigPath, "utf8")) : buildGeneratedDevcontainerConfig(context);
1637
+ } catch {
1638
+ return {
1639
+ name: "secret-environment-config",
1640
+ level: "warn",
1641
+ message: "Generated config could not be checked for secret-safe environment handling"
1642
+ };
1643
+ }
1644
+ const runArgs = Array.isArray(config.runArgs) ? config.runArgs : [];
1645
+ const containerEnv = config.containerEnv ?? {};
1646
+ const unsafe = runArgs.includes("--env-file") || runArgs.some((arg) => arg.includes(".env.development")) || BOXDOWN_SECRET_ENV_NAMES.some((name) => Object.hasOwn(containerEnv, name));
1647
+ return {
1648
+ name: "secret-environment-config",
1649
+ level: unsafe ? "warn" : "ok",
1650
+ message: unsafe ? "Generated config still exposes Boxdown secrets through Docker environment settings; recreate after upgrading Boxdown" : "Generated config uses runtime-mounted secrets without Docker environment values"
1651
+ };
1652
+ }
1132
1653
  async function runDoctorCommand(command, args) {
1133
1654
  return runBuffered(command, args, {
1134
1655
  mirrorStdout: false,
@@ -1149,46 +1670,154 @@ async function runDoctorChecks(context, options = {}) {
1149
1670
  const nodeVersion = process.versions.node;
1150
1671
  checks.push(check("node", nodeVersionPasses(nodeVersion), `Node ${nodeVersion}`, `Node ${nodeVersion}; expected >=24.0.0`));
1151
1672
  const sshAgent = await runCommand("ssh-add", ["-L"]);
1152
- const identities = sshAgent.code === 0 ? sshAgent.stdout.split(/\r?\n/).filter((line) => line.trim().startsWith("ssh-")).length : 0;
1673
+ const identityLines = sshAgent.code === 0 ? sshAgent.stdout.split(/\r?\n/).filter((line) => line.trim().startsWith("ssh-")) : [];
1674
+ const identities = identityLines.length;
1675
+ let configuredKey;
1676
+ let configuredFailure;
1677
+ const format = await runCommand("git", [
1678
+ "config",
1679
+ "--global",
1680
+ "--get",
1681
+ "gpg.format"
1682
+ ]);
1683
+ if (format.code === 0 && format.stdout.trim() === "ssh") {
1684
+ const signingKey = await runCommand("git", [
1685
+ "config",
1686
+ "--global",
1687
+ "--get",
1688
+ "user.signingkey"
1689
+ ]);
1690
+ if (signingKey.code === 0 && signingKey.stdout.trim().length > 0) {
1691
+ const resolved = resolveConfiguredSshSigningKey(signingKey.stdout.trim(), {
1692
+ homeDir: dirname(context.hostGitconfigPath),
1693
+ workspaceFolder: context.workspaceFolder
1694
+ });
1695
+ if (resolved.key === undefined) {
1696
+ configuredFailure = {
1697
+ reason: resolved.reason ?? "configured-key-invalid",
1698
+ detail: resolved.detail
1699
+ };
1700
+ } else {
1701
+ configuredKey = resolved.key;
1702
+ }
1703
+ }
1704
+ }
1705
+ const includeOptional = options.includeOptional ?? true;
1706
+ let ghAvailable = false;
1707
+ let ghAuth = false;
1708
+ let githubLogin;
1709
+ let githubAuthKeys;
1710
+ if (includeOptional) {
1711
+ ghAvailable = await commandWorks(runCommand, "gh", ["--version"]);
1712
+ if (ghAvailable) {
1713
+ ghAuth = await commandWorks(runCommand, "gh", [
1714
+ "auth",
1715
+ "status",
1716
+ "--hostname",
1717
+ "github.com"
1718
+ ]);
1719
+ if (ghAuth && configuredKey === undefined && configuredFailure === undefined && identities > 1) {
1720
+ const user = await runCommand("gh", [
1721
+ "api",
1722
+ "user",
1723
+ "--jq",
1724
+ ".login"
1725
+ ]);
1726
+ githubLogin = user.code === 0 && user.stdout.trim().length > 0 ? user.stdout.trim() : undefined;
1727
+ if (githubLogin !== undefined) {
1728
+ const authentication = await runCommand("gh", [
1729
+ "api",
1730
+ `users/${githubLogin}/keys`,
1731
+ "--paginate",
1732
+ "--jq",
1733
+ ".[].key"
1734
+ ]);
1735
+ if (authentication.code === 0) githubAuthKeys = authentication.stdout.split(/\r?\n/);
1736
+ }
1737
+ }
1738
+ }
1739
+ }
1740
+ const selected = configuredFailure ?? selectGitSigningKey(identityLines, configuredKey, githubAuthKeys);
1741
+ const selectedByConfiguration = selected.key !== undefined && configuredKey !== undefined;
1742
+ const selectedByGithub = selected.key !== undefined && configuredKey === undefined && identities > 1;
1743
+ const signingMessages = {
1744
+ "agent-unavailable": "SSH agent is unavailable; Boxdown commits will remain unsigned",
1745
+ "no-identities": "SSH agent has no identities; Boxdown commits will remain unsigned",
1746
+ "ambiguous-identities": "SSH agent has multiple identities; Boxdown will not guess a signing key and commits will remain unsigned",
1747
+ "configured-key-unreadable": "Configured SSH signing-key file could not be read; Boxdown commits will remain unsigned",
1748
+ "configured-key-invalid": "Configured SSH signing key is not a valid public key; Boxdown commits will remain unsigned",
1749
+ "configured-key-not-loaded": "Configured SSH signing key is not loaded in the agent; Boxdown commits will remain unsigned",
1750
+ "agent-socket-unavailable": "Host SSH-agent socket is unavailable; Boxdown commits will remain unsigned",
1751
+ "docker-probe-image-unavailable": "No local Docker image is available to probe commit-signing agent forwarding",
1752
+ "agent-mount-unavailable": "Docker could not mount the host SSH-agent socket; Boxdown commits will remain unsigned"
1753
+ };
1153
1754
  checks.push({
1154
1755
  name: "git-signing-agent",
1155
- level: sshAgent.code === 0 && identities === 1 ? "ok" : "warn",
1156
- message: sshAgent.code !== 0 ? "SSH agent is unavailable; Boxdown commits will remain unsigned" : identities === 0 ? "SSH agent has no identities; Boxdown commits will remain unsigned" : identities > 1 ? "SSH agent has multiple identities; Boxdown will not guess a signing key and commits will remain unsigned" : "One SSH agent identity is available for Boxdown commit signing"
1756
+ level: sshAgent.code === 0 && selected.key !== undefined ? "ok" : "warn",
1757
+ message: sshAgent.code !== 0 ? signingMessages["agent-unavailable"] : selected.key === undefined ? signingMessages[selected.reason ?? "ambiguous-identities"] : selectedByConfiguration ? "Configured SSH signing key is loaded in the agent" : selectedByGithub ? "GitHub authentication keys identify one SSH agent identity for Boxdown commit signing" : "One SSH agent identity is available for Boxdown commit signing"
1157
1758
  });
1158
1759
  checks.push(check("devcontainers-cli", await packagedDevcontainerCliWorks(context, runCommand), "Packaged @devcontainers/cli is available", "Packaged @devcontainers/cli is required but was not available"));
1159
- const dockerCliWorks = await commandWorks(runCommand, "docker", ["--version"]);
1160
- checks.push(check("docker-cli", dockerCliWorks, "Docker CLI is available", "Docker CLI is required but was not available"));
1161
- const dockerDaemonWorks = await commandWorks(runCommand, "docker", ["info"]);
1162
- checks.push(check("docker-daemon", dockerDaemonWorks, "Docker daemon is reachable", "Docker daemon is required but was not reachable"));
1760
+ let dockerCliWorks = options.containerRuntimeReady === true;
1761
+ let dockerDaemonWorks = options.containerRuntimeReady === true;
1762
+ if (options.containerRuntimeReady !== true) {
1763
+ const runtime = await probeContainerRuntime(runCommand);
1764
+ dockerCliWorks = runtime.state === "ready" || runtime.failure.reason !== "docker-cli-unavailable";
1765
+ dockerDaemonWorks = runtime.state === "ready" || runtime.state === "waiting" && runtime.failure.reason === "buildx-builder-unavailable";
1766
+ checks.push(check("docker-cli", dockerCliWorks, "Docker CLI is available", "Docker CLI is required but was not available"));
1767
+ checks.push(check("docker-daemon", dockerDaemonWorks, "Docker daemon is reachable", "Docker daemon is required but was not reachable"));
1768
+ if (!dockerCliWorks || !dockerDaemonWorks) {
1769
+ checks.push({
1770
+ name: "docker-buildx",
1771
+ level: "warn",
1772
+ message: "Docker Buildx was not checked because the Docker runtime is unavailable"
1773
+ });
1774
+ } else if (runtime.state === "ready" && runtime.mode === "fallback") {
1775
+ checks.push({
1776
+ name: "docker-buildx",
1777
+ level: "warn",
1778
+ message: runtime.warnings[0]
1779
+ });
1780
+ } else if (runtime.state === "waiting") {
1781
+ checks.push({
1782
+ name: "docker-buildx",
1783
+ level: "fail",
1784
+ message: `Docker Buildx builder was not operational: ${runtime.failure.detail}`
1785
+ });
1786
+ } else {
1787
+ checks.push({
1788
+ name: "docker-buildx",
1789
+ level: "ok",
1790
+ message: "Docker Buildx builder is operational"
1791
+ });
1792
+ }
1793
+ }
1163
1794
  checks.push(check("ssh", await commandExists(runCommand, "ssh", ["-V"]), "ssh is available", "ssh is required but was not available"));
1164
1795
  if (options.includeDockerMountProbe ?? true) {
1165
1796
  checks.push(await checkDockerBindMounts(context, runCommand, dockerCliWorks && dockerDaemonWorks));
1166
1797
  }
1167
1798
  checks.push(check("ssh-keygen", await commandExists(runCommand, "ssh-keygen", ["-?"]), "ssh-keygen is available", "ssh-keygen is required but was not available"));
1168
1799
  checks.push(check("assets", existsSync(context.assetsDevcontainerDir), `Devcontainer assets found at ${context.assetsDevcontainerDir}`, `Missing Boxdown devcontainer assets: ${context.assetsDevcontainerDir}`));
1169
- if (options.includeOptional ?? true) {
1170
- if (await commandWorks(runCommand, "gh", ["--version"])) {
1171
- const ghAuth = await commandWorks(runCommand, "gh", [
1172
- "auth",
1173
- "status",
1174
- "--hostname",
1175
- "github.com"
1176
- ]);
1800
+ checks.push(secretEnvironmentConfigCheck(context));
1801
+ if (includeOptional) {
1802
+ if (ghAvailable) {
1177
1803
  checks.push({
1178
1804
  name: "gh-auth",
1179
1805
  level: ghAuth ? "ok" : "warn",
1180
1806
  message: ghAuth ? "GitHub CLI auth is available" : "GitHub CLI is available but not authenticated"
1181
1807
  });
1182
- if (ghAuth && identities === 1) {
1183
- const user = await runCommand("gh", [
1184
- "api",
1185
- "user",
1186
- "--jq",
1187
- ".login"
1188
- ]);
1189
- const signing = user.code === 0 && user.stdout.trim().length > 0 ? await runCommand("gh", [
1808
+ if (ghAuth && selected.key !== undefined) {
1809
+ if (githubLogin === undefined) {
1810
+ const user = await runCommand("gh", [
1811
+ "api",
1812
+ "user",
1813
+ "--jq",
1814
+ ".login"
1815
+ ]);
1816
+ githubLogin = user.code === 0 && user.stdout.trim().length > 0 ? user.stdout.trim() : undefined;
1817
+ }
1818
+ const signing = githubLogin !== undefined ? await runCommand("gh", [
1190
1819
  "api",
1191
- `users/${user.stdout.trim()}/ssh_signing_keys`,
1820
+ `users/${githubLogin}/ssh_signing_keys`,
1192
1821
  "--paginate",
1193
1822
  "--jq",
1194
1823
  ".[].key"
@@ -1197,11 +1826,10 @@ async function runDoctorChecks(context, options = {}) {
1197
1826
  stdout: "",
1198
1827
  stderr: ""
1199
1828
  };
1200
- const identity = sshAgent.stdout.split(/\r?\n/).find((line) => line.trim().startsWith("ssh-"))?.trim();
1201
1829
  checks.push({
1202
1830
  name: "git-signing-github",
1203
- level: signing.code === 0 && identity !== undefined && signing.stdout.includes(identity.split(/\s+/, 3).slice(0, 2).join(" ")) ? "ok" : "warn",
1204
- message: signing.code !== 0 ? "GitHub SSH signing-key registration could not be checked" : signing.stdout.includes(identity?.split(/\s+/, 3).slice(0, 2).join(" ") ?? "") ? "Selected SSH key is registered with GitHub for commit signing" : "Register the selected public key with GitHub as a signing key to receive Verified badges"
1831
+ level: signing.code === 0 && signing.stdout.includes(selected.key) ? "ok" : "warn",
1832
+ message: signing.code !== 0 ? "GitHub SSH signing-key registration could not be checked" : signing.stdout.includes(selected.key) ? "Selected SSH key is registered with GitHub for commit signing" : "Register the selected public key with GitHub as a signing key to receive Verified badges"
1205
1833
  });
1206
1834
  }
1207
1835
  } else {
@@ -1247,6 +1875,11 @@ async function checkDockerBindMounts(context, runCommand, dockerReady) {
1247
1875
  }
1248
1876
  mkdirSync(context.workspaceDataDir, { recursive: true });
1249
1877
  const runtimeProbeDir = mkdtempSync(join(context.workspaceDataDir, "doctor-mount-probe-"));
1878
+ mkdirSync(context.workspaceSecretEnvDir, {
1879
+ recursive: true,
1880
+ mode: 448
1881
+ });
1882
+ chmodSync(context.workspaceSecretEnvDir, 448);
1250
1883
  const sources = [
1251
1884
  {
1252
1885
  label: "workspace",
@@ -1259,6 +1892,10 @@ async function checkDockerBindMounts(context, runCommand, dockerReady) {
1259
1892
  {
1260
1893
  label: "Boxdown runtime state",
1261
1894
  path: runtimeProbeDir
1895
+ },
1896
+ {
1897
+ label: "Boxdown runtime secret state",
1898
+ path: context.workspaceSecretEnvDir
1262
1899
  }
1263
1900
  ];
1264
1901
  try {
@@ -1491,147 +2128,6 @@ async function runWorkspaceGit(workspaceFolder, args) {
1491
2128
  });
1492
2129
  }
1493
2130
 
1494
- //#endregion
1495
- //#region src/git-signing.ts
1496
- function parseSshPublicKey(value) {
1497
- const [algorithm, key] = value.trim().split(/\s+/, 3);
1498
- if (algorithm === undefined || key === undefined || !/^ssh-[A-Za-z0-9-]+$/.test(algorithm) || key.length === 0) {
1499
- return undefined;
1500
- }
1501
- return `${algorithm} ${key}`;
1502
- }
1503
- function selectGitSigningKey(identities, configuredKey, githubKeys) {
1504
- const keys = identities.map(parseSshPublicKey).filter((key) => key !== undefined);
1505
- if (keys.length === 0) return { reason: "no-identities" };
1506
- const configured = configuredKey === undefined ? undefined : parseSshPublicKey(configuredKey);
1507
- if (configured !== undefined) {
1508
- return keys.includes(configured) ? { key: configured } : { reason: "configured-key-not-loaded" };
1509
- }
1510
- const github = new Set((githubKeys ?? []).map(parseSshPublicKey).filter((key) => key !== undefined));
1511
- const matches = keys.filter((key) => github.has(key));
1512
- if (matches.length === 1) return { key: matches[0] };
1513
- if (keys.length === 1) return { key: keys[0] };
1514
- return { reason: "ambiguous-identities" };
1515
- }
1516
- function writeGitSigningPublicKey(context, key) {
1517
- mkdirSync(context.gitSigningStateDir, {
1518
- recursive: true,
1519
- mode: 448
1520
- });
1521
- writeFileSync(join(context.gitSigningStateDir, "signing-key.pub"), `${parseSshPublicKey(key) ?? key}\n`, { mode: 420 });
1522
- }
1523
- async function dockerCanMountAgent(source) {
1524
- const images = await runBuffered("docker", [
1525
- "image",
1526
- "ls",
1527
- "--format",
1528
- "{{.Repository}}:{{.Tag}}"
1529
- ], {
1530
- mirrorStdout: false,
1531
- mirrorStderr: false
1532
- });
1533
- const image = images.stdout.split(/\r?\n/).map((value) => value.trim()).find((value) => value.length > 0 && value !== "<none>:<none>");
1534
- if (images.code !== 0 || image === undefined) return false;
1535
- const created = await runBuffered("docker", [
1536
- "create",
1537
- "--pull=never",
1538
- "--entrypoint",
1539
- "/bin/true",
1540
- "--mount",
1541
- `type=bind,source=${source},target=/run/boxdown/ssh-agent.sock`,
1542
- image
1543
- ], {
1544
- mirrorStdout: false,
1545
- mirrorStderr: false
1546
- });
1547
- const containerId = created.stdout.trim().split(/\r?\n/)[0];
1548
- if (created.code !== 0 || containerId === undefined || containerId.length === 0) return false;
1549
- await runBuffered("docker", [
1550
- "rm",
1551
- "-f",
1552
- containerId
1553
- ], {
1554
- mirrorStdout: false,
1555
- mirrorStderr: false
1556
- });
1557
- return true;
1558
- }
1559
- async function resolveGitSigningPlan(context) {
1560
- const result = await runBuffered("ssh-add", ["-L"], {
1561
- mirrorStdout: false,
1562
- mirrorStderr: false
1563
- });
1564
- if (result.code !== 0) return {
1565
- enabled: false,
1566
- reason: "agent-unavailable"
1567
- };
1568
- const identities = result.stdout.split(/\r?\n/);
1569
- const format = await runBuffered("git", [
1570
- "config",
1571
- "--global",
1572
- "--get",
1573
- "gpg.format"
1574
- ], {
1575
- mirrorStdout: false,
1576
- mirrorStderr: false
1577
- });
1578
- const configuredKey = format.code === 0 && format.stdout.trim() === "ssh" ? (await runBuffered("git", [
1579
- "config",
1580
- "--global",
1581
- "--get",
1582
- "user.signingkey"
1583
- ], {
1584
- mirrorStdout: false,
1585
- mirrorStderr: false
1586
- })).stdout.trim() : undefined;
1587
- let githubKeys;
1588
- if (identities.filter((key) => parseSshPublicKey(key) !== undefined).length > 1) {
1589
- const user = await runBuffered("gh", [
1590
- "api",
1591
- "user",
1592
- "--jq",
1593
- ".login"
1594
- ], {
1595
- mirrorStdout: false,
1596
- mirrorStderr: false
1597
- });
1598
- const login = user.stdout.trim();
1599
- if (user.code === 0 && login.length > 0) {
1600
- const github = await runBuffered("gh", [
1601
- "api",
1602
- `users/${login}/keys`,
1603
- "--paginate",
1604
- "--jq",
1605
- ".[].key"
1606
- ], {
1607
- mirrorStdout: false,
1608
- mirrorStderr: false
1609
- });
1610
- if (github.code === 0) githubKeys = github.stdout.split(/\r?\n/);
1611
- }
1612
- }
1613
- const selected = selectGitSigningKey(identities, configuredKey, githubKeys);
1614
- if (selected.key === undefined) return {
1615
- enabled: false,
1616
- reason: selected.reason
1617
- };
1618
- const agentSocketSource = process.platform === "darwin" ? "/run/host-services/ssh-auth.sock" : process.env.SSH_AUTH_SOCK;
1619
- if (agentSocketSource === undefined || agentSocketSource.length === 0) return {
1620
- enabled: false,
1621
- reason: "agent-mount-unavailable"
1622
- };
1623
- if (!await dockerCanMountAgent(agentSocketSource)) return {
1624
- enabled: false,
1625
- reason: "agent-mount-unavailable"
1626
- };
1627
- writeGitSigningPublicKey(context, selected.key);
1628
- return {
1629
- enabled: true,
1630
- publicKey: selected.key,
1631
- agentSocketSource
1632
- };
1633
- }
1634
-
1635
2131
  //#endregion
1636
2132
  //#region src/metadata.ts
1637
2133
  const WORKSPACE_METADATA_VERSION = 1;
@@ -1852,6 +2348,14 @@ var ProgressReporter = class {
1852
2348
  }
1853
2349
  this.#writeLine(`${promptRail()} ${color$1(message, "dim")}`);
1854
2350
  }
2351
+ status(message) {
2352
+ if (this.mode === "none") return;
2353
+ if (this.mode === "verbose") {
2354
+ this.#write(this.target, message);
2355
+ return;
2356
+ }
2357
+ this.#writeInteractiveLine(`${promptRail()} ${color$1(message, "dim")}`);
2358
+ }
1855
2359
  warn(message) {
1856
2360
  if (this.mode === "none") {
1857
2361
  return;
@@ -1860,7 +2364,7 @@ var ProgressReporter = class {
1860
2364
  this.#write(this.target, `Warning: ${message}`);
1861
2365
  return;
1862
2366
  }
1863
- this.#writeLine(`${promptRail()} ${color$1("!", "dim")} ${message}`);
2367
+ this.#writeInteractiveLine(`${promptRail()} ${color$1("!", "dim")} ${message}`);
1864
2368
  }
1865
2369
  marker(message) {
1866
2370
  if (this.#steps.length > 0) {
@@ -1973,6 +2477,16 @@ var ProgressReporter = class {
1973
2477
  }
1974
2478
  this.#write(this.target, message);
1975
2479
  }
2480
+ #writeInteractiveLine(message) {
2481
+ if (this.#isTTY && this.#renderedStepLineCount > 0) {
2482
+ this.#writeRaw(this.target, `\u001B[${this.#renderedStepLineCount}A\r\u001B[2K`);
2483
+ this.#write(this.target, message);
2484
+ this.#renderedStepLineCount = 0;
2485
+ this.#renderChecklist();
2486
+ return;
2487
+ }
2488
+ this.#writeLine(message);
2489
+ }
1976
2490
  #renderSpinner() {
1977
2491
  const spinner = this.#spinner;
1978
2492
  if (spinner === undefined) {
@@ -2105,13 +2619,27 @@ function createMarkerSink(progress) {
2105
2619
  function outputWithoutProgressMarkers(output) {
2106
2620
  return output.split(/\r?\n/u).filter((line) => !isProgressMarkerLine(line)).join("\n");
2107
2621
  }
2108
- function tailLines(output, maxLines) {
2109
- return output.split(/\r?\n/u).map((line) => line.trimEnd()).filter((line) => line.trim().length > 0).slice(-maxLines);
2622
+ function outputLines(output) {
2623
+ return output.split(/\r?\n/u).map((line) => line.trimEnd()).filter((line) => line.trim().length > 0);
2624
+ }
2625
+ function isDevcontainerErrorEnvelope(line) {
2626
+ try {
2627
+ const value = JSON.parse(line);
2628
+ return value !== null && typeof value === "object" && value.outcome === "error" && typeof value.message === "string";
2629
+ } catch {
2630
+ return false;
2631
+ }
2110
2632
  }
2111
2633
  function formatCommandFailure(label, result, options = {}) {
2112
2634
  const maxLines = options.tailLines ?? DEFAULT_FAILURE_TAIL_LINES;
2113
- const stderrTail = tailLines(outputWithoutProgressMarkers(result.stderr), maxLines);
2114
- const stdoutTail = tailLines(outputWithoutProgressMarkers(result.stdout), Math.max(0, maxLines - stderrTail.length));
2635
+ const stderrLines = outputLines(outputWithoutProgressMarkers(result.stderr));
2636
+ const stderrTail = maxLines <= 0 ? [] : stderrLines.slice(-maxLines);
2637
+ const stdoutLines = outputLines(outputWithoutProgressMarkers(result.stdout));
2638
+ const wrapperPresent = stdoutLines.some(isDevcontainerErrorEnvelope);
2639
+ const specificStdout = stdoutLines.filter((line) => !isDevcontainerErrorEnvelope(line));
2640
+ const hasSpecificDiagnostic = stderrLines.length > 0 || specificStdout.length > 0;
2641
+ const stdoutBudget = Math.max(0, maxLines - stderrTail.length);
2642
+ const stdoutTail = stdoutBudget === 0 ? [] : specificStdout.slice(-stdoutBudget);
2115
2643
  const lines = [`${label} failed with exit code ${result.code}.`, "Rerun with --verbose to see full command output."];
2116
2644
  if (stderrTail.length > 0) {
2117
2645
  lines.push("", "stderr tail:", ...stderrTail.map((line) => ` ${line}`));
@@ -2119,6 +2647,12 @@ function formatCommandFailure(label, result, options = {}) {
2119
2647
  if (stdoutTail.length > 0) {
2120
2648
  lines.push("", "stdout tail:", ...stdoutTail.map((line) => ` ${line}`));
2121
2649
  }
2650
+ if (wrapperPresent && !hasSpecificDiagnostic) {
2651
+ lines.push("", "The Dev Containers CLI reported a nested command failure without diagnostic output.");
2652
+ }
2653
+ if (options.logPath !== undefined) {
2654
+ lines.push("", `Command log: ${options.logPath}`);
2655
+ }
2122
2656
  return lines.join("\n");
2123
2657
  }
2124
2658
  async function runProgressCommand(label, command, args, options = {}) {
@@ -2163,9 +2697,9 @@ async function runProgressCommand(label, command, args, options = {}) {
2163
2697
  throw error;
2164
2698
  }
2165
2699
  }
2166
- function assertProgressCommandSucceeded(label, result, message) {
2700
+ function assertProgressCommandSucceeded(label, result, message, options = {}) {
2167
2701
  if (result.code !== 0) {
2168
- throw new Error(`${message}\n${formatCommandFailure(label, result)}`);
2702
+ throw new Error(`${message}\n${formatCommandFailure(label, result, options)}`);
2169
2703
  }
2170
2704
  }
2171
2705
 
@@ -2702,34 +3236,30 @@ async function findRunningContainerId(context, options = {}) {
2702
3236
  }
2703
3237
  return result.stdout.split(/\r?\n/).find((line) => line.length > 0);
2704
3238
  }
2705
- function isRecord(value) {
2706
- return value !== null && typeof value === "object" && !Array.isArray(value);
2707
- }
2708
3239
  function parseDockerInspectImage(output, containerId) {
2709
- const trimmed = output.trim();
2710
- if (trimmed.length === 0) {
3240
+ const [rawId, rawName] = output.trim().split("|");
3241
+ if (rawId === undefined || rawId.length === 0) {
2711
3242
  return undefined;
2712
3243
  }
2713
- let parsed;
3244
+ let id;
3245
+ let name;
2714
3246
  try {
2715
- parsed = JSON.parse(trimmed);
3247
+ id = JSON.parse(rawId);
3248
+ name = rawName === undefined || rawName.length === 0 ? undefined : JSON.parse(rawName);
2716
3249
  } catch (error) {
2717
3250
  throw new Error(`Could not parse docker inspect output for ${containerId}`, { cause: error });
2718
3251
  }
2719
- if (typeof parsed.Image !== "string" || parsed.Image.length === 0) {
2720
- return undefined;
2721
- }
2722
- const configImage = isRecord(parsed.Config) && typeof parsed.Config.Image === "string" && parsed.Config.Image.length > 0 ? parsed.Config.Image : undefined;
3252
+ if (typeof id !== "string" || id.length === 0) return undefined;
2723
3253
  return {
2724
- id: parsed.Image,
2725
- ...configImage === undefined ? {} : { name: configImage }
3254
+ id,
3255
+ ...typeof name === "string" && name.length > 0 ? { name } : {}
2726
3256
  };
2727
3257
  }
2728
3258
  async function inspectContainerImage(containerId, options = {}) {
2729
3259
  const result = await runBuffered("docker", [
2730
3260
  "inspect",
2731
3261
  "--format",
2732
- "{{json .}}",
3262
+ "{{json .Image}}|{{json .Config.Image}}",
2733
3263
  containerId
2734
3264
  ], {
2735
3265
  logger: options.logger,
@@ -2851,7 +3381,15 @@ async function startDevcontainer(context, options = {}) {
2851
3381
  progress.detail(context.generatedConfigPath);
2852
3382
  }
2853
3383
  try {
2854
- writeGeneratedDevcontainerConfig(context, await resolveGitSigningPlan(context));
3384
+ const signingPlan = await resolveGitSigningPlan(context);
3385
+ reportGitSigningPlan(signingPlan, {
3386
+ logger: options.logger,
3387
+ quiet: proxyMode,
3388
+ ...progress === undefined ? {} : { writeWarning: (message) => {
3389
+ progress.warn(message.trimEnd());
3390
+ } }
3391
+ });
3392
+ writeGeneratedDevcontainerConfig(context, signingPlan);
2855
3393
  if (hasConfigStep) {
2856
3394
  progress?.completeStep("devcontainer-config");
2857
3395
  }
@@ -2894,7 +3432,7 @@ async function startDevcontainer(context, options = {}) {
2894
3432
  mirrorStdout: proxyMode ? "stderr" : "stdout",
2895
3433
  mirrorStderr: "stderr",
2896
3434
  logger: options.logger
2897
- }) : await runProgressCommand("devcontainer up", cli.command, [...cli.argsPrefix, ...args], {
3435
+ }) : await (options.runDevcontainerUp ?? runProgressCommand)("devcontainer up", cli.command, [...cli.argsPrefix, ...args], {
2898
3436
  progress,
2899
3437
  spinnerLabel: "Starting devcontainer",
2900
3438
  stepId: "devcontainer-start",
@@ -2906,7 +3444,7 @@ async function startDevcontainer(context, options = {}) {
2906
3444
  throw new Error(`devcontainer up failed for ${context.workspaceFolder}`);
2907
3445
  }
2908
3446
  if (progress !== undefined) {
2909
- assertProgressCommandSucceeded("devcontainer up", result, `devcontainer up failed for ${context.workspaceFolder}`);
3447
+ assertProgressCommandSucceeded("devcontainer up", result, `devcontainer up failed for ${context.workspaceFolder}`, { logPath: options.logger?.logPath });
2910
3448
  }
2911
3449
  const containerId = parseContainerIdFromUpOutput(`${result.stdout}\n${result.stderr}`) ?? await findRunningContainerId(context, { logger: options.logger });
2912
3450
  if (containerId === undefined) {
@@ -3004,7 +3542,7 @@ async function ensureContainerSshRuntime(context, options = {}) {
3004
3542
  throw new Error("Failed to prepare devcontainer SSH runtime");
3005
3543
  }
3006
3544
  if (options.progress !== undefined) {
3007
- assertProgressCommandSucceeded("prepare SSH runtime", result, "Failed to prepare devcontainer SSH runtime");
3545
+ assertProgressCommandSucceeded("prepare SSH runtime", result, "Failed to prepare devcontainer SSH runtime", { logPath: options.logger?.logPath });
3008
3546
  }
3009
3547
  }
3010
3548
  async function refreshContainerCodingAgentClis(context, proxyMode = false, agents = [], options = {}) {
@@ -3071,7 +3609,7 @@ async function ensureContainerCodingAgentCli(context, agent, options = {}) {
3071
3609
  throw new Error(`Could not install or refresh ${codingAgentBinary(agent)} inside the devcontainer`);
3072
3610
  }
3073
3611
  if (options.progress !== undefined) {
3074
- assertProgressCommandSucceeded(`prepare ${codingAgentBinary(agent)}`, result, `Could not install or refresh ${codingAgentBinary(agent)} inside the devcontainer`);
3612
+ assertProgressCommandSucceeded(`prepare ${codingAgentBinary(agent)}`, result, `Could not install or refresh ${codingAgentBinary(agent)} inside the devcontainer`, { logPath: options.logger?.logPath });
3075
3613
  }
3076
3614
  }
3077
3615
  async function runSshdProxy(containerId, options = {}) {
@@ -3137,7 +3675,14 @@ async function refreshContainerGhAuth(context, options = {}) {
3137
3675
  quiet: true,
3138
3676
  progress
3139
3677
  });
3140
- writeGeneratedDevcontainerConfig(context, await resolveGitSigningPlan(context));
3678
+ const signingPlan = await resolveGitSigningPlan(context);
3679
+ reportGitSigningPlan(signingPlan, {
3680
+ logger: options.logger,
3681
+ ...progress === undefined ? {} : { writeWarning: (message) => {
3682
+ progress.warn(message.trimEnd());
3683
+ } }
3684
+ });
3685
+ writeGeneratedDevcontainerConfig(context, signingPlan);
3141
3686
  if (hasConfigStep) {
3142
3687
  progress?.completeStep("gh-auth-config");
3143
3688
  }
@@ -3859,6 +4404,9 @@ function errorMessage$1(error) {
3859
4404
  function argvText(command, args) {
3860
4405
  return JSON.stringify([command, ...args]);
3861
4406
  }
4407
+ function redactKnownSecretEnvironmentAssignments(value) {
4408
+ return value.replace(/ANTHROPIC_API_KEY=[^\s,"\]}]+/gu, "ANTHROPIC_API_KEY=[redacted]").replace(/SNYK_TOKEN=[^\s,"\]}]+/gu, "SNYK_TOKEN=[redacted]").replace(/OP_SERVICE_ACCOUNT_TOKEN=[^\s,"\]}]+/gu, "OP_SERVICE_ACCOUNT_TOKEN=[redacted]");
4409
+ }
3862
4410
  var WorkspaceCommandLogger = class {
3863
4411
  logPath;
3864
4412
  workspaceFolder;
@@ -3909,7 +4457,7 @@ var WorkspaceCommandLogger = class {
3909
4457
  return `[${this.#now().toISOString()}]`;
3910
4458
  }
3911
4459
  #redact(value) {
3912
- return this.#redactions.reduce((current, redaction) => current.replaceAll(redaction, "[redacted]"), value);
4460
+ return this.#redactions.reduce((current, redaction) => current.replaceAll(redaction, "[redacted]"), redactKnownSecretEnvironmentAssignments(value));
3913
4461
  }
3914
4462
  #stream(stream, chunk) {
3915
4463
  const text = this.#redact(Buffer.isBuffer(chunk) ? chunk.toString("utf8") : chunk);
@@ -3992,6 +4540,15 @@ function defaultDataRoot(env = process.env) {
3992
4540
  }
3993
4541
  return join(homedir(), ".local", "share", PACKAGE_NAME);
3994
4542
  }
4543
+ function defaultRuntimeRoot(env = process.env) {
4544
+ if (env.BOXDOWN_RUNTIME_HOME) {
4545
+ return env.BOXDOWN_RUNTIME_HOME;
4546
+ }
4547
+ if (env.XDG_RUNTIME_DIR) {
4548
+ return join(env.XDG_RUNTIME_DIR, PACKAGE_NAME);
4549
+ }
4550
+ return join(tmpdir(), `${PACKAGE_NAME}-${process.getuid?.() ?? "user"}`);
4551
+ }
3995
4552
  function defaultHostAgentsDir(env = process.env) {
3996
4553
  return join(env.HOME ?? homedir(), ".agents");
3997
4554
  }
@@ -4007,10 +4564,12 @@ function createWorkspaceContextFromIdentity(identity, options = {}) {
4007
4564
  const assetsDevcontainerDir = options.assetsDevcontainerDir ?? env.BOXDOWN_DEVCONTAINER_ASSETS_DIR ?? join(packageRoot, "assets", "devcontainer");
4008
4565
  const cacheRoot = defaultCacheRoot(env);
4009
4566
  const dataRoot = defaultDataRoot(env);
4567
+ const runtimeRoot = defaultRuntimeRoot(env);
4010
4568
  const hostAgentsDir = defaultHostAgentsDir(env);
4011
4569
  const hostCodexAuthPath = defaultHostCodexAuthPath(env);
4012
4570
  const workspaceCacheDir = join(cacheRoot, "workspaces", identity.workspaceId);
4013
4571
  const workspaceDataDir = join(dataRoot, "workspaces", identity.workspaceId);
4572
+ const workspaceRuntimeDir = join(runtimeRoot, "workspaces", identity.workspaceId);
4014
4573
  const hostGitconfigSnapshotDir = join(workspaceDataDir, "gitconfig");
4015
4574
  return {
4016
4575
  workspaceFolder: identity.workspaceFolder,
@@ -4020,8 +4579,11 @@ function createWorkspaceContextFromIdentity(identity, options = {}) {
4020
4579
  assetsDevcontainerDir,
4021
4580
  cacheRoot,
4022
4581
  dataRoot,
4582
+ runtimeRoot,
4023
4583
  workspaceCacheDir,
4024
4584
  workspaceDataDir,
4585
+ workspaceRuntimeDir,
4586
+ workspaceSecretEnvDir: join(workspaceRuntimeDir, "secrets"),
4025
4587
  generatedConfigPath: join(workspaceCacheDir, "devcontainer.json"),
4026
4588
  hostAgentsDir,
4027
4589
  hostCodexAuthPath,
@@ -4110,6 +4672,9 @@ function removeWorkspaceStateDir(context, label, path, root) {
4110
4672
  });
4111
4673
  process.stdout.write(`Removed ${label}: ${path}\n`);
4112
4674
  }
4675
+ function removeWorkspaceRuntimeState(context) {
4676
+ removeWorkspaceStateDir(context, "workspace runtime directory", context.workspaceRuntimeDir, context.runtimeRoot);
4677
+ }
4113
4678
  async function purgeAliasIntegrations(context, alias) {
4114
4679
  let failed = false;
4115
4680
  failed = await runPurgeStep(`SSH alias ${alias}`, () => {
@@ -4186,6 +4751,9 @@ async function purgeWorkspace(context, options = {}) {
4186
4751
  await removeDockerImage(removedImageId, { logger: options.logger });
4187
4752
  }) || failed;
4188
4753
  }
4754
+ failed = await runPurgeStep("workspace runtime directory", () => {
4755
+ removeWorkspaceRuntimeState(context);
4756
+ }) || failed;
4189
4757
  failed = await runPurgeStep("workspace cache directory", () => {
4190
4758
  removeWorkspaceStateDir(context, "workspace cache", context.workspaceCacheDir, context.cacheRoot);
4191
4759
  }) || failed;
@@ -4354,10 +4922,12 @@ function commandWritesWorkspaceMetadata(command) {
4354
4922
  "ssh-proxy",
4355
4923
  "tunnel",
4356
4924
  "refresh-gh-token",
4357
- "refresh-gh-token-running",
4358
4925
  "coding-agent"
4359
4926
  ].includes(command);
4360
4927
  }
4928
+ function commandRequiresContainerRuntime(command) {
4929
+ return command === "setup" || command === "start" || command === "ssh-proxy" || command === "tunnel" || command === "refresh-gh-token" || command === "coding-agent";
4930
+ }
4361
4931
  function parseCliArgs(argv) {
4362
4932
  const args = [...argv];
4363
4933
  const workspaces = [];
@@ -4891,6 +5461,7 @@ async function runDownCommand(workspaces, options) {
4891
5461
  const context = createWorkspaceContext({ workspace });
4892
5462
  await runLoggedLifecycle(context, "down", ["down", ...workspace === undefined ? [] : ["--workspace", workspace]], async (logger) => {
4893
5463
  await removeWorkspaceContainer(context, { logger });
5464
+ removeWorkspaceRuntimeState(context);
4894
5465
  });
4895
5466
  } catch (error) {
4896
5467
  failed = true;
@@ -5052,7 +5623,7 @@ function createCliProgress(parsed, target = "stdout", options = {}) {
5052
5623
  target
5053
5624
  });
5054
5625
  }
5055
- function startProgressSteps() {
5626
+ function devcontainerStartProgressSteps() {
5056
5627
  return [
5057
5628
  {
5058
5629
  id: "ssh-identity",
@@ -5068,13 +5639,19 @@ function startProgressSteps() {
5068
5639
  }
5069
5640
  ];
5070
5641
  }
5642
+ function startProgressSteps() {
5643
+ return [{
5644
+ id: "container-runtime",
5645
+ label: "Checking container runtime"
5646
+ }, ...devcontainerStartProgressSteps()];
5647
+ }
5071
5648
  function sshTargetProgressLabel(target) {
5072
5649
  const label = SSH_INSTALL_TARGETS.find((candidate) => candidate.value === target)?.label ?? target;
5073
5650
  return `Installing ${label} SSH target`;
5074
5651
  }
5075
5652
  function setupProgressSteps(targets) {
5076
5653
  return [
5077
- ...startProgressSteps(),
5654
+ ...devcontainerStartProgressSteps(),
5078
5655
  {
5079
5656
  id: "ssh-alias",
5080
5657
  label: "Installing SSH alias"
@@ -5087,6 +5664,9 @@ function setupProgressSteps(targets) {
5087
5664
  }
5088
5665
  function setupPreflightProgressSteps() {
5089
5666
  return [{
5667
+ id: "container-runtime",
5668
+ label: "Checking container runtime"
5669
+ }, {
5090
5670
  id: "setup-preflight",
5091
5671
  label: "Checking host readiness"
5092
5672
  }];
@@ -5095,13 +5675,55 @@ function setupPreflightFailureMessage(checks) {
5095
5675
  const failures = checks.filter((check) => check.level === "fail").map((check) => `- ${check.name}: ${check.message}`);
5096
5676
  return `Setup preflight failed:\n${failures.join("\n")}`;
5097
5677
  }
5678
+ function runtimeTransitionMessage(probe) {
5679
+ if (probe.state === "waiting" && probe.failure.reason === "docker-daemon-unavailable") {
5680
+ return "Waiting for Docker daemon";
5681
+ }
5682
+ if (probe.state === "waiting" && probe.failure.reason === "buildx-builder-unavailable") {
5683
+ return "Waiting for Docker Buildx builder";
5684
+ }
5685
+ return undefined;
5686
+ }
5687
+ async function runContainerRuntimePreflight(context, progress, options, logger) {
5688
+ const wait = options.waitForContainerRuntime ?? waitForContainerRuntime;
5689
+ progress.startStep("container-runtime");
5690
+ const result = await wait({
5691
+ runCommand: async (command, args, timeoutMs) => runBuffered(command, args, {
5692
+ env: options.env,
5693
+ logger,
5694
+ mirrorStdout: false,
5695
+ mirrorStderr: false,
5696
+ timeoutMs
5697
+ }),
5698
+ onTransition: (probe) => {
5699
+ const message = runtimeTransitionMessage(probe);
5700
+ if (message !== undefined) progress.status(message);
5701
+ }
5702
+ });
5703
+ if (result.state === "failed") {
5704
+ progress.failStep("container-runtime");
5705
+ throw new Error(formatContainerRuntimeFailure(result, { logPath: logger === undefined ? undefined : context.workspaceLogPath }));
5706
+ }
5707
+ for (const warning of result.warnings) progress.warn(warning);
5708
+ if (progress.mode === "verbose") progress.status("Container runtime ready");
5709
+ progress.completeStep("container-runtime");
5710
+ }
5711
+ async function prepareContainerLifecycle(context, alias, progress, options, logger) {
5712
+ await runContainerRuntimePreflight(context, progress, options, logger);
5713
+ const writeMetadata = options.writeWorkspaceMetadata ?? writeWorkspaceMetadata;
5714
+ writeMetadata(context, alias);
5715
+ }
5098
5716
  async function runSetupPreflight(context, alias, parsed, options) {
5099
5717
  const progress = createCliProgress(parsed, "stdout", { env: options.env });
5100
5718
  const doctor = options.runDoctorChecks ?? runDoctorChecks;
5101
5719
  await withProgressSection(progress, "Boxdown setup", [`Workspace: ${context.workspaceFolder}`, `SSH alias: ${alias}`], async () => {
5102
5720
  progress.setSteps(setupPreflightProgressSteps());
5721
+ await runContainerRuntimePreflight(context, progress, options);
5103
5722
  progress.startStep("setup-preflight");
5104
- const checks = await doctor(context, { includeOptional: false });
5723
+ const checks = await doctor(context, {
5724
+ includeOptional: false,
5725
+ containerRuntimeReady: true
5726
+ });
5105
5727
  for (const check of checks.filter((item) => item.level === "warn")) {
5106
5728
  progress.warn(`${check.name}: ${check.message}`);
5107
5729
  }
@@ -5213,9 +5835,6 @@ async function runCli(argv = process.argv.slice(2), options = {}) {
5213
5835
  const context = createWorkspaceContext({ workspace: parsed.workspace });
5214
5836
  const alias = parsed.alias ?? defaultSshAlias(context.workspaceBasename);
5215
5837
  const aliasSource = parsed.alias === undefined ? "default" : "provided";
5216
- if (parsed.command !== "ssh-install" && parsed.command !== "setup" && parsed.command !== "tunnel" && commandWritesWorkspaceMetadata(parsed.command)) {
5217
- writeWorkspaceMetadata(context, alias);
5218
- }
5219
5838
  if (parsed.command === "ssh-install") {
5220
5839
  const resolvedTargets = await resolveSshInstallTargets(parsed, options);
5221
5840
  if (resolvedTargets.cancelled) {
@@ -5312,6 +5931,7 @@ async function runCli(argv = process.argv.slice(2), options = {}) {
5312
5931
  const progress = createCliProgress(parsed, "stderr", { env: options.env });
5313
5932
  const containerId = await withProgressSection(progress, "Boxdown SSH proxy", [`Workspace: ${context.workspaceFolder}`, `SSH alias: ${alias}`], async () => {
5314
5933
  progress.setSteps(sshProxyProgressSteps());
5934
+ await (options.prepareContainerLifecycle ?? prepareContainerLifecycle)(context, alias, progress, options, logger);
5315
5935
  progress.startStep("ssh-alias");
5316
5936
  try {
5317
5937
  await installSshConfig(context, alias, { quiet: true });
@@ -5350,11 +5970,11 @@ async function runCli(argv = process.argv.slice(2), options = {}) {
5350
5970
  if (tunnelPorts.length === 0) {
5351
5971
  throw new Error("tunnel requires at least one --port value");
5352
5972
  }
5353
- writeWorkspaceMetadata(context, alias);
5354
5973
  return runLoggedLifecycle(context, "tunnel", argv, async (logger) => {
5355
5974
  const progress = createCliProgress(parsed, "stdout", { env: options.env });
5356
5975
  await withProgressSection(progress, "Boxdown tunnel", [`Workspace: ${context.workspaceFolder}`, `SSH alias: ${alias}`], async () => {
5357
5976
  progress.setSteps(tunnelProgressSteps());
5977
+ await (options.prepareContainerLifecycle ?? prepareContainerLifecycle)(context, alias, progress, options, logger);
5358
5978
  progress.startStep("ssh-alias");
5359
5979
  try {
5360
5980
  await installSshConfig(context, alias, { quiet: true });
@@ -5378,7 +5998,7 @@ async function runCli(argv = process.argv.slice(2), options = {}) {
5378
5998
  }
5379
5999
  if (parsed.command === "refresh-gh-token-running") {
5380
6000
  return runLoggedLifecycle(context, "refresh-gh-token-running", argv, async (logger) => {
5381
- const containerId = await findRunningContainerId(context, { logger });
6001
+ const containerId = await (options.findRunningContainerId ?? findRunningContainerId)(context, { logger });
5382
6002
  if (containerId === undefined) {
5383
6003
  throw new Error(`No running devcontainer found for: ${context.workspaceFolder}`);
5384
6004
  }
@@ -5400,6 +6020,7 @@ async function runCli(argv = process.argv.slice(2), options = {}) {
5400
6020
  const progress = createCliProgress(parsed, "stdout", { env: options.env });
5401
6021
  await withProgressSection(progress, "Boxdown GitHub auth refresh", [`Workspace: ${context.workspaceFolder}`], async () => {
5402
6022
  progress.setSteps(ghAuthProgressSteps(true));
6023
+ await (options.prepareContainerLifecycle ?? prepareContainerLifecycle)(context, alias, progress, options, logger);
5403
6024
  await startDevcontainer(context, {
5404
6025
  progress,
5405
6026
  logger
@@ -5421,6 +6042,7 @@ async function runCli(argv = process.argv.slice(2), options = {}) {
5421
6042
  const progress = createCliProgress(parsed, "stdout", { env: options.env });
5422
6043
  await withProgressSection(progress, `Boxdown ${agent}`, [`Workspace: ${context.workspaceFolder}`], async () => {
5423
6044
  progress.setSteps(codingAgentProgressSteps(agent));
6045
+ await (options.prepareContainerLifecycle ?? prepareContainerLifecycle)(context, alias, progress, options, logger);
5424
6046
  await startDevcontainer(context, {
5425
6047
  recreate: parsed.recreate,
5426
6048
  progress,
@@ -5438,6 +6060,7 @@ async function runCli(argv = process.argv.slice(2), options = {}) {
5438
6060
  const progress = createCliProgress(parsed, "stdout", { env: options.env });
5439
6061
  const containerId = await withProgressSection(progress, "Boxdown start", [`Workspace: ${context.workspaceFolder}`], async () => {
5440
6062
  progress.setSteps(startProgressSteps());
6063
+ await (options.prepareContainerLifecycle ?? prepareContainerLifecycle)(context, alias, progress, options, logger);
5441
6064
  return await startDevcontainer(context, {
5442
6065
  recreate: parsed.recreate,
5443
6066
  progress,
@@ -5454,5 +6077,5 @@ async function runCli(argv = process.argv.slice(2), options = {}) {
5454
6077
  }
5455
6078
 
5456
6079
  //#endregion
5457
- export { parseTunnelPortList as a, parseTunnelPort as i, commandWritesWorkspaceMetadata as n, runCli as o, parseCliArgs as r, setupWorkspace as s, USAGE as t };
5458
- //# sourceMappingURL=main-XMBsKjIK.mjs.map
6080
+ export { parseTunnelPort as a, runCli as c, parseCliArgs as i, runContainerRuntimePreflight as l, commandRequiresContainerRuntime as n, parseTunnelPortList as o, commandWritesWorkspaceMetadata as r, prepareContainerLifecycle as s, USAGE as t, setupWorkspace as u };
6081
+ //# sourceMappingURL=main-O__JaiXe.mjs.map