@pablozaiden/devbox 0.1.2 → 0.1.4
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 +106 -23
- 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() : "";
|
|
@@ -1278,12 +1305,22 @@ function formatDevcontainerProgressLine(line) {
|
|
|
1278
1305
|
return null;
|
|
1279
1306
|
}
|
|
1280
1307
|
if (type === "raw" && text === "Container started") {
|
|
1281
|
-
return "Container started.";
|
|
1308
|
+
return "Container started. Finishing devcontainer setup...";
|
|
1282
1309
|
}
|
|
1283
1310
|
if (text.startsWith("workspace root: ")) {
|
|
1284
1311
|
return `Workspace: ${text.slice("workspace root: ".length)}`;
|
|
1285
1312
|
}
|
|
1286
|
-
if (text === "
|
|
1313
|
+
if (text === "Inspecting container") {
|
|
1314
|
+
return "Inspecting container...";
|
|
1315
|
+
}
|
|
1316
|
+
if (text.startsWith("userEnvProbe")) {
|
|
1317
|
+
return "Checking container environment...";
|
|
1318
|
+
}
|
|
1319
|
+
const lifecycleProgress = formatDevcontainerLifecycleProgress(text);
|
|
1320
|
+
if (lifecycleProgress) {
|
|
1321
|
+
return lifecycleProgress;
|
|
1322
|
+
}
|
|
1323
|
+
if (text === "No user features to update" || text.startsWith("Run: ") || text.startsWith("Run in container: ") || text.startsWith("Exit code ")) {
|
|
1287
1324
|
return null;
|
|
1288
1325
|
}
|
|
1289
1326
|
if (level !== undefined && level >= 2) {
|
|
@@ -1297,8 +1334,21 @@ function formatDevcontainerProgressLine(line) {
|
|
|
1297
1334
|
function requiresSshAuthSockPermissionFix(sshAuthSockSource) {
|
|
1298
1335
|
return sshAuthSockSource === DOCKER_DESKTOP_SSH_AUTH_SOCK_SOURCE;
|
|
1299
1336
|
}
|
|
1300
|
-
function buildEnsureSshAuthSockAccessibleScript() {
|
|
1301
|
-
return `if [ -S ${quoteShell(
|
|
1337
|
+
function buildEnsureSshAuthSockAccessibleScript(containerSshAuthSock) {
|
|
1338
|
+
return `if [ -S ${quoteShell(containerSshAuthSock)} ]; then chmod 666 ${quoteShell(containerSshAuthSock)}; fi`;
|
|
1339
|
+
}
|
|
1340
|
+
function buildAssertConfiguredSshAuthSockScript() {
|
|
1341
|
+
return [
|
|
1342
|
+
'if [ -z "${SSH_AUTH_SOCK:-}" ]; then',
|
|
1343
|
+
" exit 0",
|
|
1344
|
+
"fi",
|
|
1345
|
+
'if [ -S "$SSH_AUTH_SOCK" ]; then',
|
|
1346
|
+
" exit 0",
|
|
1347
|
+
"fi",
|
|
1348
|
+
`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`,
|
|
1349
|
+
"exit 1"
|
|
1350
|
+
].join(`
|
|
1351
|
+
`);
|
|
1302
1352
|
}
|
|
1303
1353
|
function buildInteractiveShellScript() {
|
|
1304
1354
|
return [
|
|
@@ -1480,12 +1530,21 @@ async function copyKnownHosts(containerId) {
|
|
|
1480
1530
|
async function stopManagedSshd(containerId) {
|
|
1481
1531
|
await devcontainerExec(containerId, buildStopManagedSshdScript(), { quiet: true });
|
|
1482
1532
|
}
|
|
1483
|
-
async function ensureSshAuthSockAccessible(containerId) {
|
|
1484
|
-
|
|
1533
|
+
async function ensureSshAuthSockAccessible(containerId, sshAuthSockSource) {
|
|
1534
|
+
const containerSshAuthSock = getContainerSshAuthSockPath(sshAuthSockSource);
|
|
1535
|
+
if (!containerSshAuthSock) {
|
|
1536
|
+
throw new UserError("Cannot adjust SSH agent socket permissions when SSH sharing is disabled.");
|
|
1537
|
+
}
|
|
1538
|
+
await dockerExec(containerId, buildEnsureSshAuthSockAccessibleScript(containerSshAuthSock), {
|
|
1485
1539
|
quiet: true,
|
|
1486
1540
|
user: "root"
|
|
1487
1541
|
});
|
|
1488
1542
|
}
|
|
1543
|
+
async function assertConfiguredSshAuthSockAvailable(containerId) {
|
|
1544
|
+
await dockerExec(containerId, buildAssertConfiguredSshAuthSockScript(), {
|
|
1545
|
+
quiet: true
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1489
1548
|
async function restoreRunnerHostKeys(containerId, remoteWorkspaceFolder) {
|
|
1490
1549
|
await dockerExec(containerId, buildRestoreRunnerHostKeysScript(remoteWorkspaceFolder), {
|
|
1491
1550
|
quiet: true,
|
|
@@ -1664,6 +1723,7 @@ async function consumeStream(stream, mode, useStderr) {
|
|
|
1664
1723
|
stream.setEncoding("utf8");
|
|
1665
1724
|
let captured = "";
|
|
1666
1725
|
let buffered = "";
|
|
1726
|
+
let lastRenderedProgressLine = null;
|
|
1667
1727
|
for await (const chunk of stream) {
|
|
1668
1728
|
const text = typeof chunk === "string" ? chunk : String(chunk);
|
|
1669
1729
|
captured += text;
|
|
@@ -1680,13 +1740,13 @@ async function consumeStream(stream, mode, useStderr) {
|
|
|
1680
1740
|
while (newlineIndex >= 0) {
|
|
1681
1741
|
const line = buffered.slice(0, newlineIndex);
|
|
1682
1742
|
buffered = buffered.slice(newlineIndex + 1);
|
|
1683
|
-
renderDevcontainerJsonLine(line, writer);
|
|
1743
|
+
lastRenderedProgressLine = renderDevcontainerJsonLine(line, writer, lastRenderedProgressLine);
|
|
1684
1744
|
newlineIndex = buffered.indexOf(`
|
|
1685
1745
|
`);
|
|
1686
1746
|
}
|
|
1687
1747
|
}
|
|
1688
1748
|
if (mode === "devcontainer-json" && buffered.length > 0) {
|
|
1689
|
-
renderDevcontainerJsonLine(buffered, writer);
|
|
1749
|
+
renderDevcontainerJsonLine(buffered, writer, lastRenderedProgressLine);
|
|
1690
1750
|
}
|
|
1691
1751
|
return captured;
|
|
1692
1752
|
}
|
|
@@ -1722,12 +1782,14 @@ function isExecutablePath(candidate) {
|
|
|
1722
1782
|
return false;
|
|
1723
1783
|
}
|
|
1724
1784
|
}
|
|
1725
|
-
function renderDevcontainerJsonLine(line, writer) {
|
|
1785
|
+
function renderDevcontainerJsonLine(line, writer, previousLine) {
|
|
1726
1786
|
const formatted = formatDevcontainerProgressLine(line);
|
|
1727
|
-
if (formatted) {
|
|
1787
|
+
if (formatted && formatted !== previousLine) {
|
|
1728
1788
|
writer.write(`${formatted}
|
|
1729
1789
|
`);
|
|
1790
|
+
return formatted;
|
|
1730
1791
|
}
|
|
1792
|
+
return previousLine;
|
|
1731
1793
|
}
|
|
1732
1794
|
function parseDevcontainerOutcome(stdout) {
|
|
1733
1795
|
const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
@@ -1757,6 +1819,23 @@ ${details}`;
|
|
|
1757
1819
|
function stripAnsi(text) {
|
|
1758
1820
|
return text.replace(/\u001B\[[0-9;]*m/g, "");
|
|
1759
1821
|
}
|
|
1822
|
+
function formatDevcontainerLifecycleProgress(text) {
|
|
1823
|
+
if (!text.startsWith("LifecycleCommandExecutionMap:")) {
|
|
1824
|
+
return null;
|
|
1825
|
+
}
|
|
1826
|
+
const commandMatch = text.match(/\b(initializeCommand|onCreateCommand|updateContentCommand|postCreateCommand|postStartCommand|postAttachCommand)\b/);
|
|
1827
|
+
if (commandMatch) {
|
|
1828
|
+
return `Running ${commandMatch[1]}...`;
|
|
1829
|
+
}
|
|
1830
|
+
return "Running devcontainer lifecycle commands...";
|
|
1831
|
+
}
|
|
1832
|
+
function looksLikeDevcontainerUserEnvProbeDump(text) {
|
|
1833
|
+
const match = text.match(/^([0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})([\s\S]*)\1$/i);
|
|
1834
|
+
if (!match) {
|
|
1835
|
+
return false;
|
|
1836
|
+
}
|
|
1837
|
+
return /\b(?:HOME|HOSTNAME|PATH|PWD|SHLVL|SSH_AUTH_SOCK|USER)=/.test(match[2]);
|
|
1838
|
+
}
|
|
1760
1839
|
function labelsForWorkspaceHash(workspaceHash) {
|
|
1761
1840
|
return {
|
|
1762
1841
|
[MANAGED_LABEL_KEY]: "true",
|
|
@@ -1837,7 +1916,10 @@ async function handleUpLike(command, workspacePath, state, explicitPort, allowMi
|
|
|
1837
1916
|
await ensurePathIgnored(workspacePath, path3.join(workspacePath, RUNNER_HOST_KEYS_DIRNAME));
|
|
1838
1917
|
if (requiresSshAuthSockPermissionFix(environment.sshAuthSock)) {
|
|
1839
1918
|
console.log("Making the forwarded SSH agent socket accessible to the container user...");
|
|
1840
|
-
await ensureSshAuthSockAccessible(upResult.containerId);
|
|
1919
|
+
await ensureSshAuthSockAccessible(upResult.containerId, environment.sshAuthSock);
|
|
1920
|
+
}
|
|
1921
|
+
if (environment.sshAuthSock) {
|
|
1922
|
+
await assertConfiguredSshAuthSockAvailable(upResult.containerId);
|
|
1841
1923
|
}
|
|
1842
1924
|
await copyKnownHosts(upResult.containerId);
|
|
1843
1925
|
await stopManagedSshd(upResult.containerId);
|
|
@@ -1875,6 +1957,7 @@ async function handleShell(workspacePath, state) {
|
|
|
1875
1957
|
containers,
|
|
1876
1958
|
preferredContainerId: state?.lastContainerId
|
|
1877
1959
|
});
|
|
1960
|
+
await assertConfiguredSshAuthSockAvailable(containerId);
|
|
1878
1961
|
console.log(`Opening shell inside ${containerId.slice(0, 12)}...`);
|
|
1879
1962
|
process.exitCode = await openInteractiveShell(containerId);
|
|
1880
1963
|
}
|