@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.
Files changed (3) hide show
  1. package/README.md +15 -10
  2. package/dist/devbox.js +106 -23
  3. 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
- # Start or reuse the devcontainer on port 5001
49
- devbox 5001
50
+ # Show CLI help
51
+ devbox
50
52
 
51
- # Same as above
52
- devbox up 5001
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 5001 --allow-missing-ssh
57
+ devbox up <port> --allow-missing-ssh
56
58
 
57
59
  # Use a specific devcontainer under .devcontainer/services/api
58
- devbox up 5001 --devcontainer-subpath services/api
60
+ devbox up <port> --devcontainer-subpath services/api
59
61
 
60
62
  # Rebuild/recreate the managed devcontainer
61
- devbox rebuild 5001
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 5001 --allow-missing-ssh
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} [port] [--allow-missing-ssh] [--devcontainer-subpath <subpath>]
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
- - If no port is provided for up/rebuild, the last stored port for this workspace is reused.
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: "up", allowMissingSsh: false };
873
+ return { command: "help", allowMissingSsh: false };
861
874
  }
862
- let command = "up";
875
+ let command;
863
876
  const first = args[0];
864
- if (first === "up" || first === "down" || first === "rebuild" || first === "shell" || first === "help") {
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=${SSH_AUTH_SOCK_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 (options.sshAuthSock) {
1096
- containerEnv.SSH_AUTH_SOCK = SSH_AUTH_SOCK_TARGET;
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 === "No user features to update" || text === "Inspecting container" || text.startsWith("Run: ") || text.startsWith("Run in container: ") || text.startsWith("userEnvProbe") || text.startsWith("LifecycleCommandExecutionMap:") || text.startsWith("Exit code ")) {
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(SSH_AUTH_SOCK_TARGET)} ]; then chmod 666 ${quoteShell(SSH_AUTH_SOCK_TARGET)}; fi`;
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
- await dockerExec(containerId, buildEnsureSshAuthSockAccessibleScript(), {
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pablozaiden/devbox",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "description": "CLI to run and expose a devcontainer with SSH agent sharing and a forwarded ssh-server-runner port.",
6
6
  "repository": {