boxdown 1.2.1 → 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 (32) hide show
  1. package/README.md +10 -0
  2. package/assets/devcontainer/README.md +12 -1
  3. package/assets/devcontainer/devcontainer.json +1 -1
  4. package/assets/devcontainer/hooks/post-create.sh +12 -1
  5. package/dist/bin/cli.cjs +1 -1
  6. package/dist/bin/cli.mjs +1 -1
  7. package/dist/{main-BDgyf2t5.cjs → main-CT2n9qcb.cjs} +448 -67
  8. package/dist/{main-J4_2Up3o.mjs → main-O__JaiXe.mjs} +431 -68
  9. package/dist/main-O__JaiXe.mjs.map +1 -0
  10. package/dist/main.cjs +4 -1
  11. package/dist/main.d.cts +92 -26
  12. package/dist/main.d.cts.map +1 -1
  13. package/dist/main.d.mts +92 -26
  14. package/dist/main.d.mts.map +1 -1
  15. package/dist/main.mjs +2 -2
  16. package/docs/features/lifecycle.md +13 -0
  17. package/docs/features/setup.md +5 -0
  18. package/docs/features/start-and-shell.md +5 -0
  19. package/docs/superpowers/plans/2026-07-18-architecture-aware-1password-installer.md +163 -0
  20. package/docs/superpowers/plans/2026-07-18-devcontainer-node-image-digest.md +341 -0
  21. package/docs/superpowers/plans/2026-07-19-container-runtime-readiness.md +1536 -0
  22. package/docs/superpowers/specs/2026-07-18-architecture-aware-1password-installer-design.md +63 -0
  23. package/docs/superpowers/specs/2026-07-18-devcontainer-node-image-digest-design.md +108 -0
  24. package/docs/superpowers/specs/2026-07-19-container-runtime-readiness-design.md +353 -0
  25. package/package.json +1 -1
  26. package/src/container-runtime.ts +295 -0
  27. package/src/devcontainer.ts +20 -4
  28. package/src/doctor.ts +48 -22
  29. package/src/main.ts +95 -11
  30. package/src/process.ts +50 -10
  31. package/src/progress.ts +63 -8
  32. package/dist/main-J4_2Up3o.mjs.map +0 -1
@@ -3,6 +3,7 @@ import { appendFileSync, chmodSync, copyFileSync, existsSync, mkdirSync, mkdtemp
3
3
  import { createHash, randomUUID } from "node:crypto";
4
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";
@@ -970,41 +971,12 @@ function publishContainerPortFromConfig(config) {
970
971
  return config.runArgs?.find((arg) => /^[0-9.]+::[0-9]+$/.test(arg))?.split("::")[1];
971
972
  }
972
973
 
973
- //#endregion
974
- //#region src/devcontainer-cli.ts
975
- function devcontainerBinFromPackageJson(packageJson) {
976
- return typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.devcontainer;
977
- }
978
- function resolveDevcontainerCli(context) {
979
- const requireFromBoxdown = createRequire(join(context.packageRoot, "package.json"));
980
- let packageJsonPath;
981
- try {
982
- packageJsonPath = requireFromBoxdown.resolve("@devcontainers/cli/package.json");
983
- } catch (error) {
984
- throw new Error(`Boxdown's packaged @devcontainers/cli dependency is missing. Reinstall boxdown so @devcontainers/cli@${DEVCONTAINER_CLI_VERSION} is available.`, { cause: error });
985
- }
986
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
987
- const bin = devcontainerBinFromPackageJson(packageJson);
988
- if (packageJson.version !== "0.84.1") {
989
- throw new Error(`Boxdown expected @devcontainers/cli@${DEVCONTAINER_CLI_VERSION} but resolved ${packageJson.version ?? "an unknown version"}.`);
990
- }
991
- if (bin === undefined) {
992
- throw new Error("Boxdown resolved @devcontainers/cli, but the package does not expose a devcontainer binary.");
993
- }
994
- const cliPath = resolve(dirname(packageJsonPath), bin);
995
- if (!existsSync(cliPath)) {
996
- throw new Error(`Boxdown resolved @devcontainers/cli, but its devcontainer binary is missing: ${cliPath}`);
997
- }
998
- return {
999
- command: process.execPath,
1000
- argsPrefix: [cliPath],
1001
- path: cliPath,
1002
- version: packageJson.version
1003
- };
1004
- }
1005
-
1006
974
  //#endregion
1007
975
  //#region src/process.ts
976
+ const realTimeoutControl = {
977
+ schedule: (callback, milliseconds) => setTimeout(callback, milliseconds),
978
+ cancel: (handle) => clearTimeout(handle)
979
+ };
1008
980
  function mergedEnv(env) {
1009
981
  const baseEnv = {
1010
982
  ...process.env,
@@ -1057,6 +1029,9 @@ function writeChunk(target, chunk) {
1057
1029
  }
1058
1030
  }
1059
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
+ }
1060
1035
  return new Promise((resolve) => {
1061
1036
  const loggedCommand = options.logger?.startCommand(command, args, { cwd: options.cwd });
1062
1037
  const child = spawn(command, args, {
@@ -1070,24 +1045,28 @@ function runBuffered(command, args, options = {}) {
1070
1045
  });
1071
1046
  const stdoutChunks = [];
1072
1047
  const stderrChunks = [];
1048
+ let settled = false;
1049
+ let timeoutHandle;
1050
+ let timeoutScheduled = false;
1051
+ const timeoutControl = options.timeoutControl ?? realTimeoutControl;
1073
1052
  child.stdout.on("data", (chunk) => {
1053
+ if (settled) return;
1074
1054
  stdoutChunks.push(chunk);
1075
1055
  loggedCommand?.stream("stdout", chunk);
1076
1056
  options.onStdout?.(chunk);
1077
1057
  writeChunk(options.mirrorStdout ?? "stdout", chunk);
1078
1058
  });
1079
1059
  child.stderr.on("data", (chunk) => {
1060
+ if (settled) return;
1080
1061
  stderrChunks.push(chunk);
1081
1062
  loggedCommand?.stream("stderr", chunk);
1082
1063
  options.onStderr?.(chunk);
1083
1064
  writeChunk(options.mirrorStderr ?? "stderr", chunk);
1084
1065
  });
1085
- let resolved = false;
1086
1066
  child.on("error", (error) => {
1087
- if (resolved) {
1088
- return;
1089
- }
1090
- resolved = true;
1067
+ if (settled) return;
1068
+ settled = true;
1069
+ if (timeoutScheduled) timeoutControl.cancel(timeoutHandle);
1091
1070
  loggedCommand?.error(error);
1092
1071
  loggedCommand?.finish(127);
1093
1072
  resolve({
@@ -1097,10 +1076,9 @@ function runBuffered(command, args, options = {}) {
1097
1076
  });
1098
1077
  });
1099
1078
  child.on("close", (code) => {
1100
- if (resolved) {
1101
- return;
1102
- }
1103
- resolved = true;
1079
+ if (settled) return;
1080
+ settled = true;
1081
+ if (timeoutScheduled) timeoutControl.cancel(timeoutHandle);
1104
1082
  loggedCommand?.finish(code ?? 1);
1105
1083
  resolve({
1106
1084
  code: code ?? 1,
@@ -1108,6 +1086,25 @@ function runBuffered(command, args, options = {}) {
1108
1086
  stderr: Buffer.concat(stderrChunks).toString("utf8")
1109
1087
  });
1110
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
+ }
1111
1108
  if (options.input !== undefined) {
1112
1109
  child.stdin.end(options.input);
1113
1110
  } else {
@@ -1136,6 +1133,250 @@ function runInteractive(command, args, options = {}) {
1136
1133
  });
1137
1134
  }
1138
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
+
1139
1380
  //#endregion
1140
1381
  //#region src/git-signing.ts
1141
1382
  const GIT_SIGNING_REASON_MESSAGES = {
@@ -1516,10 +1757,40 @@ async function runDoctorChecks(context, options = {}) {
1516
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"
1517
1758
  });
1518
1759
  checks.push(check("devcontainers-cli", await packagedDevcontainerCliWorks(context, runCommand), "Packaged @devcontainers/cli is available", "Packaged @devcontainers/cli is required but was not available"));
1519
- const dockerCliWorks = await commandWorks(runCommand, "docker", ["--version"]);
1520
- checks.push(check("docker-cli", dockerCliWorks, "Docker CLI is available", "Docker CLI is required but was not available"));
1521
- const dockerDaemonWorks = await commandWorks(runCommand, "docker", ["info"]);
1522
- 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
+ }
1523
1794
  checks.push(check("ssh", await commandExists(runCommand, "ssh", ["-V"]), "ssh is available", "ssh is required but was not available"));
1524
1795
  if (options.includeDockerMountProbe ?? true) {
1525
1796
  checks.push(await checkDockerBindMounts(context, runCommand, dockerCliWorks && dockerDaemonWorks));
@@ -2077,6 +2348,14 @@ var ProgressReporter = class {
2077
2348
  }
2078
2349
  this.#writeLine(`${promptRail()} ${color$1(message, "dim")}`);
2079
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
+ }
2080
2359
  warn(message) {
2081
2360
  if (this.mode === "none") {
2082
2361
  return;
@@ -2085,7 +2364,7 @@ var ProgressReporter = class {
2085
2364
  this.#write(this.target, `Warning: ${message}`);
2086
2365
  return;
2087
2366
  }
2088
- this.#writeLine(`${promptRail()} ${color$1("!", "dim")} ${message}`);
2367
+ this.#writeInteractiveLine(`${promptRail()} ${color$1("!", "dim")} ${message}`);
2089
2368
  }
2090
2369
  marker(message) {
2091
2370
  if (this.#steps.length > 0) {
@@ -2198,6 +2477,16 @@ var ProgressReporter = class {
2198
2477
  }
2199
2478
  this.#write(this.target, message);
2200
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
+ }
2201
2490
  #renderSpinner() {
2202
2491
  const spinner = this.#spinner;
2203
2492
  if (spinner === undefined) {
@@ -2330,13 +2619,27 @@ function createMarkerSink(progress) {
2330
2619
  function outputWithoutProgressMarkers(output) {
2331
2620
  return output.split(/\r?\n/u).filter((line) => !isProgressMarkerLine(line)).join("\n");
2332
2621
  }
2333
- function tailLines(output, maxLines) {
2334
- 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
+ }
2335
2632
  }
2336
2633
  function formatCommandFailure(label, result, options = {}) {
2337
2634
  const maxLines = options.tailLines ?? DEFAULT_FAILURE_TAIL_LINES;
2338
- const stderrTail = tailLines(outputWithoutProgressMarkers(result.stderr), maxLines);
2339
- 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);
2340
2643
  const lines = [`${label} failed with exit code ${result.code}.`, "Rerun with --verbose to see full command output."];
2341
2644
  if (stderrTail.length > 0) {
2342
2645
  lines.push("", "stderr tail:", ...stderrTail.map((line) => ` ${line}`));
@@ -2344,6 +2647,12 @@ function formatCommandFailure(label, result, options = {}) {
2344
2647
  if (stdoutTail.length > 0) {
2345
2648
  lines.push("", "stdout tail:", ...stdoutTail.map((line) => ` ${line}`));
2346
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
+ }
2347
2656
  return lines.join("\n");
2348
2657
  }
2349
2658
  async function runProgressCommand(label, command, args, options = {}) {
@@ -2388,9 +2697,9 @@ async function runProgressCommand(label, command, args, options = {}) {
2388
2697
  throw error;
2389
2698
  }
2390
2699
  }
2391
- function assertProgressCommandSucceeded(label, result, message) {
2700
+ function assertProgressCommandSucceeded(label, result, message, options = {}) {
2392
2701
  if (result.code !== 0) {
2393
- throw new Error(`${message}\n${formatCommandFailure(label, result)}`);
2702
+ throw new Error(`${message}\n${formatCommandFailure(label, result, options)}`);
2394
2703
  }
2395
2704
  }
2396
2705
 
@@ -3123,7 +3432,7 @@ async function startDevcontainer(context, options = {}) {
3123
3432
  mirrorStdout: proxyMode ? "stderr" : "stdout",
3124
3433
  mirrorStderr: "stderr",
3125
3434
  logger: options.logger
3126
- }) : await runProgressCommand("devcontainer up", cli.command, [...cli.argsPrefix, ...args], {
3435
+ }) : await (options.runDevcontainerUp ?? runProgressCommand)("devcontainer up", cli.command, [...cli.argsPrefix, ...args], {
3127
3436
  progress,
3128
3437
  spinnerLabel: "Starting devcontainer",
3129
3438
  stepId: "devcontainer-start",
@@ -3135,7 +3444,7 @@ async function startDevcontainer(context, options = {}) {
3135
3444
  throw new Error(`devcontainer up failed for ${context.workspaceFolder}`);
3136
3445
  }
3137
3446
  if (progress !== undefined) {
3138
- 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 });
3139
3448
  }
3140
3449
  const containerId = parseContainerIdFromUpOutput(`${result.stdout}\n${result.stderr}`) ?? await findRunningContainerId(context, { logger: options.logger });
3141
3450
  if (containerId === undefined) {
@@ -3233,7 +3542,7 @@ async function ensureContainerSshRuntime(context, options = {}) {
3233
3542
  throw new Error("Failed to prepare devcontainer SSH runtime");
3234
3543
  }
3235
3544
  if (options.progress !== undefined) {
3236
- 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 });
3237
3546
  }
3238
3547
  }
3239
3548
  async function refreshContainerCodingAgentClis(context, proxyMode = false, agents = [], options = {}) {
@@ -3300,7 +3609,7 @@ async function ensureContainerCodingAgentCli(context, agent, options = {}) {
3300
3609
  throw new Error(`Could not install or refresh ${codingAgentBinary(agent)} inside the devcontainer`);
3301
3610
  }
3302
3611
  if (options.progress !== undefined) {
3303
- 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 });
3304
3613
  }
3305
3614
  }
3306
3615
  async function runSshdProxy(containerId, options = {}) {
@@ -4613,10 +4922,12 @@ function commandWritesWorkspaceMetadata(command) {
4613
4922
  "ssh-proxy",
4614
4923
  "tunnel",
4615
4924
  "refresh-gh-token",
4616
- "refresh-gh-token-running",
4617
4925
  "coding-agent"
4618
4926
  ].includes(command);
4619
4927
  }
4928
+ function commandRequiresContainerRuntime(command) {
4929
+ return command === "setup" || command === "start" || command === "ssh-proxy" || command === "tunnel" || command === "refresh-gh-token" || command === "coding-agent";
4930
+ }
4620
4931
  function parseCliArgs(argv) {
4621
4932
  const args = [...argv];
4622
4933
  const workspaces = [];
@@ -5312,7 +5623,7 @@ function createCliProgress(parsed, target = "stdout", options = {}) {
5312
5623
  target
5313
5624
  });
5314
5625
  }
5315
- function startProgressSteps() {
5626
+ function devcontainerStartProgressSteps() {
5316
5627
  return [
5317
5628
  {
5318
5629
  id: "ssh-identity",
@@ -5328,13 +5639,19 @@ function startProgressSteps() {
5328
5639
  }
5329
5640
  ];
5330
5641
  }
5642
+ function startProgressSteps() {
5643
+ return [{
5644
+ id: "container-runtime",
5645
+ label: "Checking container runtime"
5646
+ }, ...devcontainerStartProgressSteps()];
5647
+ }
5331
5648
  function sshTargetProgressLabel(target) {
5332
5649
  const label = SSH_INSTALL_TARGETS.find((candidate) => candidate.value === target)?.label ?? target;
5333
5650
  return `Installing ${label} SSH target`;
5334
5651
  }
5335
5652
  function setupProgressSteps(targets) {
5336
5653
  return [
5337
- ...startProgressSteps(),
5654
+ ...devcontainerStartProgressSteps(),
5338
5655
  {
5339
5656
  id: "ssh-alias",
5340
5657
  label: "Installing SSH alias"
@@ -5347,6 +5664,9 @@ function setupProgressSteps(targets) {
5347
5664
  }
5348
5665
  function setupPreflightProgressSteps() {
5349
5666
  return [{
5667
+ id: "container-runtime",
5668
+ label: "Checking container runtime"
5669
+ }, {
5350
5670
  id: "setup-preflight",
5351
5671
  label: "Checking host readiness"
5352
5672
  }];
@@ -5355,13 +5675,55 @@ function setupPreflightFailureMessage(checks) {
5355
5675
  const failures = checks.filter((check) => check.level === "fail").map((check) => `- ${check.name}: ${check.message}`);
5356
5676
  return `Setup preflight failed:\n${failures.join("\n")}`;
5357
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
+ }
5358
5716
  async function runSetupPreflight(context, alias, parsed, options) {
5359
5717
  const progress = createCliProgress(parsed, "stdout", { env: options.env });
5360
5718
  const doctor = options.runDoctorChecks ?? runDoctorChecks;
5361
5719
  await withProgressSection(progress, "Boxdown setup", [`Workspace: ${context.workspaceFolder}`, `SSH alias: ${alias}`], async () => {
5362
5720
  progress.setSteps(setupPreflightProgressSteps());
5721
+ await runContainerRuntimePreflight(context, progress, options);
5363
5722
  progress.startStep("setup-preflight");
5364
- const checks = await doctor(context, { includeOptional: false });
5723
+ const checks = await doctor(context, {
5724
+ includeOptional: false,
5725
+ containerRuntimeReady: true
5726
+ });
5365
5727
  for (const check of checks.filter((item) => item.level === "warn")) {
5366
5728
  progress.warn(`${check.name}: ${check.message}`);
5367
5729
  }
@@ -5473,9 +5835,6 @@ async function runCli(argv = process.argv.slice(2), options = {}) {
5473
5835
  const context = createWorkspaceContext({ workspace: parsed.workspace });
5474
5836
  const alias = parsed.alias ?? defaultSshAlias(context.workspaceBasename);
5475
5837
  const aliasSource = parsed.alias === undefined ? "default" : "provided";
5476
- if (parsed.command !== "ssh-install" && parsed.command !== "setup" && parsed.command !== "tunnel" && commandWritesWorkspaceMetadata(parsed.command)) {
5477
- writeWorkspaceMetadata(context, alias);
5478
- }
5479
5838
  if (parsed.command === "ssh-install") {
5480
5839
  const resolvedTargets = await resolveSshInstallTargets(parsed, options);
5481
5840
  if (resolvedTargets.cancelled) {
@@ -5572,6 +5931,7 @@ async function runCli(argv = process.argv.slice(2), options = {}) {
5572
5931
  const progress = createCliProgress(parsed, "stderr", { env: options.env });
5573
5932
  const containerId = await withProgressSection(progress, "Boxdown SSH proxy", [`Workspace: ${context.workspaceFolder}`, `SSH alias: ${alias}`], async () => {
5574
5933
  progress.setSteps(sshProxyProgressSteps());
5934
+ await (options.prepareContainerLifecycle ?? prepareContainerLifecycle)(context, alias, progress, options, logger);
5575
5935
  progress.startStep("ssh-alias");
5576
5936
  try {
5577
5937
  await installSshConfig(context, alias, { quiet: true });
@@ -5610,11 +5970,11 @@ async function runCli(argv = process.argv.slice(2), options = {}) {
5610
5970
  if (tunnelPorts.length === 0) {
5611
5971
  throw new Error("tunnel requires at least one --port value");
5612
5972
  }
5613
- writeWorkspaceMetadata(context, alias);
5614
5973
  return runLoggedLifecycle(context, "tunnel", argv, async (logger) => {
5615
5974
  const progress = createCliProgress(parsed, "stdout", { env: options.env });
5616
5975
  await withProgressSection(progress, "Boxdown tunnel", [`Workspace: ${context.workspaceFolder}`, `SSH alias: ${alias}`], async () => {
5617
5976
  progress.setSteps(tunnelProgressSteps());
5977
+ await (options.prepareContainerLifecycle ?? prepareContainerLifecycle)(context, alias, progress, options, logger);
5618
5978
  progress.startStep("ssh-alias");
5619
5979
  try {
5620
5980
  await installSshConfig(context, alias, { quiet: true });
@@ -5638,7 +5998,7 @@ async function runCli(argv = process.argv.slice(2), options = {}) {
5638
5998
  }
5639
5999
  if (parsed.command === "refresh-gh-token-running") {
5640
6000
  return runLoggedLifecycle(context, "refresh-gh-token-running", argv, async (logger) => {
5641
- const containerId = await findRunningContainerId(context, { logger });
6001
+ const containerId = await (options.findRunningContainerId ?? findRunningContainerId)(context, { logger });
5642
6002
  if (containerId === undefined) {
5643
6003
  throw new Error(`No running devcontainer found for: ${context.workspaceFolder}`);
5644
6004
  }
@@ -5660,6 +6020,7 @@ async function runCli(argv = process.argv.slice(2), options = {}) {
5660
6020
  const progress = createCliProgress(parsed, "stdout", { env: options.env });
5661
6021
  await withProgressSection(progress, "Boxdown GitHub auth refresh", [`Workspace: ${context.workspaceFolder}`], async () => {
5662
6022
  progress.setSteps(ghAuthProgressSteps(true));
6023
+ await (options.prepareContainerLifecycle ?? prepareContainerLifecycle)(context, alias, progress, options, logger);
5663
6024
  await startDevcontainer(context, {
5664
6025
  progress,
5665
6026
  logger
@@ -5681,6 +6042,7 @@ async function runCli(argv = process.argv.slice(2), options = {}) {
5681
6042
  const progress = createCliProgress(parsed, "stdout", { env: options.env });
5682
6043
  await withProgressSection(progress, `Boxdown ${agent}`, [`Workspace: ${context.workspaceFolder}`], async () => {
5683
6044
  progress.setSteps(codingAgentProgressSteps(agent));
6045
+ await (options.prepareContainerLifecycle ?? prepareContainerLifecycle)(context, alias, progress, options, logger);
5684
6046
  await startDevcontainer(context, {
5685
6047
  recreate: parsed.recreate,
5686
6048
  progress,
@@ -5698,6 +6060,7 @@ async function runCli(argv = process.argv.slice(2), options = {}) {
5698
6060
  const progress = createCliProgress(parsed, "stdout", { env: options.env });
5699
6061
  const containerId = await withProgressSection(progress, "Boxdown start", [`Workspace: ${context.workspaceFolder}`], async () => {
5700
6062
  progress.setSteps(startProgressSteps());
6063
+ await (options.prepareContainerLifecycle ?? prepareContainerLifecycle)(context, alias, progress, options, logger);
5701
6064
  return await startDevcontainer(context, {
5702
6065
  recreate: parsed.recreate,
5703
6066
  progress,
@@ -5714,5 +6077,5 @@ async function runCli(argv = process.argv.slice(2), options = {}) {
5714
6077
  }
5715
6078
 
5716
6079
  //#endregion
5717
- export { parseTunnelPortList as a, parseTunnelPort as i, commandWritesWorkspaceMetadata as n, runCli as o, parseCliArgs as r, setupWorkspace as s, USAGE as t };
5718
- //# sourceMappingURL=main-J4_2Up3o.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