@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.
Files changed (2) hide show
  1. package/dist/cli.mjs +117 -30
  2. 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"])) return invalid$1("invalid-local-configuration", "Local configuration contains unknown or missing fields.");
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
- return input.selectedNode === null && service.environment?.NODE_VERSION === void 0 || input.selectedNode !== null && service.environment?.NODE_VERSION === input.selectedNode;
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: await promptValue(p.select({
1528
- message: "Selected Node release line",
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 -f -g 1000 devbox \\",
1701
- " && useradd -m -s /bin/bash -N -g 1000 -u 1000 devbox \\",
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycs77/devbox",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "A Lucas Yang's opinionated devbox for AI agents",
5
5
  "keywords": [
6
6
  "agent",