@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.
Files changed (3) hide show
  1. package/README.md +25 -10
  2. package/dist/devbox.js +227 -32
  3. 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
- # 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
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 5001
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 5001 --allow-missing-ssh
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} [port] [--allow-missing-ssh]
843
- ${CLI_NAME} up [port] [--allow-missing-ssh]
844
- ${CLI_NAME} rebuild [port] [--allow-missing-ssh]
845
- ${CLI_NAME} down
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
- - If no port is provided for up/rebuild, the last stored port for this workspace is reused.
851
- - Pass --allow-missing-ssh to continue without SSH agent sharing when no usable SSH agent socket is available.
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: "up", allowMissingSsh: false };
873
+ return { command: "help", allowMissingSsh: false };
858
874
  }
859
- let command = "up";
875
+ let command;
860
876
  const first = args[0];
861
- if (first === "up" || first === "down" || first === "rebuild" || first === "help") {
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
- throw new UserError(`No devcontainer definition was found in ${workspacePath}. Expected .devcontainer/devcontainer.json or .devcontainer.json.`);
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=${SSH_AUTH_SOCK_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 (options.sshAuthSock) {
1069
- containerEnv.SSH_AUTH_SOCK = SSH_AUTH_SOCK_TARGET;
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(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
+ `);
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
- 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), {
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 handleDown(workspacePath, state) {
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 {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pablozaiden/devbox",
3
- "version": "0.1.1",
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": {