@pablozaiden/devbox 0.1.1 → 0.1.3
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 +25 -10
- package/dist/devbox.js +227 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ It does not modify the original `devcontainer.json`. Instead, it generates a der
|
|
|
6
6
|
|
|
7
7
|
## What it does
|
|
8
8
|
|
|
9
|
-
- Discovers `.devcontainer/devcontainer.json` or `.devcontainer.json` in the current directory.
|
|
9
|
+
- Discovers `.devcontainer/devcontainer.json` or `.devcontainer.json` in the current directory, and can target `.devcontainer/<subpath>/devcontainer.json` with a flag.
|
|
10
10
|
- Reuses or creates the devcontainer with Docker + Dev Container CLI.
|
|
11
11
|
- Names the managed container as `devbox-<project>-<port>`.
|
|
12
12
|
- Publishes the same TCP port on host and container.
|
|
@@ -31,6 +31,8 @@ npm install -g @pablozaiden/devbox
|
|
|
31
31
|
|
|
32
32
|
After either install, `devbox` is available in any directory.
|
|
33
33
|
|
|
34
|
+
Run `devbox` with no arguments to see the CLI help.
|
|
35
|
+
|
|
34
36
|
## Requirements
|
|
35
37
|
|
|
36
38
|
- macOS or Linux
|
|
@@ -45,23 +47,34 @@ After either install, `devbox` is available in any directory.
|
|
|
45
47
|
## Commands
|
|
46
48
|
|
|
47
49
|
```bash
|
|
48
|
-
#
|
|
49
|
-
devbox
|
|
50
|
+
# Show CLI help
|
|
51
|
+
devbox
|
|
50
52
|
|
|
51
|
-
#
|
|
52
|
-
devbox up
|
|
53
|
+
# Start or reuse the devcontainer on a chosen port
|
|
54
|
+
devbox up <port>
|
|
53
55
|
|
|
54
56
|
# Continue even if SSH agent sharing is unavailable
|
|
55
|
-
devbox up
|
|
57
|
+
devbox up <port> --allow-missing-ssh
|
|
58
|
+
|
|
59
|
+
# Use a specific devcontainer under .devcontainer/services/api
|
|
60
|
+
devbox up <port> --devcontainer-subpath services/api
|
|
56
61
|
|
|
57
62
|
# Rebuild/recreate the managed devcontainer
|
|
58
|
-
devbox rebuild
|
|
63
|
+
devbox rebuild <port>
|
|
64
|
+
|
|
65
|
+
# Reuse the last stored port for this workspace
|
|
66
|
+
devbox up
|
|
67
|
+
|
|
68
|
+
# Open an interactive shell in the running managed devcontainer for this workspace
|
|
69
|
+
devbox shell
|
|
59
70
|
|
|
60
71
|
# Stop and remove the managed container while preserving the workspace-mounted SSH credentials
|
|
61
72
|
devbox down
|
|
62
73
|
```
|
|
63
74
|
|
|
64
|
-
If you omit the port for `up` or `rebuild`, `devbox` will reuse the last port stored for the current workspace.
|
|
75
|
+
There is no default port. If you omit the port for `up` or `rebuild`, `devbox` will reuse the last port stored for the current workspace; otherwise pass a port explicitly.
|
|
76
|
+
|
|
77
|
+
`devbox shell` requires an already running managed container for the current workspace. If none is running, use `devbox up` first.
|
|
65
78
|
|
|
66
79
|
## Development
|
|
67
80
|
|
|
@@ -82,14 +95,16 @@ For a quick smoke test, this repository includes `examples/smoke-workspace/.devc
|
|
|
82
95
|
|
|
83
96
|
```bash
|
|
84
97
|
cd examples/smoke-workspace
|
|
85
|
-
../../dist/devbox.js up
|
|
98
|
+
../../dist/devbox.js up <port> --allow-missing-ssh
|
|
86
99
|
```
|
|
87
100
|
|
|
88
101
|
## Notes
|
|
89
102
|
|
|
90
103
|
- The generated config is written next to the original devcontainer config, using the alternate accepted devcontainer filename so relative Dockerfile paths keep working.
|
|
104
|
+
- `--devcontainer-subpath services/api` tells `devbox` to use `.devcontainer/services/api/devcontainer.json`.
|
|
105
|
+
- `devbox shell` opens an interactive shell inside the running managed container for the current workspace.
|
|
91
106
|
- `down` removes managed containers but does not delete the workspace `.sshcred` or `.devbox-ssh-host-keys/`, so the SSH password and SSH host identity survive rebuilds.
|
|
92
|
-
- Re-running `devbox` after a host restart recreates the desired state: container up, port published, SSH runner started again.
|
|
107
|
+
- Re-running `devbox up` after a host restart recreates the desired state: container up, port published, SSH runner started again.
|
|
93
108
|
- When Docker Desktop host services are available, `devbox` can share the SSH agent without relying on a host-shell `SSH_AUTH_SOCK`.
|
|
94
109
|
- On Docker Desktop, `devbox` prefers the Docker-provided SSH agent socket over the host `SSH_AUTH_SOCK`, which avoids macOS launchd socket mount issues.
|
|
95
110
|
- `--allow-missing-ssh` starts the workspace without mounting an SSH agent and prints a warning instead of failing.
|
package/dist/devbox.js
CHANGED
|
@@ -839,33 +839,54 @@ function helpText() {
|
|
|
839
839
|
return `${CLI_NAME} - manage a devcontainer plus ssh-server-runner
|
|
840
840
|
|
|
841
841
|
Usage:
|
|
842
|
-
${CLI_NAME}
|
|
843
|
-
${CLI_NAME} up [port] [--allow-missing-ssh]
|
|
844
|
-
${CLI_NAME} rebuild [port] [--allow-missing-ssh]
|
|
845
|
-
${CLI_NAME}
|
|
842
|
+
${CLI_NAME}
|
|
843
|
+
${CLI_NAME} up [port] [--allow-missing-ssh] [--devcontainer-subpath <subpath>]
|
|
844
|
+
${CLI_NAME} rebuild [port] [--allow-missing-ssh] [--devcontainer-subpath <subpath>]
|
|
845
|
+
${CLI_NAME} shell
|
|
846
|
+
${CLI_NAME} down [--devcontainer-subpath <subpath>]
|
|
847
|
+
${CLI_NAME} help
|
|
846
848
|
${CLI_NAME} --help
|
|
847
849
|
|
|
850
|
+
Commands:
|
|
851
|
+
up Start or reuse the managed devcontainer.
|
|
852
|
+
rebuild Recreate the managed devcontainer.
|
|
853
|
+
shell Open an interactive shell in the running managed container.
|
|
854
|
+
down Stop and remove the managed container for this workspace.
|
|
855
|
+
help Show this help.
|
|
856
|
+
|
|
857
|
+
Options:
|
|
858
|
+
-p, --port <port> Publish the same port on host and container.
|
|
859
|
+
--allow-missing-ssh Continue without SSH agent sharing when unavailable.
|
|
860
|
+
--devcontainer-subpath <path> Use .devcontainer/<path>/devcontainer.json.
|
|
861
|
+
-h, --help Show this help.
|
|
862
|
+
|
|
848
863
|
Notes:
|
|
864
|
+
- Running ${CLI_NAME} with no arguments shows this help.
|
|
849
865
|
- The same port is published on host and container.
|
|
850
|
-
-
|
|
851
|
-
-
|
|
866
|
+
- There is no default port. Pass one explicitly the first time, or reuse the last stored port with \`${CLI_NAME} up\` / \`${CLI_NAME} rebuild\`.
|
|
867
|
+
- ${CLI_NAME} shell opens an interactive shell in the running managed container for this workspace.
|
|
852
868
|
- Only image/Dockerfile-based devcontainers are supported in v1.`;
|
|
853
869
|
}
|
|
854
870
|
function parseArgs(argv) {
|
|
855
871
|
const args = [...argv];
|
|
856
872
|
if (args.length === 0) {
|
|
857
|
-
return { command: "
|
|
873
|
+
return { command: "help", allowMissingSsh: false };
|
|
858
874
|
}
|
|
859
|
-
let command
|
|
875
|
+
let command;
|
|
860
876
|
const first = args[0];
|
|
861
|
-
if (first === "up" || first === "down" || first === "rebuild" || first === "
|
|
877
|
+
if (first === "up" || first === "down" || first === "rebuild" || first === "shell") {
|
|
862
878
|
command = first;
|
|
863
879
|
args.shift();
|
|
880
|
+
} else if (first === "help") {
|
|
881
|
+
return { command: "help", allowMissingSsh: false };
|
|
864
882
|
} else if (first === "--help" || first === "-h") {
|
|
865
883
|
return { command: "help", allowMissingSsh: false };
|
|
884
|
+
} else {
|
|
885
|
+
throw new UserError(`A command is required. Run \`${CLI_NAME} --help\` for usage.`);
|
|
866
886
|
}
|
|
867
887
|
let port;
|
|
868
888
|
let allowMissingSsh = false;
|
|
889
|
+
let devcontainerSubpath;
|
|
869
890
|
const positionals = [];
|
|
870
891
|
for (let index = 0;index < args.length; index += 1) {
|
|
871
892
|
const arg = args[index];
|
|
@@ -876,6 +897,19 @@ function parseArgs(argv) {
|
|
|
876
897
|
allowMissingSsh = true;
|
|
877
898
|
continue;
|
|
878
899
|
}
|
|
900
|
+
if (arg === "--devcontainer-subpath") {
|
|
901
|
+
const value = args[index + 1];
|
|
902
|
+
if (!value) {
|
|
903
|
+
throw new UserError("Expected a value after --devcontainer-subpath.");
|
|
904
|
+
}
|
|
905
|
+
devcontainerSubpath = parseDevcontainerSubpath(value);
|
|
906
|
+
index += 1;
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
if (arg.startsWith("--devcontainer-subpath=")) {
|
|
910
|
+
devcontainerSubpath = parseDevcontainerSubpath(arg.slice("--devcontainer-subpath=".length));
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
879
913
|
if (arg === "--port" || arg === "-p") {
|
|
880
914
|
const value = args[index + 1];
|
|
881
915
|
if (!value) {
|
|
@@ -901,11 +935,23 @@ function parseArgs(argv) {
|
|
|
901
935
|
if (command === "down") {
|
|
902
936
|
throw new UserError("The down command does not accept a port.");
|
|
903
937
|
}
|
|
938
|
+
if (command === "shell") {
|
|
939
|
+
throw new UserError("The shell command does not accept a port.");
|
|
940
|
+
}
|
|
904
941
|
port = parsePort(positionals[0]);
|
|
905
942
|
}
|
|
906
943
|
if (command === "down" && port !== undefined) {
|
|
907
944
|
throw new UserError("The down command does not accept a port.");
|
|
908
945
|
}
|
|
946
|
+
if (command === "shell" && port !== undefined) {
|
|
947
|
+
throw new UserError("The shell command does not accept a port.");
|
|
948
|
+
}
|
|
949
|
+
if (command === "shell" && devcontainerSubpath !== undefined) {
|
|
950
|
+
throw new UserError("The shell command does not accept --devcontainer-subpath.");
|
|
951
|
+
}
|
|
952
|
+
if (devcontainerSubpath) {
|
|
953
|
+
return { command, port, allowMissingSsh, devcontainerSubpath };
|
|
954
|
+
}
|
|
909
955
|
return { command, port, allowMissingSsh };
|
|
910
956
|
}
|
|
911
957
|
function parsePort(raw) {
|
|
@@ -977,7 +1023,7 @@ async function deleteWorkspaceState(workspacePath) {
|
|
|
977
1023
|
await rm(getWorkspaceStateDir(workspacePath), { recursive: true, force: true });
|
|
978
1024
|
}
|
|
979
1025
|
function resolvePort(command, explicitPort, state) {
|
|
980
|
-
if (command === "down" || command === "help") {
|
|
1026
|
+
if (command === "down" || command === "shell" || command === "help") {
|
|
981
1027
|
throw new UserError(`resolvePort cannot be used for ${command}.`);
|
|
982
1028
|
}
|
|
983
1029
|
if (explicitPort !== undefined) {
|
|
@@ -986,13 +1032,10 @@ function resolvePort(command, explicitPort, state) {
|
|
|
986
1032
|
if (state) {
|
|
987
1033
|
return state.port;
|
|
988
1034
|
}
|
|
989
|
-
throw new UserError(`No port was provided and no previous port is stored for this workspace. Run \`${CLI_NAME} <port>\` first.`);
|
|
1035
|
+
throw new UserError(`No port was provided and no previous port is stored for this workspace. Run \`${CLI_NAME} up <port>\` first.`);
|
|
990
1036
|
}
|
|
991
|
-
async function discoverDevcontainerConfig(workspacePath) {
|
|
992
|
-
const candidates =
|
|
993
|
-
path.join(workspacePath, ".devcontainer", "devcontainer.json"),
|
|
994
|
-
path.join(workspacePath, ".devcontainer.json")
|
|
995
|
-
];
|
|
1037
|
+
async function discoverDevcontainerConfig(workspacePath, devcontainerSubpath) {
|
|
1038
|
+
const candidates = getDevcontainerCandidates(workspacePath, devcontainerSubpath);
|
|
996
1039
|
for (const candidate of candidates) {
|
|
997
1040
|
if (!existsSync(candidate)) {
|
|
998
1041
|
continue;
|
|
@@ -1014,7 +1057,8 @@ async function discoverDevcontainerConfig(workspacePath) {
|
|
|
1014
1057
|
validateSupportedDevcontainerConfig(config);
|
|
1015
1058
|
return { path: candidate, config };
|
|
1016
1059
|
}
|
|
1017
|
-
|
|
1060
|
+
const expectedLocations = devcontainerSubpath ? `.devcontainer/${formatDevcontainerSubpath(devcontainerSubpath)}/devcontainer.json` : ".devcontainer/devcontainer.json or .devcontainer.json";
|
|
1061
|
+
throw new UserError(`No devcontainer definition was found in ${workspacePath}. Expected ${expectedLocations}.`);
|
|
1018
1062
|
}
|
|
1019
1063
|
function validateSupportedDevcontainerConfig(config) {
|
|
1020
1064
|
if (config.dockerComposeFile !== undefined) {
|
|
@@ -1056,17 +1100,18 @@ function buildManagedConfig(baseConfig, options) {
|
|
|
1056
1100
|
runArgs.push("-p", `${options.port}:${options.port}`);
|
|
1057
1101
|
}
|
|
1058
1102
|
managedConfig.runArgs = runArgs;
|
|
1103
|
+
const containerSshAuthSock = getContainerSshAuthSockPath(options.sshAuthSock);
|
|
1059
1104
|
const mounts = getStringArray(managedConfig.mounts, "mounts");
|
|
1060
|
-
if (options.sshAuthSock) {
|
|
1061
|
-
mounts.push(`type=bind,source=${options.sshAuthSock},target=${
|
|
1105
|
+
if (options.sshAuthSock && containerSshAuthSock) {
|
|
1106
|
+
mounts.push(`type=bind,source=${options.sshAuthSock},target=${containerSshAuthSock}`);
|
|
1062
1107
|
}
|
|
1063
1108
|
if (options.knownHostsPath) {
|
|
1064
1109
|
mounts.push(`type=bind,source=${options.knownHostsPath},target=${KNOWN_HOSTS_TARGET},readonly`);
|
|
1065
1110
|
}
|
|
1066
1111
|
managedConfig.mounts = dedupe(mounts);
|
|
1067
1112
|
const containerEnv = getStringRecord(managedConfig.containerEnv, "containerEnv");
|
|
1068
|
-
if (
|
|
1069
|
-
containerEnv.SSH_AUTH_SOCK =
|
|
1113
|
+
if (containerSshAuthSock) {
|
|
1114
|
+
containerEnv.SSH_AUTH_SOCK = containerSshAuthSock;
|
|
1070
1115
|
}
|
|
1071
1116
|
managedConfig.containerEnv = containerEnv;
|
|
1072
1117
|
return managedConfig;
|
|
@@ -1097,6 +1142,38 @@ async function getKnownHostsPath() {
|
|
|
1097
1142
|
function quoteShell(value) {
|
|
1098
1143
|
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
1099
1144
|
}
|
|
1145
|
+
function getContainerSshAuthSockPath(sshAuthSock) {
|
|
1146
|
+
if (!sshAuthSock) {
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
return sshAuthSock === DOCKER_DESKTOP_SSH_AUTH_SOCK_SOURCE ? DOCKER_DESKTOP_SSH_AUTH_SOCK_SOURCE : SSH_AUTH_SOCK_TARGET;
|
|
1150
|
+
}
|
|
1151
|
+
function parseDevcontainerSubpath(raw) {
|
|
1152
|
+
const trimmed = raw.trim();
|
|
1153
|
+
if (trimmed.length === 0) {
|
|
1154
|
+
throw new UserError("Devcontainer subpath cannot be empty.");
|
|
1155
|
+
}
|
|
1156
|
+
if (path.isAbsolute(trimmed) || trimmed.startsWith("\\")) {
|
|
1157
|
+
throw new UserError(`Devcontainer subpath must stay inside .devcontainer. Received: ${raw}`);
|
|
1158
|
+
}
|
|
1159
|
+
const segments = trimmed.split(/[\\/]+/u).filter((segment) => segment.length > 0);
|
|
1160
|
+
if (segments.length === 0 || segments.some((segment) => segment === "." || segment === "..")) {
|
|
1161
|
+
throw new UserError(`Devcontainer subpath must stay inside .devcontainer. Received: ${raw}`);
|
|
1162
|
+
}
|
|
1163
|
+
return path.join(...segments);
|
|
1164
|
+
}
|
|
1165
|
+
function getDevcontainerCandidates(workspacePath, devcontainerSubpath) {
|
|
1166
|
+
if (devcontainerSubpath) {
|
|
1167
|
+
return [path.join(workspacePath, ".devcontainer", devcontainerSubpath, "devcontainer.json")];
|
|
1168
|
+
}
|
|
1169
|
+
return [
|
|
1170
|
+
path.join(workspacePath, ".devcontainer", "devcontainer.json"),
|
|
1171
|
+
path.join(workspacePath, ".devcontainer.json")
|
|
1172
|
+
];
|
|
1173
|
+
}
|
|
1174
|
+
function formatDevcontainerSubpath(subpath) {
|
|
1175
|
+
return subpath.split(path.sep).join("/");
|
|
1176
|
+
}
|
|
1100
1177
|
function dedupe(values) {
|
|
1101
1178
|
return [...new Set(values)];
|
|
1102
1179
|
}
|
|
@@ -1201,6 +1278,9 @@ function formatDevcontainerProgressLine(line) {
|
|
|
1201
1278
|
if (!cleaned) {
|
|
1202
1279
|
return null;
|
|
1203
1280
|
}
|
|
1281
|
+
if (looksLikeDevcontainerUserEnvProbeDump(cleaned) || cleaned.startsWith("bash: cannot set terminal process group") || cleaned === "bash: no job control in this shell") {
|
|
1282
|
+
return null;
|
|
1283
|
+
}
|
|
1204
1284
|
try {
|
|
1205
1285
|
const parsed = JSON.parse(cleaned);
|
|
1206
1286
|
const text = typeof parsed.text === "string" ? stripAnsi(parsed.text).trim() : "";
|
|
@@ -1244,8 +1324,30 @@ function formatDevcontainerProgressLine(line) {
|
|
|
1244
1324
|
function requiresSshAuthSockPermissionFix(sshAuthSockSource) {
|
|
1245
1325
|
return sshAuthSockSource === DOCKER_DESKTOP_SSH_AUTH_SOCK_SOURCE;
|
|
1246
1326
|
}
|
|
1247
|
-
function buildEnsureSshAuthSockAccessibleScript() {
|
|
1248
|
-
return `if [ -S ${quoteShell(
|
|
1327
|
+
function buildEnsureSshAuthSockAccessibleScript(containerSshAuthSock) {
|
|
1328
|
+
return `if [ -S ${quoteShell(containerSshAuthSock)} ]; then chmod 666 ${quoteShell(containerSshAuthSock)}; fi`;
|
|
1329
|
+
}
|
|
1330
|
+
function buildAssertConfiguredSshAuthSockScript() {
|
|
1331
|
+
return [
|
|
1332
|
+
'if [ -z "${SSH_AUTH_SOCK:-}" ]; then',
|
|
1333
|
+
" exit 0",
|
|
1334
|
+
"fi",
|
|
1335
|
+
'if [ -S "$SSH_AUTH_SOCK" ]; then',
|
|
1336
|
+
" exit 0",
|
|
1337
|
+
"fi",
|
|
1338
|
+
`printf '%s\\n' "SSH_AUTH_SOCK points to a missing socket inside the container: $SSH_AUTH_SOCK. Run devbox rebuild to refresh SSH agent sharing." >&2`,
|
|
1339
|
+
"exit 1"
|
|
1340
|
+
].join(`
|
|
1341
|
+
`);
|
|
1342
|
+
}
|
|
1343
|
+
function buildInteractiveShellScript() {
|
|
1344
|
+
return [
|
|
1345
|
+
"if command -v bash >/dev/null 2>&1; then",
|
|
1346
|
+
" exec bash -l",
|
|
1347
|
+
"fi",
|
|
1348
|
+
"exec sh"
|
|
1349
|
+
].join(`
|
|
1350
|
+
`);
|
|
1249
1351
|
}
|
|
1250
1352
|
function getRunnerHostKeysDir(remoteWorkspaceFolder) {
|
|
1251
1353
|
const trimmed = remoteWorkspaceFolder.endsWith("/") ? remoteWorkspaceFolder.slice(0, -1) : remoteWorkspaceFolder;
|
|
@@ -1418,12 +1520,21 @@ async function copyKnownHosts(containerId) {
|
|
|
1418
1520
|
async function stopManagedSshd(containerId) {
|
|
1419
1521
|
await devcontainerExec(containerId, buildStopManagedSshdScript(), { quiet: true });
|
|
1420
1522
|
}
|
|
1421
|
-
async function ensureSshAuthSockAccessible(containerId) {
|
|
1422
|
-
|
|
1523
|
+
async function ensureSshAuthSockAccessible(containerId, sshAuthSockSource) {
|
|
1524
|
+
const containerSshAuthSock = getContainerSshAuthSockPath(sshAuthSockSource);
|
|
1525
|
+
if (!containerSshAuthSock) {
|
|
1526
|
+
throw new UserError("Cannot adjust SSH agent socket permissions when SSH sharing is disabled.");
|
|
1527
|
+
}
|
|
1528
|
+
await dockerExec(containerId, buildEnsureSshAuthSockAccessibleScript(containerSshAuthSock), {
|
|
1423
1529
|
quiet: true,
|
|
1424
1530
|
user: "root"
|
|
1425
1531
|
});
|
|
1426
1532
|
}
|
|
1533
|
+
async function assertConfiguredSshAuthSockAvailable(containerId) {
|
|
1534
|
+
await dockerExec(containerId, buildAssertConfiguredSshAuthSockScript(), {
|
|
1535
|
+
quiet: true
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1427
1538
|
async function restoreRunnerHostKeys(containerId, remoteWorkspaceFolder) {
|
|
1428
1539
|
await dockerExec(containerId, buildRestoreRunnerHostKeysScript(remoteWorkspaceFolder), {
|
|
1429
1540
|
quiet: true,
|
|
@@ -1453,6 +1564,39 @@ async function persistRunnerHostKeys(containerId, remoteWorkspaceFolder) {
|
|
|
1453
1564
|
user: "root"
|
|
1454
1565
|
});
|
|
1455
1566
|
}
|
|
1567
|
+
function resolveShellContainerId(input) {
|
|
1568
|
+
const running = input.containers.filter((container) => container.State?.Running);
|
|
1569
|
+
if (input.preferredContainerId) {
|
|
1570
|
+
const preferred = running.find((container) => container.Id === input.preferredContainerId);
|
|
1571
|
+
if (preferred) {
|
|
1572
|
+
return preferred.Id;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
if (running.length === 1) {
|
|
1576
|
+
return running[0].Id;
|
|
1577
|
+
}
|
|
1578
|
+
if (running.length === 0) {
|
|
1579
|
+
throw new UserError("No running managed container was found for this workspace. Run `devbox up` first.");
|
|
1580
|
+
}
|
|
1581
|
+
throw new UserError("More than one managed container is running for this workspace. Run `devbox down` first.");
|
|
1582
|
+
}
|
|
1583
|
+
function buildDevcontainerShellCommand(containerId, terminalSize) {
|
|
1584
|
+
const args = ["devcontainer", "exec", "--container-id", containerId];
|
|
1585
|
+
if (terminalSize?.columns && terminalSize.columns > 0) {
|
|
1586
|
+
args.push("--terminal-columns", String(terminalSize.columns));
|
|
1587
|
+
}
|
|
1588
|
+
if (terminalSize?.rows && terminalSize.rows > 0) {
|
|
1589
|
+
args.push("--terminal-rows", String(terminalSize.rows));
|
|
1590
|
+
}
|
|
1591
|
+
args.push("sh", "-lc", buildInteractiveShellScript());
|
|
1592
|
+
return args;
|
|
1593
|
+
}
|
|
1594
|
+
async function openInteractiveShell(containerId) {
|
|
1595
|
+
return executeInteractive(buildDevcontainerShellCommand(containerId, {
|
|
1596
|
+
columns: process.stdout.isTTY ? process.stdout.columns : undefined,
|
|
1597
|
+
rows: process.stdout.isTTY ? process.stdout.rows : undefined
|
|
1598
|
+
}));
|
|
1599
|
+
}
|
|
1456
1600
|
async function hasDockerDesktopHostService() {
|
|
1457
1601
|
const result = await execute(["docker", "info", "--format", "{{.OperatingSystem}}"], {
|
|
1458
1602
|
stdoutMode: "capture",
|
|
@@ -1542,6 +1686,25 @@ async function execute(command, options) {
|
|
|
1542
1686
|
}
|
|
1543
1687
|
return result;
|
|
1544
1688
|
}
|
|
1689
|
+
async function executeInteractive(command, options) {
|
|
1690
|
+
const env = { ...process.env, ...options?.env ?? {} };
|
|
1691
|
+
for (const [key, value] of Object.entries(env)) {
|
|
1692
|
+
if (value === undefined) {
|
|
1693
|
+
delete env[key];
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
const subprocess = spawn(command[0], command.slice(1), {
|
|
1697
|
+
cwd: options?.cwd,
|
|
1698
|
+
env,
|
|
1699
|
+
stdio: "inherit"
|
|
1700
|
+
});
|
|
1701
|
+
return new Promise((resolve, reject) => {
|
|
1702
|
+
subprocess.once("error", reject);
|
|
1703
|
+
subprocess.once("close", (exitCode) => {
|
|
1704
|
+
resolve(exitCode ?? 0);
|
|
1705
|
+
});
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1545
1708
|
async function consumeStream(stream, mode, useStderr) {
|
|
1546
1709
|
if (!stream) {
|
|
1547
1710
|
return "";
|
|
@@ -1643,6 +1806,13 @@ ${details}`;
|
|
|
1643
1806
|
function stripAnsi(text) {
|
|
1644
1807
|
return text.replace(/\u001B\[[0-9;]*m/g, "");
|
|
1645
1808
|
}
|
|
1809
|
+
function looksLikeDevcontainerUserEnvProbeDump(text) {
|
|
1810
|
+
const match = text.match(/^([0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})([\s\S]*)\1$/i);
|
|
1811
|
+
if (!match) {
|
|
1812
|
+
return false;
|
|
1813
|
+
}
|
|
1814
|
+
return /\b(?:HOME|HOSTNAME|PATH|PWD|SHLVL|SSH_AUTH_SOCK|USER)=/.test(match[2]);
|
|
1815
|
+
}
|
|
1646
1816
|
function labelsForWorkspaceHash(workspaceHash) {
|
|
1647
1817
|
return {
|
|
1648
1818
|
[MANAGED_LABEL_KEY]: "true",
|
|
@@ -1659,17 +1829,21 @@ async function main() {
|
|
|
1659
1829
|
}
|
|
1660
1830
|
const workspacePath = await realpath(process.cwd());
|
|
1661
1831
|
const state = await loadWorkspaceState(workspacePath);
|
|
1832
|
+
if (parsed.command === "shell") {
|
|
1833
|
+
await handleShell(workspacePath, state);
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1662
1836
|
if (parsed.command === "down") {
|
|
1663
|
-
await handleDown(workspacePath, state);
|
|
1837
|
+
await handleDown(workspacePath, state, parsed.devcontainerSubpath);
|
|
1664
1838
|
return;
|
|
1665
1839
|
}
|
|
1666
|
-
await handleUpLike(parsed.command, workspacePath, state, parsed.port, parsed.allowMissingSsh);
|
|
1840
|
+
await handleUpLike(parsed.command, workspacePath, state, parsed.port, parsed.allowMissingSsh, parsed.devcontainerSubpath);
|
|
1667
1841
|
}
|
|
1668
|
-
async function handleUpLike(command, workspacePath, state, explicitPort, allowMissingSsh) {
|
|
1842
|
+
async function handleUpLike(command, workspacePath, state, explicitPort, allowMissingSsh, devcontainerSubpath) {
|
|
1669
1843
|
const environment = await ensureHostEnvironment({ allowMissingSsh });
|
|
1670
1844
|
const port = resolvePort(command, explicitPort, state);
|
|
1671
1845
|
const knownHostsPath = await getKnownHostsPath();
|
|
1672
|
-
const discovered = await discoverDevcontainerConfig(workspacePath);
|
|
1846
|
+
const discovered = await discoverDevcontainerConfig(workspacePath, devcontainerSubpath);
|
|
1673
1847
|
const generatedConfigPath = getGeneratedConfigPath(discovered.path);
|
|
1674
1848
|
const legacyGeneratedConfigPath = getLegacyGeneratedConfigPath(discovered.path);
|
|
1675
1849
|
const workspaceHash = hashWorkspacePath(workspacePath);
|
|
@@ -1719,7 +1893,10 @@ async function handleUpLike(command, workspacePath, state, explicitPort, allowMi
|
|
|
1719
1893
|
await ensurePathIgnored(workspacePath, path3.join(workspacePath, RUNNER_HOST_KEYS_DIRNAME));
|
|
1720
1894
|
if (requiresSshAuthSockPermissionFix(environment.sshAuthSock)) {
|
|
1721
1895
|
console.log("Making the forwarded SSH agent socket accessible to the container user...");
|
|
1722
|
-
await ensureSshAuthSockAccessible(upResult.containerId);
|
|
1896
|
+
await ensureSshAuthSockAccessible(upResult.containerId, environment.sshAuthSock);
|
|
1897
|
+
}
|
|
1898
|
+
if (environment.sshAuthSock) {
|
|
1899
|
+
await assertConfiguredSshAuthSockAvailable(upResult.containerId);
|
|
1723
1900
|
}
|
|
1724
1901
|
await copyKnownHosts(upResult.containerId);
|
|
1725
1902
|
await stopManagedSshd(upResult.containerId);
|
|
@@ -1743,7 +1920,25 @@ Ready. ${upResult.containerId.slice(0, 12)} is available on port ${port}.`);
|
|
|
1743
1920
|
console.log("Host known_hosts was not found, so only SSH agent sharing was configured.");
|
|
1744
1921
|
}
|
|
1745
1922
|
}
|
|
1746
|
-
async function
|
|
1923
|
+
async function handleShell(workspacePath, state) {
|
|
1924
|
+
if (!isExecutableAvailable("docker")) {
|
|
1925
|
+
throw new UserError("Docker is required but was not found in PATH.");
|
|
1926
|
+
}
|
|
1927
|
+
if (!isExecutableAvailable("devcontainer")) {
|
|
1928
|
+
throw new UserError("Dev Container CLI is required but was not found in PATH.");
|
|
1929
|
+
}
|
|
1930
|
+
const labels = labelsForWorkspaceHash(hashWorkspacePath(workspacePath));
|
|
1931
|
+
const containerIds = await listManagedContainers(labels);
|
|
1932
|
+
const containers = await inspectContainers(containerIds);
|
|
1933
|
+
const containerId = resolveShellContainerId({
|
|
1934
|
+
containers,
|
|
1935
|
+
preferredContainerId: state?.lastContainerId
|
|
1936
|
+
});
|
|
1937
|
+
await assertConfiguredSshAuthSockAvailable(containerId);
|
|
1938
|
+
console.log(`Opening shell inside ${containerId.slice(0, 12)}...`);
|
|
1939
|
+
process.exitCode = await openInteractiveShell(containerId);
|
|
1940
|
+
}
|
|
1941
|
+
async function handleDown(workspacePath, state, devcontainerSubpath) {
|
|
1747
1942
|
if (!isExecutableAvailable("docker")) {
|
|
1748
1943
|
throw new UserError("Docker is required but was not found in PATH.");
|
|
1749
1944
|
}
|
|
@@ -1755,7 +1950,7 @@ async function handleDown(workspacePath, state) {
|
|
|
1755
1950
|
generatedConfigPaths.add(state.generatedConfigPath);
|
|
1756
1951
|
}
|
|
1757
1952
|
try {
|
|
1758
|
-
const discovered = await discoverDevcontainerConfig(workspacePath);
|
|
1953
|
+
const discovered = await discoverDevcontainerConfig(workspacePath, devcontainerSubpath);
|
|
1759
1954
|
generatedConfigPaths.add(getGeneratedConfigPath(discovered.path));
|
|
1760
1955
|
generatedConfigPaths.add(getLegacyGeneratedConfigPath(discovered.path));
|
|
1761
1956
|
} catch {}
|