@pablozaiden/devbox 0.1.0 → 0.1.2
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 +11 -1
- package/dist/devbox.js +153 -18
- 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.
|
|
@@ -54,15 +54,23 @@ devbox up 5001
|
|
|
54
54
|
# Continue even if SSH agent sharing is unavailable
|
|
55
55
|
devbox up 5001 --allow-missing-ssh
|
|
56
56
|
|
|
57
|
+
# Use a specific devcontainer under .devcontainer/services/api
|
|
58
|
+
devbox up 5001 --devcontainer-subpath services/api
|
|
59
|
+
|
|
57
60
|
# Rebuild/recreate the managed devcontainer
|
|
58
61
|
devbox rebuild 5001
|
|
59
62
|
|
|
63
|
+
# Open an interactive shell in the running managed devcontainer for this workspace
|
|
64
|
+
devbox shell
|
|
65
|
+
|
|
60
66
|
# Stop and remove the managed container while preserving the workspace-mounted SSH credentials
|
|
61
67
|
devbox down
|
|
62
68
|
```
|
|
63
69
|
|
|
64
70
|
If you omit the port for `up` or `rebuild`, `devbox` will reuse the last port stored for the current workspace.
|
|
65
71
|
|
|
72
|
+
`devbox shell` requires an already running managed container for the current workspace. If none is running, use `devbox up` first.
|
|
73
|
+
|
|
66
74
|
## Development
|
|
67
75
|
|
|
68
76
|
```bash
|
|
@@ -88,6 +96,8 @@ cd examples/smoke-workspace
|
|
|
88
96
|
## Notes
|
|
89
97
|
|
|
90
98
|
- The generated config is written next to the original devcontainer config, using the alternate accepted devcontainer filename so relative Dockerfile paths keep working.
|
|
99
|
+
- `--devcontainer-subpath services/api` tells `devbox` to use `.devcontainer/services/api/devcontainer.json`.
|
|
100
|
+
- `devbox shell` opens an interactive shell inside the running managed container for the current workspace.
|
|
91
101
|
- `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
102
|
- Re-running `devbox` after a host restart recreates the desired state: container up, port published, SSH runner started again.
|
|
93
103
|
- When Docker Desktop host services are available, `devbox` can share the SSH agent without relying on a host-shell `SSH_AUTH_SOCK`.
|
package/dist/devbox.js
CHANGED
|
@@ -839,16 +839,19 @@ 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}
|
|
842
|
+
${CLI_NAME} [port] [--allow-missing-ssh] [--devcontainer-subpath <subpath>]
|
|
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>]
|
|
846
847
|
${CLI_NAME} --help
|
|
847
848
|
|
|
848
849
|
Notes:
|
|
849
850
|
- The same port is published on host and container.
|
|
850
851
|
- If no port is provided for up/rebuild, the last stored port for this workspace is reused.
|
|
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.
|
|
854
|
+
- ${CLI_NAME} shell opens an interactive shell in the running managed container for this workspace.
|
|
852
855
|
- Only image/Dockerfile-based devcontainers are supported in v1.`;
|
|
853
856
|
}
|
|
854
857
|
function parseArgs(argv) {
|
|
@@ -858,7 +861,7 @@ function parseArgs(argv) {
|
|
|
858
861
|
}
|
|
859
862
|
let command = "up";
|
|
860
863
|
const first = args[0];
|
|
861
|
-
if (first === "up" || first === "down" || first === "rebuild" || first === "help") {
|
|
864
|
+
if (first === "up" || first === "down" || first === "rebuild" || first === "shell" || first === "help") {
|
|
862
865
|
command = first;
|
|
863
866
|
args.shift();
|
|
864
867
|
} else if (first === "--help" || first === "-h") {
|
|
@@ -866,6 +869,7 @@ function parseArgs(argv) {
|
|
|
866
869
|
}
|
|
867
870
|
let port;
|
|
868
871
|
let allowMissingSsh = false;
|
|
872
|
+
let devcontainerSubpath;
|
|
869
873
|
const positionals = [];
|
|
870
874
|
for (let index = 0;index < args.length; index += 1) {
|
|
871
875
|
const arg = args[index];
|
|
@@ -876,6 +880,19 @@ function parseArgs(argv) {
|
|
|
876
880
|
allowMissingSsh = true;
|
|
877
881
|
continue;
|
|
878
882
|
}
|
|
883
|
+
if (arg === "--devcontainer-subpath") {
|
|
884
|
+
const value = args[index + 1];
|
|
885
|
+
if (!value) {
|
|
886
|
+
throw new UserError("Expected a value after --devcontainer-subpath.");
|
|
887
|
+
}
|
|
888
|
+
devcontainerSubpath = parseDevcontainerSubpath(value);
|
|
889
|
+
index += 1;
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
if (arg.startsWith("--devcontainer-subpath=")) {
|
|
893
|
+
devcontainerSubpath = parseDevcontainerSubpath(arg.slice("--devcontainer-subpath=".length));
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
879
896
|
if (arg === "--port" || arg === "-p") {
|
|
880
897
|
const value = args[index + 1];
|
|
881
898
|
if (!value) {
|
|
@@ -901,11 +918,23 @@ function parseArgs(argv) {
|
|
|
901
918
|
if (command === "down") {
|
|
902
919
|
throw new UserError("The down command does not accept a port.");
|
|
903
920
|
}
|
|
921
|
+
if (command === "shell") {
|
|
922
|
+
throw new UserError("The shell command does not accept a port.");
|
|
923
|
+
}
|
|
904
924
|
port = parsePort(positionals[0]);
|
|
905
925
|
}
|
|
906
926
|
if (command === "down" && port !== undefined) {
|
|
907
927
|
throw new UserError("The down command does not accept a port.");
|
|
908
928
|
}
|
|
929
|
+
if (command === "shell" && port !== undefined) {
|
|
930
|
+
throw new UserError("The shell command does not accept a port.");
|
|
931
|
+
}
|
|
932
|
+
if (command === "shell" && devcontainerSubpath !== undefined) {
|
|
933
|
+
throw new UserError("The shell command does not accept --devcontainer-subpath.");
|
|
934
|
+
}
|
|
935
|
+
if (devcontainerSubpath) {
|
|
936
|
+
return { command, port, allowMissingSsh, devcontainerSubpath };
|
|
937
|
+
}
|
|
909
938
|
return { command, port, allowMissingSsh };
|
|
910
939
|
}
|
|
911
940
|
function parsePort(raw) {
|
|
@@ -977,7 +1006,7 @@ async function deleteWorkspaceState(workspacePath) {
|
|
|
977
1006
|
await rm(getWorkspaceStateDir(workspacePath), { recursive: true, force: true });
|
|
978
1007
|
}
|
|
979
1008
|
function resolvePort(command, explicitPort, state) {
|
|
980
|
-
if (command === "down" || command === "help") {
|
|
1009
|
+
if (command === "down" || command === "shell" || command === "help") {
|
|
981
1010
|
throw new UserError(`resolvePort cannot be used for ${command}.`);
|
|
982
1011
|
}
|
|
983
1012
|
if (explicitPort !== undefined) {
|
|
@@ -988,11 +1017,8 @@ function resolvePort(command, explicitPort, state) {
|
|
|
988
1017
|
}
|
|
989
1018
|
throw new UserError(`No port was provided and no previous port is stored for this workspace. Run \`${CLI_NAME} <port>\` first.`);
|
|
990
1019
|
}
|
|
991
|
-
async function discoverDevcontainerConfig(workspacePath) {
|
|
992
|
-
const candidates =
|
|
993
|
-
path.join(workspacePath, ".devcontainer", "devcontainer.json"),
|
|
994
|
-
path.join(workspacePath, ".devcontainer.json")
|
|
995
|
-
];
|
|
1020
|
+
async function discoverDevcontainerConfig(workspacePath, devcontainerSubpath) {
|
|
1021
|
+
const candidates = getDevcontainerCandidates(workspacePath, devcontainerSubpath);
|
|
996
1022
|
for (const candidate of candidates) {
|
|
997
1023
|
if (!existsSync(candidate)) {
|
|
998
1024
|
continue;
|
|
@@ -1014,7 +1040,8 @@ async function discoverDevcontainerConfig(workspacePath) {
|
|
|
1014
1040
|
validateSupportedDevcontainerConfig(config);
|
|
1015
1041
|
return { path: candidate, config };
|
|
1016
1042
|
}
|
|
1017
|
-
|
|
1043
|
+
const expectedLocations = devcontainerSubpath ? `.devcontainer/${formatDevcontainerSubpath(devcontainerSubpath)}/devcontainer.json` : ".devcontainer/devcontainer.json or .devcontainer.json";
|
|
1044
|
+
throw new UserError(`No devcontainer definition was found in ${workspacePath}. Expected ${expectedLocations}.`);
|
|
1018
1045
|
}
|
|
1019
1046
|
function validateSupportedDevcontainerConfig(config) {
|
|
1020
1047
|
if (config.dockerComposeFile !== undefined) {
|
|
@@ -1097,6 +1124,32 @@ async function getKnownHostsPath() {
|
|
|
1097
1124
|
function quoteShell(value) {
|
|
1098
1125
|
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
1099
1126
|
}
|
|
1127
|
+
function parseDevcontainerSubpath(raw) {
|
|
1128
|
+
const trimmed = raw.trim();
|
|
1129
|
+
if (trimmed.length === 0) {
|
|
1130
|
+
throw new UserError("Devcontainer subpath cannot be empty.");
|
|
1131
|
+
}
|
|
1132
|
+
if (path.isAbsolute(trimmed) || trimmed.startsWith("\\")) {
|
|
1133
|
+
throw new UserError(`Devcontainer subpath must stay inside .devcontainer. Received: ${raw}`);
|
|
1134
|
+
}
|
|
1135
|
+
const segments = trimmed.split(/[\\/]+/u).filter((segment) => segment.length > 0);
|
|
1136
|
+
if (segments.length === 0 || segments.some((segment) => segment === "." || segment === "..")) {
|
|
1137
|
+
throw new UserError(`Devcontainer subpath must stay inside .devcontainer. Received: ${raw}`);
|
|
1138
|
+
}
|
|
1139
|
+
return path.join(...segments);
|
|
1140
|
+
}
|
|
1141
|
+
function getDevcontainerCandidates(workspacePath, devcontainerSubpath) {
|
|
1142
|
+
if (devcontainerSubpath) {
|
|
1143
|
+
return [path.join(workspacePath, ".devcontainer", devcontainerSubpath, "devcontainer.json")];
|
|
1144
|
+
}
|
|
1145
|
+
return [
|
|
1146
|
+
path.join(workspacePath, ".devcontainer", "devcontainer.json"),
|
|
1147
|
+
path.join(workspacePath, ".devcontainer.json")
|
|
1148
|
+
];
|
|
1149
|
+
}
|
|
1150
|
+
function formatDevcontainerSubpath(subpath) {
|
|
1151
|
+
return subpath.split(path.sep).join("/");
|
|
1152
|
+
}
|
|
1100
1153
|
function dedupe(values) {
|
|
1101
1154
|
return [...new Set(values)];
|
|
1102
1155
|
}
|
|
@@ -1247,6 +1300,15 @@ function requiresSshAuthSockPermissionFix(sshAuthSockSource) {
|
|
|
1247
1300
|
function buildEnsureSshAuthSockAccessibleScript() {
|
|
1248
1301
|
return `if [ -S ${quoteShell(SSH_AUTH_SOCK_TARGET)} ]; then chmod 666 ${quoteShell(SSH_AUTH_SOCK_TARGET)}; fi`;
|
|
1249
1302
|
}
|
|
1303
|
+
function buildInteractiveShellScript() {
|
|
1304
|
+
return [
|
|
1305
|
+
"if command -v bash >/dev/null 2>&1; then",
|
|
1306
|
+
" exec bash -l",
|
|
1307
|
+
"fi",
|
|
1308
|
+
"exec sh"
|
|
1309
|
+
].join(`
|
|
1310
|
+
`);
|
|
1311
|
+
}
|
|
1250
1312
|
function getRunnerHostKeysDir(remoteWorkspaceFolder) {
|
|
1251
1313
|
const trimmed = remoteWorkspaceFolder.endsWith("/") ? remoteWorkspaceFolder.slice(0, -1) : remoteWorkspaceFolder;
|
|
1252
1314
|
return `${trimmed}/${RUNNER_HOST_KEYS_DIRNAME}`;
|
|
@@ -1453,6 +1515,39 @@ async function persistRunnerHostKeys(containerId, remoteWorkspaceFolder) {
|
|
|
1453
1515
|
user: "root"
|
|
1454
1516
|
});
|
|
1455
1517
|
}
|
|
1518
|
+
function resolveShellContainerId(input) {
|
|
1519
|
+
const running = input.containers.filter((container) => container.State?.Running);
|
|
1520
|
+
if (input.preferredContainerId) {
|
|
1521
|
+
const preferred = running.find((container) => container.Id === input.preferredContainerId);
|
|
1522
|
+
if (preferred) {
|
|
1523
|
+
return preferred.Id;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
if (running.length === 1) {
|
|
1527
|
+
return running[0].Id;
|
|
1528
|
+
}
|
|
1529
|
+
if (running.length === 0) {
|
|
1530
|
+
throw new UserError("No running managed container was found for this workspace. Run `devbox up` first.");
|
|
1531
|
+
}
|
|
1532
|
+
throw new UserError("More than one managed container is running for this workspace. Run `devbox down` first.");
|
|
1533
|
+
}
|
|
1534
|
+
function buildDevcontainerShellCommand(containerId, terminalSize) {
|
|
1535
|
+
const args = ["devcontainer", "exec", "--container-id", containerId];
|
|
1536
|
+
if (terminalSize?.columns && terminalSize.columns > 0) {
|
|
1537
|
+
args.push("--terminal-columns", String(terminalSize.columns));
|
|
1538
|
+
}
|
|
1539
|
+
if (terminalSize?.rows && terminalSize.rows > 0) {
|
|
1540
|
+
args.push("--terminal-rows", String(terminalSize.rows));
|
|
1541
|
+
}
|
|
1542
|
+
args.push("sh", "-lc", buildInteractiveShellScript());
|
|
1543
|
+
return args;
|
|
1544
|
+
}
|
|
1545
|
+
async function openInteractiveShell(containerId) {
|
|
1546
|
+
return executeInteractive(buildDevcontainerShellCommand(containerId, {
|
|
1547
|
+
columns: process.stdout.isTTY ? process.stdout.columns : undefined,
|
|
1548
|
+
rows: process.stdout.isTTY ? process.stdout.rows : undefined
|
|
1549
|
+
}));
|
|
1550
|
+
}
|
|
1456
1551
|
async function hasDockerDesktopHostService() {
|
|
1457
1552
|
const result = await execute(["docker", "info", "--format", "{{.OperatingSystem}}"], {
|
|
1458
1553
|
stdoutMode: "capture",
|
|
@@ -1542,6 +1637,25 @@ async function execute(command, options) {
|
|
|
1542
1637
|
}
|
|
1543
1638
|
return result;
|
|
1544
1639
|
}
|
|
1640
|
+
async function executeInteractive(command, options) {
|
|
1641
|
+
const env = { ...process.env, ...options?.env ?? {} };
|
|
1642
|
+
for (const [key, value] of Object.entries(env)) {
|
|
1643
|
+
if (value === undefined) {
|
|
1644
|
+
delete env[key];
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
const subprocess = spawn(command[0], command.slice(1), {
|
|
1648
|
+
cwd: options?.cwd,
|
|
1649
|
+
env,
|
|
1650
|
+
stdio: "inherit"
|
|
1651
|
+
});
|
|
1652
|
+
return new Promise((resolve, reject) => {
|
|
1653
|
+
subprocess.once("error", reject);
|
|
1654
|
+
subprocess.once("close", (exitCode) => {
|
|
1655
|
+
resolve(exitCode ?? 0);
|
|
1656
|
+
});
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1545
1659
|
async function consumeStream(stream, mode, useStderr) {
|
|
1546
1660
|
if (!stream) {
|
|
1547
1661
|
return "";
|
|
@@ -1659,17 +1773,21 @@ async function main() {
|
|
|
1659
1773
|
}
|
|
1660
1774
|
const workspacePath = await realpath(process.cwd());
|
|
1661
1775
|
const state = await loadWorkspaceState(workspacePath);
|
|
1776
|
+
if (parsed.command === "shell") {
|
|
1777
|
+
await handleShell(workspacePath, state);
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1662
1780
|
if (parsed.command === "down") {
|
|
1663
|
-
await handleDown(workspacePath, state);
|
|
1781
|
+
await handleDown(workspacePath, state, parsed.devcontainerSubpath);
|
|
1664
1782
|
return;
|
|
1665
1783
|
}
|
|
1666
|
-
await handleUpLike(parsed.command, workspacePath, state, parsed.port, parsed.allowMissingSsh);
|
|
1784
|
+
await handleUpLike(parsed.command, workspacePath, state, parsed.port, parsed.allowMissingSsh, parsed.devcontainerSubpath);
|
|
1667
1785
|
}
|
|
1668
|
-
async function handleUpLike(command, workspacePath, state, explicitPort, allowMissingSsh) {
|
|
1786
|
+
async function handleUpLike(command, workspacePath, state, explicitPort, allowMissingSsh, devcontainerSubpath) {
|
|
1669
1787
|
const environment = await ensureHostEnvironment({ allowMissingSsh });
|
|
1670
1788
|
const port = resolvePort(command, explicitPort, state);
|
|
1671
1789
|
const knownHostsPath = await getKnownHostsPath();
|
|
1672
|
-
const discovered = await discoverDevcontainerConfig(workspacePath);
|
|
1790
|
+
const discovered = await discoverDevcontainerConfig(workspacePath, devcontainerSubpath);
|
|
1673
1791
|
const generatedConfigPath = getGeneratedConfigPath(discovered.path);
|
|
1674
1792
|
const legacyGeneratedConfigPath = getLegacyGeneratedConfigPath(discovered.path);
|
|
1675
1793
|
const workspaceHash = hashWorkspacePath(workspacePath);
|
|
@@ -1743,7 +1861,24 @@ Ready. ${upResult.containerId.slice(0, 12)} is available on port ${port}.`);
|
|
|
1743
1861
|
console.log("Host known_hosts was not found, so only SSH agent sharing was configured.");
|
|
1744
1862
|
}
|
|
1745
1863
|
}
|
|
1746
|
-
async function
|
|
1864
|
+
async function handleShell(workspacePath, state) {
|
|
1865
|
+
if (!isExecutableAvailable("docker")) {
|
|
1866
|
+
throw new UserError("Docker is required but was not found in PATH.");
|
|
1867
|
+
}
|
|
1868
|
+
if (!isExecutableAvailable("devcontainer")) {
|
|
1869
|
+
throw new UserError("Dev Container CLI is required but was not found in PATH.");
|
|
1870
|
+
}
|
|
1871
|
+
const labels = labelsForWorkspaceHash(hashWorkspacePath(workspacePath));
|
|
1872
|
+
const containerIds = await listManagedContainers(labels);
|
|
1873
|
+
const containers = await inspectContainers(containerIds);
|
|
1874
|
+
const containerId = resolveShellContainerId({
|
|
1875
|
+
containers,
|
|
1876
|
+
preferredContainerId: state?.lastContainerId
|
|
1877
|
+
});
|
|
1878
|
+
console.log(`Opening shell inside ${containerId.slice(0, 12)}...`);
|
|
1879
|
+
process.exitCode = await openInteractiveShell(containerId);
|
|
1880
|
+
}
|
|
1881
|
+
async function handleDown(workspacePath, state, devcontainerSubpath) {
|
|
1747
1882
|
if (!isExecutableAvailable("docker")) {
|
|
1748
1883
|
throw new UserError("Docker is required but was not found in PATH.");
|
|
1749
1884
|
}
|
|
@@ -1755,7 +1890,7 @@ async function handleDown(workspacePath, state) {
|
|
|
1755
1890
|
generatedConfigPaths.add(state.generatedConfigPath);
|
|
1756
1891
|
}
|
|
1757
1892
|
try {
|
|
1758
|
-
const discovered = await discoverDevcontainerConfig(workspacePath);
|
|
1893
|
+
const discovered = await discoverDevcontainerConfig(workspacePath, devcontainerSubpath);
|
|
1759
1894
|
generatedConfigPaths.add(getGeneratedConfigPath(discovered.path));
|
|
1760
1895
|
generatedConfigPaths.add(getLegacyGeneratedConfigPath(discovered.path));
|
|
1761
1896
|
} catch {}
|