@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.
Files changed (3) hide show
  1. package/README.md +15 -10
  2. package/dist/devbox.js +77 -17
  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() : "";
@@ -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(SSH_AUTH_SOCK_TARGET)} ]; then chmod 666 ${quoteShell(SSH_AUTH_SOCK_TARGET)}; fi`;
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
- await dockerExec(containerId, buildEnsureSshAuthSockAccessibleScript(), {
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pablozaiden/devbox",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
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": {