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.
- package/README.md +10 -0
- package/assets/devcontainer/README.md +29 -6
- package/assets/devcontainer/devcontainer.json +4 -9
- package/assets/devcontainer/hooks/initialize.sh +49 -27
- package/assets/devcontainer/hooks/post-create.sh +23 -1
- package/assets/devcontainer/hooks/post-start.sh +0 -10
- package/assets/devcontainer/utils/git-signing-bootstrap.sh +58 -5
- package/assets/devcontainer/utils/secret-env-bootstrap.sh +22 -0
- package/assets/devcontainer/utils/ssh-agent-proxy-bootstrap.sh +27 -0
- package/assets/devcontainer/utils/ssh-agent-proxy.mjs +39 -0
- package/dist/bin/cli.cjs +1 -1
- package/dist/bin/cli.mjs +1 -1
- package/dist/{main-Df4E8ARj.cjs → main-CT2n9qcb.cjs} +907 -266
- package/dist/{main-XMBsKjIK.mjs → main-O__JaiXe.mjs} +891 -268
- package/dist/main-O__JaiXe.mjs.map +1 -0
- package/dist/main.cjs +4 -1
- package/dist/main.d.cts +98 -26
- package/dist/main.d.cts.map +1 -1
- package/dist/main.d.mts +98 -26
- package/dist/main.d.mts.map +1 -1
- package/dist/main.mjs +2 -2
- package/docs/features/commit-signing.md +71 -0
- package/docs/features/generated-config-and-state.md +19 -4
- package/docs/features/github-auth-refresh.md +3 -2
- package/docs/features/lifecycle.md +19 -0
- package/docs/features/setup.md +5 -0
- package/docs/features/start-and-shell.md +10 -0
- package/docs/superpowers/plans/2026-07-17-node-ssh-agent-proxy-signing.md +132 -0
- package/docs/superpowers/plans/2026-07-17-runtime-secret-environment.md +174 -0
- package/docs/superpowers/plans/2026-07-18-architecture-aware-1password-installer.md +163 -0
- package/docs/superpowers/plans/2026-07-18-devcontainer-node-image-digest.md +341 -0
- package/docs/superpowers/plans/2026-07-19-container-runtime-readiness.md +1536 -0
- package/docs/superpowers/specs/2026-07-11-default-commit-signing-design.md +20 -0
- package/docs/superpowers/specs/2026-07-17-node-ssh-agent-proxy-design.md +63 -0
- package/docs/superpowers/specs/2026-07-17-runtime-secret-environment-design.md +194 -0
- package/docs/superpowers/specs/2026-07-18-architecture-aware-1password-installer-design.md +63 -0
- package/docs/superpowers/specs/2026-07-18-devcontainer-node-image-digest-design.md +108 -0
- package/docs/superpowers/specs/2026-07-19-container-runtime-readiness-design.md +353 -0
- package/package.json +1 -1
- package/src/config.ts +14 -2
- package/src/constants.ts +7 -0
- package/src/container-runtime.ts +295 -0
- package/src/devcontainer.ts +57 -30
- package/src/doctor.ts +169 -46
- package/src/git-signing.ts +205 -25
- package/src/logging.ts +11 -1
- package/src/main.ts +97 -12
- package/src/paths.ts +21 -1
- package/src/process.ts +50 -10
- package/src/progress.ts +63 -8
- package/src/purge.ts +8 -0
- package/src/shell.ts +6 -0
- package/dist/main-XMBsKjIK.mjs.map +0 -1
|
@@ -2,15 +2,16 @@ let node_fs = require("node:fs");
|
|
|
2
2
|
let node_crypto = require("node:crypto");
|
|
3
3
|
let node_os = require("node:os");
|
|
4
4
|
let node_path = require("node:path");
|
|
5
|
-
let
|
|
5
|
+
let node_perf_hooks = require("node:perf_hooks");
|
|
6
6
|
let node_child_process = require("node:child_process");
|
|
7
|
+
let node_module = require("node:module");
|
|
7
8
|
let node_readline = require("node:readline");
|
|
8
9
|
let node_url = require("node:url");
|
|
9
10
|
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 = (0, node_fs.readFileSync)(statePath, "utf8");
|
|
610
611
|
const existingState = JSON.parse(existingJson);
|
|
611
|
-
if (!isRecord
|
|
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 = (0, node_fs.readFileSync)(statePath, "utf8");
|
|
640
641
|
const existingState = JSON.parse(existingJson);
|
|
641
|
-
if (!isRecord
|
|
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 ===
|
|
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 = (0, node_module.createRequire)((0, node_path.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((0, node_fs.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 = (0, node_path.resolve)((0, node_path.dirname)(packageJsonPath), bin);
|
|
975
|
-
if (!(0, node_fs.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 = (0, node_child_process.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 (
|
|
1068
|
-
|
|
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 (
|
|
1081
|
-
|
|
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: () => node_perf_hooks.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 = (0, node_module.createRequire)((0, node_path.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((0, node_fs.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 = (0, node_path.resolve)((0, node_path.dirname)(packageJsonPath), bin);
|
|
1369
|
+
if (!(0, node_fs.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 = (0, node_path.join)(options.homeDir, value.slice(2));
|
|
1434
|
+
} else {
|
|
1435
|
+
keyPath = (0, node_path.isAbsolute)(value) ? value : (0, node_path.resolve)(options.workspaceFolder, value);
|
|
1436
|
+
}
|
|
1437
|
+
let publicKeyText;
|
|
1438
|
+
try {
|
|
1439
|
+
publicKeyText = (0, node_fs.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
|
+
(0, node_fs.mkdirSync)(context.gitSigningStateDir, {
|
|
1470
|
+
recursive: true,
|
|
1471
|
+
mode: 448
|
|
1472
|
+
});
|
|
1473
|
+
(0, node_fs.writeFileSync)((0, node_path.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 = (0, node_fs.existsSync)(context.generatedConfigPath) ? JSON.parse((0, node_fs.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
|
|
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: (0, node_path.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 &&
|
|
1156
|
-
message: sshAgent.code !== 0 ? "
|
|
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
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
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", (0, node_fs.existsSync)(context.assetsDevcontainerDir), `Devcontainer assets found at ${context.assetsDevcontainerDir}`, `Missing Boxdown devcontainer assets: ${context.assetsDevcontainerDir}`));
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
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 &&
|
|
1183
|
-
|
|
1184
|
-
"
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
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/${
|
|
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 &&
|
|
1204
|
-
message: signing.code !== 0 ? "GitHub SSH signing-key registration could not be checked" : signing.stdout.includes(
|
|
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
|
(0, node_fs.mkdirSync)(context.workspaceDataDir, { recursive: true });
|
|
1249
1877
|
const runtimeProbeDir = (0, node_fs.mkdtempSync)((0, node_path.join)(context.workspaceDataDir, "doctor-mount-probe-"));
|
|
1878
|
+
(0, node_fs.mkdirSync)(context.workspaceSecretEnvDir, {
|
|
1879
|
+
recursive: true,
|
|
1880
|
+
mode: 448
|
|
1881
|
+
});
|
|
1882
|
+
(0, node_fs.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
|
-
(0, node_fs.mkdirSync)(context.gitSigningStateDir, {
|
|
1518
|
-
recursive: true,
|
|
1519
|
-
mode: 448
|
|
1520
|
-
});
|
|
1521
|
-
(0, node_fs.writeFileSync)((0, node_path.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.#
|
|
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
|
|
2109
|
-
return output.split(/\r?\n/u).map((line) => line.trimEnd()).filter((line) => line.trim().length > 0)
|
|
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
|
|
2114
|
-
const
|
|
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
|
|
2710
|
-
if (
|
|
3240
|
+
const [rawId, rawName] = output.trim().split("|");
|
|
3241
|
+
if (rawId === undefined || rawId.length === 0) {
|
|
2711
3242
|
return undefined;
|
|
2712
3243
|
}
|
|
2713
|
-
let
|
|
3244
|
+
let id;
|
|
3245
|
+
let name;
|
|
2714
3246
|
try {
|
|
2715
|
-
|
|
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
|
|
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
|
|
2725
|
-
...
|
|
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
|
-
|
|
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
|
-
|
|
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 (0, node_path.join)((0, node_os.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 (0, node_path.join)(env.XDG_RUNTIME_DIR, PACKAGE_NAME);
|
|
4549
|
+
}
|
|
4550
|
+
return (0, node_path.join)((0, node_os.tmpdir)(), `${PACKAGE_NAME}-${process.getuid?.() ?? "user"}`);
|
|
4551
|
+
}
|
|
3995
4552
|
function defaultHostAgentsDir(env = process.env) {
|
|
3996
4553
|
return (0, node_path.join)(env.HOME ?? (0, node_os.homedir)(), ".agents");
|
|
3997
4554
|
}
|
|
@@ -4007,10 +4564,12 @@ function createWorkspaceContextFromIdentity(identity, options = {}) {
|
|
|
4007
4564
|
const assetsDevcontainerDir = options.assetsDevcontainerDir ?? env.BOXDOWN_DEVCONTAINER_ASSETS_DIR ?? (0, node_path.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 = (0, node_path.join)(cacheRoot, "workspaces", identity.workspaceId);
|
|
4013
4571
|
const workspaceDataDir = (0, node_path.join)(dataRoot, "workspaces", identity.workspaceId);
|
|
4572
|
+
const workspaceRuntimeDir = (0, node_path.join)(runtimeRoot, "workspaces", identity.workspaceId);
|
|
4014
4573
|
const hostGitconfigSnapshotDir = (0, node_path.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: (0, node_path.join)(workspaceRuntimeDir, "secrets"),
|
|
4025
4587
|
generatedConfigPath: (0, node_path.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
|
|
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
|
-
...
|
|
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, {
|
|
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,
|
|
@@ -5460,6 +6083,12 @@ Object.defineProperty(exports, 'USAGE', {
|
|
|
5460
6083
|
return USAGE;
|
|
5461
6084
|
}
|
|
5462
6085
|
});
|
|
6086
|
+
Object.defineProperty(exports, 'commandRequiresContainerRuntime', {
|
|
6087
|
+
enumerable: true,
|
|
6088
|
+
get: function () {
|
|
6089
|
+
return commandRequiresContainerRuntime;
|
|
6090
|
+
}
|
|
6091
|
+
});
|
|
5463
6092
|
Object.defineProperty(exports, 'commandWritesWorkspaceMetadata', {
|
|
5464
6093
|
enumerable: true,
|
|
5465
6094
|
get: function () {
|
|
@@ -5484,12 +6113,24 @@ Object.defineProperty(exports, 'parseTunnelPortList', {
|
|
|
5484
6113
|
return parseTunnelPortList;
|
|
5485
6114
|
}
|
|
5486
6115
|
});
|
|
6116
|
+
Object.defineProperty(exports, 'prepareContainerLifecycle', {
|
|
6117
|
+
enumerable: true,
|
|
6118
|
+
get: function () {
|
|
6119
|
+
return prepareContainerLifecycle;
|
|
6120
|
+
}
|
|
6121
|
+
});
|
|
5487
6122
|
Object.defineProperty(exports, 'runCli', {
|
|
5488
6123
|
enumerable: true,
|
|
5489
6124
|
get: function () {
|
|
5490
6125
|
return runCli;
|
|
5491
6126
|
}
|
|
5492
6127
|
});
|
|
6128
|
+
Object.defineProperty(exports, 'runContainerRuntimePreflight', {
|
|
6129
|
+
enumerable: true,
|
|
6130
|
+
get: function () {
|
|
6131
|
+
return runContainerRuntimePreflight;
|
|
6132
|
+
}
|
|
6133
|
+
});
|
|
5493
6134
|
Object.defineProperty(exports, 'setupWorkspace', {
|
|
5494
6135
|
enumerable: true,
|
|
5495
6136
|
get: function () {
|