@ycs77/devbox 0.3.1 → 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 (3) hide show
  1. package/README.md +2 -3
  2. package/dist/cli.mjs +135 -37
  3. package/package.json +5 -3
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # @ycs77/devbox
4
4
 
5
- **A containerized devbox for AI agents.**
5
+ **A opinionated devbox for AI agents.**
6
6
 
7
7
  [![NPM version][ico-version]][link-npm]
8
8
  [![Software License][ico-license]](LICENSE)
@@ -79,8 +79,7 @@ Run these commands from the initialized project directory:
79
79
  | `devbox sh` | Open a Bash shell in the running sandbox. |
80
80
  | `devbox stop` | Stop the sandbox and preserve it for a later restart. |
81
81
  | `devbox down` | Stop and remove the sandbox container. |
82
- | `devbox config` | Change the current project's Node.js selection. |
83
- | `devbox config -g` | Change the globally configured Node.js versions or AI agents. |
82
+ | `devbox config` | Change the globally configured or current project's Node.js selection. |
84
83
 
85
84
  ## Troubleshooting
86
85
 
package/dist/cli.mjs CHANGED
@@ -45,9 +45,11 @@ const PACKAGED_AGENTS = {
45
45
  },
46
46
  installation: {
47
47
  url: "https://claude.ai/install.sh",
48
- shell: "bash"
48
+ shell: "bash",
49
+ preInstallCommands: ["echo '{}' > /home/devbox/.claude.json"]
49
50
  },
50
51
  supportsNotifications: true,
52
+ notificationInstallationCommands: ["claude plugin marketplace add ycs77/claude-code-notifications", "claude plugin install notification-basic-wsl@ycs77-notifications"],
51
53
  supportsSkillInstallation: true
52
54
  },
53
55
  codex: {
@@ -60,7 +62,8 @@ const PACKAGED_AGENTS = {
60
62
  shell: "sh"
61
63
  },
62
64
  supportsNotifications: true,
63
- supportsSkillInstallation: true
65
+ supportsSkillInstallation: true,
66
+ notificationInstallationCommands: ["codex plugin marketplace add ycs77/codex-notifications", "codex plugin add notification-basic-wsl@ycs77-notifications"]
64
67
  },
65
68
  agy: {
66
69
  home: {
@@ -84,7 +87,8 @@ const PACKAGED_AGENTS = {
84
87
  shell: "sh"
85
88
  },
86
89
  supportsNotifications: true,
87
- supportsSkillInstallation: false
90
+ supportsSkillInstallation: false,
91
+ notificationInstallationCommands: ["omp plugin marketplace add ycs77/omp-notifications", "omp plugin install notification-basic@ycs77-notifications"]
88
92
  }
89
93
  };
90
94
  const PACKAGED_RUNTIME_CATALOG = {
@@ -205,7 +209,8 @@ function defaultGlobalConfiguration(catalog = PACKAGED_RUNTIME_CATALOG) {
205
209
  function defaultLocalConfiguration(globalConfiguration) {
206
210
  return {
207
211
  version: 1,
208
- node: globalConfiguration.node[0] ?? null
212
+ node: globalConfiguration.node[0] ?? null,
213
+ ports: ["APP_PORT:5173:5173"]
209
214
  };
210
215
  }
211
216
  function parseGlobalConfiguration(source, catalog = PACKAGED_RUNTIME_CATALOG) {
@@ -245,15 +250,22 @@ function parseLocalConfiguration(source, globalConfiguration, catalog = PACKAGED
245
250
  const parsed = parseYaml(source);
246
251
  if (!parsed.ok) return invalid$1("invalid-local-configuration", parsed.observed);
247
252
  const document = parsed.value;
248
- 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.");
249
258
  if (document.version !== 1) return invalid$1("invalid-local-configuration", "Local configuration must use version: 1.");
250
259
  const node = runtimeLineOrNull(document.node);
251
260
  if (node === void 0) return invalid$1("invalid-local-configuration", `Node Runtime is invalid: ${String(document.node)}.`);
252
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)}.`);
253
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;
254
265
  return success({
255
266
  version: 1,
256
- node
267
+ node,
268
+ ports: ports.value
257
269
  });
258
270
  }
259
271
  function serializeGlobalConfiguration(configuration) {
@@ -267,12 +279,31 @@ function serializeGlobalConfiguration(configuration) {
267
279
  function serializeLocalConfiguration(configuration) {
268
280
  return stringify({
269
281
  version: 1,
270
- node: configuration.node
282
+ node: configuration.node,
283
+ ports: [...configuration.ports]
271
284
  });
272
285
  }
273
286
  function configurationsEqual(left, right) {
274
287
  return JSON.stringify(left) === JSON.stringify(right);
275
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
+ }
276
307
  function parseYaml(source) {
277
308
  try {
278
309
  const document = parseDocument(source, {
@@ -303,6 +334,22 @@ function runtimeLineOrNull(value) {
303
334
  if (value === null) return null;
304
335
  return runtimeLine(value);
305
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
+ }
306
353
  function isRecord(value) {
307
354
  if (typeof value !== "object" || value === null) return false;
308
355
  const prototype = Object.getPrototypeOf(value);
@@ -460,6 +507,7 @@ function selectComposeFragments({ selectedNode, configuredAgents, agentNotificat
460
507
  function renderProjectCompose(input) {
461
508
  const fragments = selectComposeFragments(input);
462
509
  const environment = {
510
+ TERM: "xterm-256color",
463
511
  ...fragments.node?.environment,
464
512
  ...fragments.notification?.environment
465
513
  };
@@ -496,8 +544,10 @@ function renderProjectCompose(input) {
496
544
  },
497
545
  services: { devbox: {
498
546
  image: WORKSPACE_IMAGE,
499
- container_name: input.sandboxName,
547
+ hostname: "devbox",
548
+ container_name: `devbox-${input.sandboxName}`,
500
549
  working_dir: `/workspace/${input.sandboxName}`,
550
+ ...input.ports.length === 0 ? {} : { ports: input.ports.map((port) => renderComposePortMapping(port)) },
501
551
  ...Object.keys(environment).length === 0 ? {} : { environment },
502
552
  volumes
503
553
  } },
@@ -521,8 +571,9 @@ function validatesProjectCompose(source, input) {
521
571
  const metadata = definition["x-devbox"];
522
572
  if (metadata.version !== 1 || metadata.project_root !== input.projectRoot || metadata.compose_name !== input.sandboxName || metadata.sandbox_service !== "devbox") return false;
523
573
  const service = definition.services.devbox;
524
- if (service.image !== "devbox-workspace:latest" || service.container_name !== input.sandboxName || service.working_dir !== `/workspace/${input.sandboxName}` || "build" in service) return false;
525
- return input.selectedNode === null && service.environment?.NODE_VERSION === void 0 || input.selectedNode !== null && service.environment?.NODE_VERSION === input.selectedNode;
574
+ if (service.image !== "devbox-workspace:latest" || service.container_name !== `devbox-${input.sandboxName}` || service.working_dir !== `/workspace/${input.sandboxName}` || "build" in service) return false;
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);
526
577
  } catch {
527
578
  return false;
528
579
  }
@@ -915,7 +966,7 @@ async function configureGlobalUnlocked(input = {}, locks) {
915
966
  for (const [root, localConfiguration] of localConfigurations) {
916
967
  if (localConfiguration.node === null || !removedNode.has(localConfiguration.node)) continue;
917
968
  let replacement = input.nextLocalConfigurations?.[root];
918
- 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);
919
970
  if (replacement === void 0) return failure({
920
971
  kind: "validation",
921
972
  code: "node-replacement-required",
@@ -1324,7 +1375,8 @@ async function composeStateWrite(input) {
1324
1375
  selectedNode: input.localConfiguration.node,
1325
1376
  configuredAgents: input.globalConfiguration.agent,
1326
1377
  agentNotifications: input.globalConfiguration.agent_notifications,
1327
- claudeHostConfiguration: claudeHostConfiguration.value
1378
+ claudeHostConfiguration: claudeHostConfiguration.value,
1379
+ ports: input.localConfiguration.ports
1328
1380
  });
1329
1381
  if (!rendered.ok) return rendered;
1330
1382
  const path = join(input.stateDirectory, "compose.yaml");
@@ -1515,22 +1567,38 @@ function createConfigurationPrompter({ signal, input, output }) {
1515
1567
  }))
1516
1568
  };
1517
1569
  },
1518
- editLocal: async (configuration, catalog, globalConfiguration) => {
1519
- 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);
1520
1598
  return {
1521
1599
  version: 1,
1522
- node: await promptValue(p.select({
1523
- message: "Selected Node release line",
1524
- options: [{
1525
- value: null,
1526
- label: "None"
1527
- }, ...(globalConfiguration.node ?? []).map((value) => ({
1528
- value,
1529
- label: value
1530
- }))],
1531
- initialValue: configuration.node,
1532
- ...common
1533
- }))
1600
+ node,
1601
+ ports: ports.value
1534
1602
  };
1535
1603
  }
1536
1604
  };
@@ -1625,6 +1693,8 @@ async function buildWorkspaceUnlocked(input) {
1625
1693
  const parsedGlobal = parseGlobalConfiguration(globalSource, catalogCheck.value);
1626
1694
  if (!parsedGlobal.ok) return parsedGlobal;
1627
1695
  const globalConfiguration = parsedGlobal.value;
1696
+ const hostIdentity = currentHostIdentity();
1697
+ if (!hostIdentity.ok) return hostIdentity;
1628
1698
  const agents = globalConfiguration.agent.map((name) => ({
1629
1699
  name,
1630
1700
  recipe: PACKAGED_AGENTS[name]
@@ -1635,6 +1705,7 @@ async function buildWorkspaceUnlocked(input) {
1635
1705
  nodeRuntimes,
1636
1706
  buildNodeRuntime: buildNodeRuntimeRecipe(globalConfiguration),
1637
1707
  agents,
1708
+ agentNotifications: globalConfiguration.agent_notifications,
1638
1709
  skillAgents: globalConfiguration.agent.filter((agent) => PACKAGED_AGENTS[agent]?.supportsSkillInstallation === true)
1639
1710
  });
1640
1711
  await mkdir(paths.buildContext, { recursive: true });
@@ -1652,7 +1723,8 @@ async function buildWorkspaceUnlocked(input) {
1652
1723
  const buildResult = await (input.executeDockerBuild ?? runDockerBuild)({
1653
1724
  context: paths.buildContext,
1654
1725
  image: WORKSPACE_IMAGE,
1655
- noCache: input.noCache === true
1726
+ noCache: input.noCache === true,
1727
+ buildArgs: hostIdentity.value
1656
1728
  });
1657
1729
  if (!buildResult.ok) return buildResult;
1658
1730
  return success({ image: WORKSPACE_IMAGE });
@@ -1665,17 +1737,38 @@ function buildNodeRuntimeRecipe(globalConfiguration) {
1665
1737
  }
1666
1738
  return buildNodeRuntime;
1667
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
+ }
1668
1754
  function renderBuildDockerfile(input) {
1669
1755
  const lines = [
1670
1756
  "# Generated by devbox build. Machine-owned Workspace build context.",
1671
1757
  "FROM ubuntu:24.04",
1672
1758
  "",
1759
+ "ARG USER_ID",
1760
+ "ARG GROUP_ID",
1761
+ "",
1673
1762
  "ENV DEBIAN_FRONTEND=noninteractive",
1674
1763
  "ENV TZ=UTC",
1675
1764
  "ENV LANG=C.UTF-8",
1676
1765
  "",
1677
1766
  "RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone",
1678
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
+ "",
1679
1772
  "# Install the Base profile packages and build tools",
1680
1773
  "RUN set -eux \\",
1681
1774
  " && apt-get update && apt-get upgrade -y \\",
@@ -1691,8 +1784,8 @@ function renderBuildDockerfile(input) {
1691
1784
  "",
1692
1785
  "# Create the devbox user and group",
1693
1786
  "RUN userdel -r ubuntu \\",
1694
- " && groupadd -f -g 1000 devbox \\",
1695
- " && 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 \\",
1696
1789
  " && echo \"devbox ALL=(ALL) NOPASSWD:ALL\" > /etc/sudoers.d/devbox \\",
1697
1790
  " && chmod 0440 /etc/sudoers.d/devbox",
1698
1791
  ""
@@ -1700,7 +1793,10 @@ function renderBuildDockerfile(input) {
1700
1793
  for (const recipe of input.nodeRuntimes) lines.push(...renderNodeRuntimeStage(recipe));
1701
1794
  lines.push("WORKDIR /workspace", "USER devbox", "");
1702
1795
  if (input.buildNodeRuntime !== void 0) lines.push("# Configure pnpm for the non-root Sandbox user", "RUN mkdir -p /home/devbox/.config/pnpm /home/devbox/.pnpm-store \\", " && printf 'storeDir: /home/devbox/.pnpm-store\\n' > /home/devbox/.config/pnpm/config.yaml \\", " && chown devbox:devbox /home/devbox/.config/pnpm/config.yaml", "");
1703
- for (const agent of input.agents) lines.push(`# Install ${agent.name}`, `RUN mkdir -p ${agent.recipe.home.target} \\`, ` && curl -fsSL ${agent.recipe.installation.url} | ${agent.recipe.installation.shell}`, "");
1796
+ for (const agent of input.agents) {
1797
+ const preInstallCommands = agent.recipe.installation.preInstallCommands ?? [];
1798
+ lines.push(`# Install ${agent.name}`, `RUN mkdir -p ${agent.recipe.home.target} \\`, ...preInstallCommands.map((command) => ` && ${command} \\`), ` && curl -fsSL ${agent.recipe.installation.url} | ${agent.recipe.installation.shell}`, "");
1799
+ }
1704
1800
  if (input.agents.length > 0) lines.push("# Set the PATH to include AI tools", "ENV PATH=\"/home/devbox/.local/bin:${PATH}\"", "");
1705
1801
  if (input.buildNodeRuntime !== void 0 && input.skillAgents.length > 0) {
1706
1802
  const skillAgentArguments = input.skillAgents.map((agent) => `-a ${agent}`).join(" ");
@@ -1722,6 +1818,8 @@ function renderBuildDockerfile(input) {
1722
1818
  }
1723
1819
  lines.push(`RUN ${ownershipCommands.join(" \\\n && ")}`, "");
1724
1820
  }
1821
+ const notificationInstallationCommands = input.agentNotifications ? input.agents.flatMap((agent) => agent.recipe.supportsNotifications ? agent.recipe.notificationInstallationCommands : []) : [];
1822
+ if (notificationInstallationCommands.length > 0) lines.push("# Install Agent Notification Plugins", "USER devbox", "RUN set -eux \\", ...notificationInstallationCommands.map((command, index) => ` && ${command}${index < notificationInstallationCommands.length - 1 ? " \\" : ""}`), "USER root", "");
1725
1823
  lines.push("COPY entrypoint.sh /usr/local/bin/entrypoint.sh", "COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf", "RUN chmod +x /usr/local/bin/entrypoint.sh", "", "ENTRYPOINT [\"/usr/local/bin/entrypoint.sh\"]");
1726
1824
  return `${lines.join("\n")}\n`;
1727
1825
  }
@@ -1756,12 +1854,8 @@ function selectBuildContextAssets(agents) {
1756
1854
  return assets;
1757
1855
  }
1758
1856
  function renderEntrypoint(input) {
1759
- const lines = [
1760
- "#!/bin/sh",
1761
- "set -eu",
1762
- ""
1763
- ];
1764
- 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", "", " export NODE_RUNTIME_ROOT", " export PATH=\"$NODE_RUNTIME_ROOT/bin:$PATH\"", "fi", "");
1857
+ const lines = ["#!/usr/bin/env bash", ""];
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", "");
1765
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", "");
1766
1860
  lines.push("if [ $# -gt 0 ]; then", " exec gosu devbox \"$@\"", "else", " exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf", "fi");
1767
1861
  return `${lines.join("\n")}\n`;
@@ -1823,7 +1917,7 @@ function renderNodeRuntimeStage(recipe) {
1823
1917
  " && export PATH=\"$NODE_RUNTIME_ROOT/bin:$PATH\" \\",
1824
1918
  " && npm install -g npm \\",
1825
1919
  " && npm uninstall -g corepack \\",
1826
- " && npm install -g yarn pnpm @antfu/ni \\",
1920
+ " && npm install -g yarn pnpm @antfu/ni --allow-scripts=pnpm,yarn \\",
1827
1921
  " && \"$NODE_RUNTIME_ROOT/bin/node\" --version \\",
1828
1922
  " && \"$NODE_RUNTIME_ROOT/bin/npm\" --version \\",
1829
1923
  " && rm -rf /tmp/node-source",
@@ -1834,6 +1928,10 @@ async function runDockerBuild(invocation, signal) {
1834
1928
  const build = spawn("docker", [
1835
1929
  "build",
1836
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}`,
1837
1935
  "--tag",
1838
1936
  invocation.image,
1839
1937
  "--file",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ycs77/devbox",
3
- "version": "0.3.1",
4
- "description": "A containerized devbox for AI agents",
3
+ "version": "0.5.0",
4
+ "description": "A Lucas Yang's opinionated devbox for AI agents",
5
5
  "keywords": [
6
6
  "agent",
7
7
  "agentic",
@@ -40,6 +40,7 @@
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "^26.2.0",
43
+ "actionspack": "^0.1.5",
43
44
  "bumpp": "^12.2.0",
44
45
  "memfs": "^4.68.1",
45
46
  "oxfmt": "^0.63.0",
@@ -60,7 +61,8 @@
60
61
  "typecheck": "tsc --noEmit",
61
62
  "test": "vitest",
62
63
  "check": "pnpm lint && pnpm fmt --check && pnpm typecheck && pnpm test --run",
63
- "smoke:cli": "node scripts/package-devbox-smoke.mjs",
64
+ "smoke": "node scripts/package-devbox-smoke.mjs",
65
+ "update-actions": "actionspack update",
64
66
  "release": "bumpp --commit \"Release v%s\""
65
67
  }
66
68
  }