@ycs77/devbox 0.4.0 → 0.5.0
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/dist/cli.mjs +117 -30
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -209,7 +209,8 @@ function defaultGlobalConfiguration(catalog = PACKAGED_RUNTIME_CATALOG) {
|
|
|
209
209
|
function defaultLocalConfiguration(globalConfiguration) {
|
|
210
210
|
return {
|
|
211
211
|
version: 1,
|
|
212
|
-
node: globalConfiguration.node[0] ?? null
|
|
212
|
+
node: globalConfiguration.node[0] ?? null,
|
|
213
|
+
ports: ["APP_PORT:5173:5173"]
|
|
213
214
|
};
|
|
214
215
|
}
|
|
215
216
|
function parseGlobalConfiguration(source, catalog = PACKAGED_RUNTIME_CATALOG) {
|
|
@@ -249,15 +250,22 @@ function parseLocalConfiguration(source, globalConfiguration, catalog = PACKAGED
|
|
|
249
250
|
const parsed = parseYaml(source);
|
|
250
251
|
if (!parsed.ok) return invalid$1("invalid-local-configuration", parsed.observed);
|
|
251
252
|
const document = parsed.value;
|
|
252
|
-
if (!isRecord(document) || !hasExactKeys$1(document, ["version", "node"])
|
|
253
|
+
if (!isRecord(document) || !hasExactKeys$1(document, ["version", "node"]) && !hasExactKeys$1(document, [
|
|
254
|
+
"version",
|
|
255
|
+
"node",
|
|
256
|
+
"ports"
|
|
257
|
+
])) return invalid$1("invalid-local-configuration", "Local configuration contains unknown or missing fields.");
|
|
253
258
|
if (document.version !== 1) return invalid$1("invalid-local-configuration", "Local configuration must use version: 1.");
|
|
254
259
|
const node = runtimeLineOrNull(document.node);
|
|
255
260
|
if (node === void 0) return invalid$1("invalid-local-configuration", `Node Runtime is invalid: ${String(document.node)}.`);
|
|
256
261
|
if (node !== null && !catalog.runtimes.node?.includes(node)) return invalid$1("invalid-local-configuration", `Node Runtime is not in the packaged catalog: ${String(document.node)}.`);
|
|
257
262
|
if (node !== null && !globalConfiguration.node.includes(node)) return invalid$1("unconfigured-runtime-selection", `Local configuration selects a Node Runtime that is not configured globally: ${node}.`);
|
|
263
|
+
const ports = document.ports === void 0 ? success([]) : normalizePortMappings(document.ports);
|
|
264
|
+
if (!ports.ok) return ports;
|
|
258
265
|
return success({
|
|
259
266
|
version: 1,
|
|
260
|
-
node
|
|
267
|
+
node,
|
|
268
|
+
ports: ports.value
|
|
261
269
|
});
|
|
262
270
|
}
|
|
263
271
|
function serializeGlobalConfiguration(configuration) {
|
|
@@ -271,12 +279,31 @@ function serializeGlobalConfiguration(configuration) {
|
|
|
271
279
|
function serializeLocalConfiguration(configuration) {
|
|
272
280
|
return stringify({
|
|
273
281
|
version: 1,
|
|
274
|
-
node: configuration.node
|
|
282
|
+
node: configuration.node,
|
|
283
|
+
ports: [...configuration.ports]
|
|
275
284
|
});
|
|
276
285
|
}
|
|
277
286
|
function configurationsEqual(left, right) {
|
|
278
287
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
279
288
|
}
|
|
289
|
+
function renderComposePortMapping(mapping) {
|
|
290
|
+
return parsePortMapping(mapping)?.compose ?? mapping;
|
|
291
|
+
}
|
|
292
|
+
function normalizePortMappings(values) {
|
|
293
|
+
if (!Array.isArray(values) || !values.every((value) => typeof value === "string")) return invalid$1("invalid-local-configuration", "Ports must be a list of port mappings.");
|
|
294
|
+
const mappings = [];
|
|
295
|
+
const hostPorts = /* @__PURE__ */ new Set();
|
|
296
|
+
for (const input of values) {
|
|
297
|
+
const mapping = input.trim();
|
|
298
|
+
if (mapping.length === 0) continue;
|
|
299
|
+
const parsed = parsePortMapping(mapping);
|
|
300
|
+
if (parsed === void 0) return invalid$1("invalid-local-configuration", `Port mapping is invalid: ${mapping}.`);
|
|
301
|
+
if (hostPorts.has(parsed.hostPort)) return invalid$1("invalid-local-configuration", `Host port is configured more than once: ${parsed.hostPort}.`);
|
|
302
|
+
hostPorts.add(parsed.hostPort);
|
|
303
|
+
mappings.push(mapping);
|
|
304
|
+
}
|
|
305
|
+
return success(mappings);
|
|
306
|
+
}
|
|
280
307
|
function parseYaml(source) {
|
|
281
308
|
try {
|
|
282
309
|
const document = parseDocument(source, {
|
|
@@ -307,6 +334,22 @@ function runtimeLineOrNull(value) {
|
|
|
307
334
|
if (value === null) return null;
|
|
308
335
|
return runtimeLine(value);
|
|
309
336
|
}
|
|
337
|
+
function parsePortMapping(mapping) {
|
|
338
|
+
const literal = /^(\d+):(\d+)$/.exec(mapping);
|
|
339
|
+
if (literal !== null) return validPort(literal[1]) && validPort(literal[2]) ? {
|
|
340
|
+
hostPort: String(Number(literal[1])),
|
|
341
|
+
compose: mapping
|
|
342
|
+
} : void 0;
|
|
343
|
+
const environment = /^([A-Za-z_][A-Za-z0-9_]*):(\d+):(\d+)$/.exec(mapping);
|
|
344
|
+
if (environment !== null) return validPort(environment[2]) && validPort(environment[3]) ? {
|
|
345
|
+
hostPort: String(Number(environment[2])),
|
|
346
|
+
compose: `\${${environment[1]}:-${environment[2]}}:${environment[3]}`
|
|
347
|
+
} : void 0;
|
|
348
|
+
}
|
|
349
|
+
function validPort(value) {
|
|
350
|
+
const port = Number(value);
|
|
351
|
+
return Number.isInteger(port) && port >= 1 && port <= 65535;
|
|
352
|
+
}
|
|
310
353
|
function isRecord(value) {
|
|
311
354
|
if (typeof value !== "object" || value === null) return false;
|
|
312
355
|
const prototype = Object.getPrototypeOf(value);
|
|
@@ -501,8 +544,10 @@ function renderProjectCompose(input) {
|
|
|
501
544
|
},
|
|
502
545
|
services: { devbox: {
|
|
503
546
|
image: WORKSPACE_IMAGE,
|
|
547
|
+
hostname: "devbox",
|
|
504
548
|
container_name: `devbox-${input.sandboxName}`,
|
|
505
549
|
working_dir: `/workspace/${input.sandboxName}`,
|
|
550
|
+
...input.ports.length === 0 ? {} : { ports: input.ports.map((port) => renderComposePortMapping(port)) },
|
|
506
551
|
...Object.keys(environment).length === 0 ? {} : { environment },
|
|
507
552
|
volumes
|
|
508
553
|
} },
|
|
@@ -527,7 +572,8 @@ function validatesProjectCompose(source, input) {
|
|
|
527
572
|
if (metadata.version !== 1 || metadata.project_root !== input.projectRoot || metadata.compose_name !== input.sandboxName || metadata.sandbox_service !== "devbox") return false;
|
|
528
573
|
const service = definition.services.devbox;
|
|
529
574
|
if (service.image !== "devbox-workspace:latest" || service.container_name !== `devbox-${input.sandboxName}` || service.working_dir !== `/workspace/${input.sandboxName}` || "build" in service) return false;
|
|
530
|
-
|
|
575
|
+
const ports = input.ports.map((port) => renderComposePortMapping(port));
|
|
576
|
+
return JSON.stringify(service.ports ?? []) === JSON.stringify(ports) && (input.selectedNode === null && service.environment?.NODE_VERSION === void 0 || input.selectedNode !== null && service.environment?.NODE_VERSION === input.selectedNode);
|
|
531
577
|
} catch {
|
|
532
578
|
return false;
|
|
533
579
|
}
|
|
@@ -920,7 +966,7 @@ async function configureGlobalUnlocked(input = {}, locks) {
|
|
|
920
966
|
for (const [root, localConfiguration] of localConfigurations) {
|
|
921
967
|
if (localConfiguration.node === null || !removedNode.has(localConfiguration.node)) continue;
|
|
922
968
|
let replacement = input.nextLocalConfigurations?.[root];
|
|
923
|
-
if (replacement === void 0 && input.prompt?.editLocal) replacement = await input.prompt.editLocal(localConfiguration, catalog, nextCheck.value);
|
|
969
|
+
if (replacement === void 0 && input.prompt?.editLocal) replacement = await input.prompt.editLocal(localConfiguration, catalog, nextCheck.value, false);
|
|
924
970
|
if (replacement === void 0) return failure({
|
|
925
971
|
kind: "validation",
|
|
926
972
|
code: "node-replacement-required",
|
|
@@ -1329,7 +1375,8 @@ async function composeStateWrite(input) {
|
|
|
1329
1375
|
selectedNode: input.localConfiguration.node,
|
|
1330
1376
|
configuredAgents: input.globalConfiguration.agent,
|
|
1331
1377
|
agentNotifications: input.globalConfiguration.agent_notifications,
|
|
1332
|
-
claudeHostConfiguration: claudeHostConfiguration.value
|
|
1378
|
+
claudeHostConfiguration: claudeHostConfiguration.value,
|
|
1379
|
+
ports: input.localConfiguration.ports
|
|
1333
1380
|
});
|
|
1334
1381
|
if (!rendered.ok) return rendered;
|
|
1335
1382
|
const path = join(input.stateDirectory, "compose.yaml");
|
|
@@ -1520,22 +1567,38 @@ function createConfigurationPrompter({ signal, input, output }) {
|
|
|
1520
1567
|
}))
|
|
1521
1568
|
};
|
|
1522
1569
|
},
|
|
1523
|
-
editLocal: async (configuration, catalog, globalConfiguration) => {
|
|
1524
|
-
p.log.step(`${c.bold(c.cyan("Project configuration"))}\n${c.dim("Choose the runtime for this Project.")}`, common);
|
|
1570
|
+
editLocal: async (configuration, catalog, globalConfiguration, editPorts = true) => {
|
|
1571
|
+
p.log.step(`${c.bold(c.cyan("Project configuration"))}\n${c.dim(editPorts ? "Choose the runtime and published ports for this Project." : "Choose the replacement runtime for this Project.")}`, common);
|
|
1572
|
+
const node = await promptValue(p.select({
|
|
1573
|
+
message: "Selected Node release line",
|
|
1574
|
+
options: [{
|
|
1575
|
+
value: null,
|
|
1576
|
+
label: "None"
|
|
1577
|
+
}, ...(globalConfiguration.node ?? []).map((value) => ({
|
|
1578
|
+
value,
|
|
1579
|
+
label: value
|
|
1580
|
+
}))],
|
|
1581
|
+
initialValue: configuration.node,
|
|
1582
|
+
...common
|
|
1583
|
+
}));
|
|
1584
|
+
if (!editPorts) return {
|
|
1585
|
+
...configuration,
|
|
1586
|
+
node
|
|
1587
|
+
};
|
|
1588
|
+
const ports = normalizePortMappings((await promptValue(p.multiline({
|
|
1589
|
+
message: "Published ports",
|
|
1590
|
+
initialValue: configuration.ports.join("\n"),
|
|
1591
|
+
validate: (value) => {
|
|
1592
|
+
const ports = normalizePortMappings((value ?? "").split("\n"));
|
|
1593
|
+
return ports.ok ? void 0 : ports.error.observed;
|
|
1594
|
+
},
|
|
1595
|
+
...common
|
|
1596
|
+
}))).split("\n"));
|
|
1597
|
+
if (!ports.ok) throw new Error(ports.error.observed);
|
|
1525
1598
|
return {
|
|
1526
1599
|
version: 1,
|
|
1527
|
-
node
|
|
1528
|
-
|
|
1529
|
-
options: [{
|
|
1530
|
-
value: null,
|
|
1531
|
-
label: "None"
|
|
1532
|
-
}, ...(globalConfiguration.node ?? []).map((value) => ({
|
|
1533
|
-
value,
|
|
1534
|
-
label: value
|
|
1535
|
-
}))],
|
|
1536
|
-
initialValue: configuration.node,
|
|
1537
|
-
...common
|
|
1538
|
-
}))
|
|
1600
|
+
node,
|
|
1601
|
+
ports: ports.value
|
|
1539
1602
|
};
|
|
1540
1603
|
}
|
|
1541
1604
|
};
|
|
@@ -1630,6 +1693,8 @@ async function buildWorkspaceUnlocked(input) {
|
|
|
1630
1693
|
const parsedGlobal = parseGlobalConfiguration(globalSource, catalogCheck.value);
|
|
1631
1694
|
if (!parsedGlobal.ok) return parsedGlobal;
|
|
1632
1695
|
const globalConfiguration = parsedGlobal.value;
|
|
1696
|
+
const hostIdentity = currentHostIdentity();
|
|
1697
|
+
if (!hostIdentity.ok) return hostIdentity;
|
|
1633
1698
|
const agents = globalConfiguration.agent.map((name) => ({
|
|
1634
1699
|
name,
|
|
1635
1700
|
recipe: PACKAGED_AGENTS[name]
|
|
@@ -1658,7 +1723,8 @@ async function buildWorkspaceUnlocked(input) {
|
|
|
1658
1723
|
const buildResult = await (input.executeDockerBuild ?? runDockerBuild)({
|
|
1659
1724
|
context: paths.buildContext,
|
|
1660
1725
|
image: WORKSPACE_IMAGE,
|
|
1661
|
-
noCache: input.noCache === true
|
|
1726
|
+
noCache: input.noCache === true,
|
|
1727
|
+
buildArgs: hostIdentity.value
|
|
1662
1728
|
});
|
|
1663
1729
|
if (!buildResult.ok) return buildResult;
|
|
1664
1730
|
return success({ image: WORKSPACE_IMAGE });
|
|
@@ -1671,17 +1737,38 @@ function buildNodeRuntimeRecipe(globalConfiguration) {
|
|
|
1671
1737
|
}
|
|
1672
1738
|
return buildNodeRuntime;
|
|
1673
1739
|
}
|
|
1740
|
+
function currentHostIdentity() {
|
|
1741
|
+
const userId = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
1742
|
+
const groupId = typeof process.getgid === "function" ? process.getgid() : void 0;
|
|
1743
|
+
if (typeof userId !== "number" || typeof groupId !== "number" || !Number.isSafeInteger(userId) || !Number.isSafeInteger(groupId) || userId <= 0 || groupId <= 0) return failure({
|
|
1744
|
+
kind: "validation",
|
|
1745
|
+
code: "invalid-host-identity",
|
|
1746
|
+
observed: "The invoking host process must have a non-root POSIX numeric UID and GID.",
|
|
1747
|
+
nextAction: "Run devbox build as the intended non-root host user."
|
|
1748
|
+
});
|
|
1749
|
+
return success({
|
|
1750
|
+
USER_ID: String(userId),
|
|
1751
|
+
GROUP_ID: String(groupId)
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1674
1754
|
function renderBuildDockerfile(input) {
|
|
1675
1755
|
const lines = [
|
|
1676
1756
|
"# Generated by devbox build. Machine-owned Workspace build context.",
|
|
1677
1757
|
"FROM ubuntu:24.04",
|
|
1678
1758
|
"",
|
|
1759
|
+
"ARG USER_ID",
|
|
1760
|
+
"ARG GROUP_ID",
|
|
1761
|
+
"",
|
|
1679
1762
|
"ENV DEBIAN_FRONTEND=noninteractive",
|
|
1680
1763
|
"ENV TZ=UTC",
|
|
1681
1764
|
"ENV LANG=C.UTF-8",
|
|
1682
1765
|
"",
|
|
1683
1766
|
"RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone",
|
|
1684
1767
|
"",
|
|
1768
|
+
"RUN echo \"Acquire::http::Pipeline-Depth 0;\" > /etc/apt/apt.conf.d/99custom && \\",
|
|
1769
|
+
" echo \"Acquire::http::No-Cache true;\" >> /etc/apt/apt.conf.d/99custom && \\",
|
|
1770
|
+
" echo \"Acquire::BrokenProxy true;\" >> /etc/apt/apt.conf.d/99custom",
|
|
1771
|
+
"",
|
|
1685
1772
|
"# Install the Base profile packages and build tools",
|
|
1686
1773
|
"RUN set -eux \\",
|
|
1687
1774
|
" && apt-get update && apt-get upgrade -y \\",
|
|
@@ -1697,8 +1784,8 @@ function renderBuildDockerfile(input) {
|
|
|
1697
1784
|
"",
|
|
1698
1785
|
"# Create the devbox user and group",
|
|
1699
1786
|
"RUN userdel -r ubuntu \\",
|
|
1700
|
-
" && groupadd -
|
|
1701
|
-
" && useradd -m -s /bin/bash -N -g
|
|
1787
|
+
" && groupadd -g \"$GROUP_ID\" devbox \\",
|
|
1788
|
+
" && useradd -m -s /bin/bash -N -g \"$GROUP_ID\" -u \"$USER_ID\" devbox \\",
|
|
1702
1789
|
" && echo \"devbox ALL=(ALL) NOPASSWD:ALL\" > /etc/sudoers.d/devbox \\",
|
|
1703
1790
|
" && chmod 0440 /etc/sudoers.d/devbox",
|
|
1704
1791
|
""
|
|
@@ -1767,11 +1854,7 @@ function selectBuildContextAssets(agents) {
|
|
|
1767
1854
|
return assets;
|
|
1768
1855
|
}
|
|
1769
1856
|
function renderEntrypoint(input) {
|
|
1770
|
-
const lines = [
|
|
1771
|
-
"#!/bin/sh",
|
|
1772
|
-
"set -eu",
|
|
1773
|
-
""
|
|
1774
|
-
];
|
|
1857
|
+
const lines = ["#!/usr/bin/env bash", ""];
|
|
1775
1858
|
if (input.nodeRuntimes.length > 0) lines.push("# Set the Node.js release line to use", "NODE_VERSION=\"${NODE_VERSION:-}\"", "if [ -n \"$NODE_VERSION\" ]; then", " case \"$NODE_VERSION\" in", " *[!0-9]*)", " echo \"NODE_VERSION must be a numeric release line: $NODE_VERSION\" >&2", " exit 1", " ;;", " esac", "", " NODE_RUNTIME_ROOT=\"/opt/devbox/runtimes/node/$NODE_VERSION\"", " if [ ! -x \"$NODE_RUNTIME_ROOT/bin/node\" ]; then", " echo \"Node.js release line $NODE_VERSION is not installed.\" >&2", " exit 1", " fi", "", " if ! grep -q \"# Devbox\" /etc/bash.bashrc; then", " printf \"\\n# Devbox\\nexport PATH=\\\"$NODE_RUNTIME_ROOT/bin:\\$PATH\\\"\\n\" >> /etc/bash.bashrc", " fi", "", " export PATH=\"$NODE_RUNTIME_ROOT/bin:$PATH\"", "fi", "");
|
|
1776
1859
|
if (input.agents.some((agent) => agent.name === "agy")) lines.push("# Link shared Agent Skills", "if [ ! -L \"/home/devbox/.gemini/skills\" ]; then", " ln -sfn /home/devbox/.agents/skills /home/devbox/.gemini/skills", " chown -h devbox:devbox /home/devbox/.gemini/skills", "fi", "");
|
|
1777
1860
|
lines.push("if [ $# -gt 0 ]; then", " exec gosu devbox \"$@\"", "else", " exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf", "fi");
|
|
@@ -1834,7 +1917,7 @@ function renderNodeRuntimeStage(recipe) {
|
|
|
1834
1917
|
" && export PATH=\"$NODE_RUNTIME_ROOT/bin:$PATH\" \\",
|
|
1835
1918
|
" && npm install -g npm \\",
|
|
1836
1919
|
" && npm uninstall -g corepack \\",
|
|
1837
|
-
" && npm install -g yarn pnpm @antfu/ni \\",
|
|
1920
|
+
" && npm install -g yarn pnpm @antfu/ni --allow-scripts=pnpm,yarn \\",
|
|
1838
1921
|
" && \"$NODE_RUNTIME_ROOT/bin/node\" --version \\",
|
|
1839
1922
|
" && \"$NODE_RUNTIME_ROOT/bin/npm\" --version \\",
|
|
1840
1923
|
" && rm -rf /tmp/node-source",
|
|
@@ -1845,6 +1928,10 @@ async function runDockerBuild(invocation, signal) {
|
|
|
1845
1928
|
const build = spawn("docker", [
|
|
1846
1929
|
"build",
|
|
1847
1930
|
...invocation.noCache ? ["--no-cache"] : [],
|
|
1931
|
+
"--build-arg",
|
|
1932
|
+
`USER_ID=${invocation.buildArgs.USER_ID}`,
|
|
1933
|
+
"--build-arg",
|
|
1934
|
+
`GROUP_ID=${invocation.buildArgs.GROUP_ID}`,
|
|
1848
1935
|
"--tag",
|
|
1849
1936
|
invocation.image,
|
|
1850
1937
|
"--file",
|