@pablozaiden/devbox 0.1.2 → 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 +15 -10
- package/dist/devbox.js +77 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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,20 +47,23 @@ 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
|
|
56
58
|
|
|
57
59
|
# Use a specific devcontainer under .devcontainer/services/api
|
|
58
|
-
devbox up
|
|
60
|
+
devbox up <port> --devcontainer-subpath services/api
|
|
59
61
|
|
|
60
62
|
# Rebuild/recreate the managed devcontainer
|
|
61
|
-
devbox rebuild
|
|
63
|
+
devbox rebuild <port>
|
|
64
|
+
|
|
65
|
+
# Reuse the last stored port for this workspace
|
|
66
|
+
devbox up
|
|
62
67
|
|
|
63
68
|
# Open an interactive shell in the running managed devcontainer for this workspace
|
|
64
69
|
devbox shell
|
|
@@ -67,7 +72,7 @@ devbox shell
|
|
|
67
72
|
devbox down
|
|
68
73
|
```
|
|
69
74
|
|
|
70
|
-
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.
|
|
71
76
|
|
|
72
77
|
`devbox shell` requires an already running managed container for the current workspace. If none is running, use `devbox up` first.
|
|
73
78
|
|
|
@@ -90,7 +95,7 @@ For a quick smoke test, this repository includes `examples/smoke-workspace/.devc
|
|
|
90
95
|
|
|
91
96
|
```bash
|
|
92
97
|
cd examples/smoke-workspace
|
|
93
|
-
../../dist/devbox.js up
|
|
98
|
+
../../dist/devbox.js up <port> --allow-missing-ssh
|
|
94
99
|
```
|
|
95
100
|
|
|
96
101
|
## Notes
|
|
@@ -99,7 +104,7 @@ cd examples/smoke-workspace
|
|
|
99
104
|
- `--devcontainer-subpath services/api` tells `devbox` to use `.devcontainer/services/api/devcontainer.json`.
|
|
100
105
|
- `devbox shell` opens an interactive shell inside the running managed container for the current workspace.
|
|
101
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.
|
|
102
|
-
- 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.
|
|
103
108
|
- When Docker Desktop host services are available, `devbox` can share the SSH agent without relying on a host-shell `SSH_AUTH_SOCK`.
|
|
104
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.
|
|
105
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,50 @@ function helpText() {
|
|
|
839
839
|
return `${CLI_NAME} - manage a devcontainer plus ssh-server-runner
|
|
840
840
|
|
|
841
841
|
Usage:
|
|
842
|
-
${CLI_NAME}
|
|
842
|
+
${CLI_NAME}
|
|
843
843
|
${CLI_NAME} up [port] [--allow-missing-ssh] [--devcontainer-subpath <subpath>]
|
|
844
844
|
${CLI_NAME} rebuild [port] [--allow-missing-ssh] [--devcontainer-subpath <subpath>]
|
|
845
845
|
${CLI_NAME} shell
|
|
846
846
|
${CLI_NAME} down [--devcontainer-subpath <subpath>]
|
|
847
|
+
${CLI_NAME} help
|
|
847
848
|
${CLI_NAME} --help
|
|
848
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
|
+
|
|
849
863
|
Notes:
|
|
864
|
+
- Running ${CLI_NAME} with no arguments shows this help.
|
|
850
865
|
- The same port is published on host and container.
|
|
851
|
-
-
|
|
852
|
-
- Pass --allow-missing-ssh to continue without SSH agent sharing when no usable SSH agent socket is available.
|
|
853
|
-
- Pass --devcontainer-subpath to use .devcontainer/<subpath>/devcontainer.json.
|
|
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\`.
|
|
854
867
|
- ${CLI_NAME} shell opens an interactive shell in the running managed container for this workspace.
|
|
855
868
|
- Only image/Dockerfile-based devcontainers are supported in v1.`;
|
|
856
869
|
}
|
|
857
870
|
function parseArgs(argv) {
|
|
858
871
|
const args = [...argv];
|
|
859
872
|
if (args.length === 0) {
|
|
860
|
-
return { command: "
|
|
873
|
+
return { command: "help", allowMissingSsh: false };
|
|
861
874
|
}
|
|
862
|
-
let command
|
|
875
|
+
let command;
|
|
863
876
|
const first = args[0];
|
|
864
|
-
if (first === "up" || first === "down" || first === "rebuild" || first === "shell"
|
|
877
|
+
if (first === "up" || first === "down" || first === "rebuild" || first === "shell") {
|
|
865
878
|
command = first;
|
|
866
879
|
args.shift();
|
|
880
|
+
} else if (first === "help") {
|
|
881
|
+
return { command: "help", allowMissingSsh: false };
|
|
867
882
|
} else if (first === "--help" || first === "-h") {
|
|
868
883
|
return { command: "help", allowMissingSsh: false };
|
|
884
|
+
} else {
|
|
885
|
+
throw new UserError(`A command is required. Run \`${CLI_NAME} --help\` for usage.`);
|
|
869
886
|
}
|
|
870
887
|
let port;
|
|
871
888
|
let allowMissingSsh = false;
|
|
@@ -1015,7 +1032,7 @@ function resolvePort(command, explicitPort, state) {
|
|
|
1015
1032
|
if (state) {
|
|
1016
1033
|
return state.port;
|
|
1017
1034
|
}
|
|
1018
|
-
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.`);
|
|
1019
1036
|
}
|
|
1020
1037
|
async function discoverDevcontainerConfig(workspacePath, devcontainerSubpath) {
|
|
1021
1038
|
const candidates = getDevcontainerCandidates(workspacePath, devcontainerSubpath);
|
|
@@ -1083,17 +1100,18 @@ function buildManagedConfig(baseConfig, options) {
|
|
|
1083
1100
|
runArgs.push("-p", `${options.port}:${options.port}`);
|
|
1084
1101
|
}
|
|
1085
1102
|
managedConfig.runArgs = runArgs;
|
|
1103
|
+
const containerSshAuthSock = getContainerSshAuthSockPath(options.sshAuthSock);
|
|
1086
1104
|
const mounts = getStringArray(managedConfig.mounts, "mounts");
|
|
1087
|
-
if (options.sshAuthSock) {
|
|
1088
|
-
mounts.push(`type=bind,source=${options.sshAuthSock},target=${
|
|
1105
|
+
if (options.sshAuthSock && containerSshAuthSock) {
|
|
1106
|
+
mounts.push(`type=bind,source=${options.sshAuthSock},target=${containerSshAuthSock}`);
|
|
1089
1107
|
}
|
|
1090
1108
|
if (options.knownHostsPath) {
|
|
1091
1109
|
mounts.push(`type=bind,source=${options.knownHostsPath},target=${KNOWN_HOSTS_TARGET},readonly`);
|
|
1092
1110
|
}
|
|
1093
1111
|
managedConfig.mounts = dedupe(mounts);
|
|
1094
1112
|
const containerEnv = getStringRecord(managedConfig.containerEnv, "containerEnv");
|
|
1095
|
-
if (
|
|
1096
|
-
containerEnv.SSH_AUTH_SOCK =
|
|
1113
|
+
if (containerSshAuthSock) {
|
|
1114
|
+
containerEnv.SSH_AUTH_SOCK = containerSshAuthSock;
|
|
1097
1115
|
}
|
|
1098
1116
|
managedConfig.containerEnv = containerEnv;
|
|
1099
1117
|
return managedConfig;
|
|
@@ -1124,6 +1142,12 @@ async function getKnownHostsPath() {
|
|
|
1124
1142
|
function quoteShell(value) {
|
|
1125
1143
|
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
1126
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
|
+
}
|
|
1127
1151
|
function parseDevcontainerSubpath(raw) {
|
|
1128
1152
|
const trimmed = raw.trim();
|
|
1129
1153
|
if (trimmed.length === 0) {
|
|
@@ -1254,6 +1278,9 @@ function formatDevcontainerProgressLine(line) {
|
|
|
1254
1278
|
if (!cleaned) {
|
|
1255
1279
|
return null;
|
|
1256
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
|
+
}
|
|
1257
1284
|
try {
|
|
1258
1285
|
const parsed = JSON.parse(cleaned);
|
|
1259
1286
|
const text = typeof parsed.text === "string" ? stripAnsi(parsed.text).trim() : "";
|
|
@@ -1297,8 +1324,21 @@ function formatDevcontainerProgressLine(line) {
|
|
|
1297
1324
|
function requiresSshAuthSockPermissionFix(sshAuthSockSource) {
|
|
1298
1325
|
return sshAuthSockSource === DOCKER_DESKTOP_SSH_AUTH_SOCK_SOURCE;
|
|
1299
1326
|
}
|
|
1300
|
-
function buildEnsureSshAuthSockAccessibleScript() {
|
|
1301
|
-
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
|
+
`);
|
|
1302
1342
|
}
|
|
1303
1343
|
function buildInteractiveShellScript() {
|
|
1304
1344
|
return [
|
|
@@ -1480,12 +1520,21 @@ async function copyKnownHosts(containerId) {
|
|
|
1480
1520
|
async function stopManagedSshd(containerId) {
|
|
1481
1521
|
await devcontainerExec(containerId, buildStopManagedSshdScript(), { quiet: true });
|
|
1482
1522
|
}
|
|
1483
|
-
async function ensureSshAuthSockAccessible(containerId) {
|
|
1484
|
-
|
|
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), {
|
|
1485
1529
|
quiet: true,
|
|
1486
1530
|
user: "root"
|
|
1487
1531
|
});
|
|
1488
1532
|
}
|
|
1533
|
+
async function assertConfiguredSshAuthSockAvailable(containerId) {
|
|
1534
|
+
await dockerExec(containerId, buildAssertConfiguredSshAuthSockScript(), {
|
|
1535
|
+
quiet: true
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1489
1538
|
async function restoreRunnerHostKeys(containerId, remoteWorkspaceFolder) {
|
|
1490
1539
|
await dockerExec(containerId, buildRestoreRunnerHostKeysScript(remoteWorkspaceFolder), {
|
|
1491
1540
|
quiet: true,
|
|
@@ -1757,6 +1806,13 @@ ${details}`;
|
|
|
1757
1806
|
function stripAnsi(text) {
|
|
1758
1807
|
return text.replace(/\u001B\[[0-9;]*m/g, "");
|
|
1759
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
|
+
}
|
|
1760
1816
|
function labelsForWorkspaceHash(workspaceHash) {
|
|
1761
1817
|
return {
|
|
1762
1818
|
[MANAGED_LABEL_KEY]: "true",
|
|
@@ -1837,7 +1893,10 @@ async function handleUpLike(command, workspacePath, state, explicitPort, allowMi
|
|
|
1837
1893
|
await ensurePathIgnored(workspacePath, path3.join(workspacePath, RUNNER_HOST_KEYS_DIRNAME));
|
|
1838
1894
|
if (requiresSshAuthSockPermissionFix(environment.sshAuthSock)) {
|
|
1839
1895
|
console.log("Making the forwarded SSH agent socket accessible to the container user...");
|
|
1840
|
-
await ensureSshAuthSockAccessible(upResult.containerId);
|
|
1896
|
+
await ensureSshAuthSockAccessible(upResult.containerId, environment.sshAuthSock);
|
|
1897
|
+
}
|
|
1898
|
+
if (environment.sshAuthSock) {
|
|
1899
|
+
await assertConfiguredSshAuthSockAvailable(upResult.containerId);
|
|
1841
1900
|
}
|
|
1842
1901
|
await copyKnownHosts(upResult.containerId);
|
|
1843
1902
|
await stopManagedSshd(upResult.containerId);
|
|
@@ -1875,6 +1934,7 @@ async function handleShell(workspacePath, state) {
|
|
|
1875
1934
|
containers,
|
|
1876
1935
|
preferredContainerId: state?.lastContainerId
|
|
1877
1936
|
});
|
|
1937
|
+
await assertConfiguredSshAuthSockAvailable(containerId);
|
|
1878
1938
|
console.log(`Opening shell inside ${containerId.slice(0, 12)}...`);
|
|
1879
1939
|
process.exitCode = await openInteractiveShell(containerId);
|
|
1880
1940
|
}
|