@ycs77/devbox 0.4.0 → 0.5.1

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 +18 -15
  2. package/dist/cli.mjs +143 -39
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # @ycs77/devbox
4
4
 
5
- **A opinionated devbox for AI agents.**
5
+ **An opinionated devbox for AI agents.**
6
6
 
7
7
  [![NPM version][ico-version]][link-npm]
8
8
  [![Software License][ico-license]](LICENSE)
@@ -17,20 +17,20 @@ English | [繁體中文](README-zh-TW.md)
17
17
 
18
18
  ## Overview
19
19
 
20
- Devbox gives each project an isolated Docker sandbox designed for AI agents, with Lucas's opinionated development setup. Choose the Node.js release lines and AI agents you use, then work from a consistent container without configuring it yourself.
20
+ Devbox gives each project an isolated Docker sandbox for AI agents, preconfigured with Lucas's development preferences.
21
21
 
22
22
  ## Features
23
23
 
24
24
  - **Container environment for AI agents:** choose Claude Code, Codex, and more during setup, then use them inside the sandbox.
25
- - **Simple setup and project lifecycle:** initialize, start, enter, stop, and remove a sandbox with a few commands.
26
- - **Flexible Node.js environments:** configure Node.js 22 and 24, then select a version for each project.
25
+ - **Simple project sandbox management:** initialize, start, enter, stop, and remove a sandbox with a few commands.
26
+ - **Flexible Node.js versions:** choose the versions to include, then select one for each project.
27
27
  - **Selective builds:** build only the Node.js versions and AI agents you choose.
28
28
  - **No project configuration files:** keep Devbox configuration local to your machine, not in your repository.
29
29
 
30
30
  ## Requirements
31
31
 
32
- - **Node.js 22 or later:** installs and runs the CLI with npm.
33
- - **Docker with Docker Compose:** runs the development sandbox.
32
+ - **Node.js 22 or later:** required to install and run the CLI with npm.
33
+ - **Docker with Docker Compose:** required to run the development sandbox.
34
34
  - **Windows:** use Devbox from a **WSL2** Linux distribution with Docker Desktop integration enabled.
35
35
 
36
36
  ## Getting started
@@ -47,9 +47,11 @@ From your project directory, initialize Devbox:
47
47
  devbox init
48
48
  ```
49
49
 
50
- The interactive setup lets you choose the Node.js release lines and AI agents to configure, then select the Node.js version for the current project.
50
+ The interactive setup lets you choose the Node.js versions and AI agents to include, then select a Node.js version for the current project.
51
51
 
52
- Build the shared workspace image once after setup or after changing global configuration:
52
+ Global configuration controls what goes into the shared Workspace Image. Each Node.js version or AI agent you add makes the image larger and uses more disk space. Projects can use only the Node.js versions included in the image.
53
+
54
+ Build the shared Workspace Image once after setup or whenever you change global configuration:
53
55
 
54
56
  ```bash
55
57
  devbox build
@@ -67,9 +69,9 @@ Open a shell inside the running sandbox and start working with your AI agent:
67
69
  devbox sh
68
70
  ```
69
71
 
70
- For example, run `claude`, `codex`, `agy`, or `omp` after opening the shell when that agent was selected during setup.
72
+ If you selected an agent during setup, run `claude`, `codex`, `agy`, or `omp` inside the shell.
71
73
 
72
- ## Everyday commands
74
+ ## Commands
73
75
 
74
76
  Run these commands from the initialized project directory:
75
77
 
@@ -79,23 +81,24 @@ Run these commands from the initialized project directory:
79
81
  | `devbox sh` | Open a Bash shell in the running sandbox. |
80
82
  | `devbox stop` | Stop the sandbox and preserve it for a later restart. |
81
83
  | `devbox down` | Stop and remove the sandbox container. |
82
- | `devbox config` | Change the globally configured or current project's Node.js selection. |
84
+ | `devbox config` | Change global settings or this project's configuration. |
85
+ | `devbox cleanup --missing-projects` | Remove registrations for projects whose root directory no longer exists. |
83
86
 
84
87
  ## Troubleshooting
85
88
 
86
- ### A command says a Devbox marker is occupied
89
+ ### Devbox reports that a command marker is in use
87
90
 
88
- An interrupted process or sudden system shutdown can leave a command marker behind. First confirm that no Devbox process is still running, then remove only the stale marker and retry:
91
+ An interrupted command or sudden system shutdown can leave a command marker behind. First confirm that no other Devbox command is still running, then remove the stale marker and retry:
89
92
 
90
93
  ```bash
91
94
  rm -rf ~/.devbox/locks/<marker>
92
95
  ```
93
96
 
94
- Markers are directories named `global` or `project-<hash>`. Do not remove a marker while its Devbox command is still running.
97
+ A marker is a directory named `global` or `project-<hash>`. Do not remove a marker while its Devbox command is still running.
95
98
 
96
99
  ## Sponsor
97
100
 
98
- If you think this package has helped you, please consider [Becoming a sponsor](https://www.patreon.com/ycs77) to support my work~ and your avatar will be visible on my major projects.
101
+ If Devbox has helped you, consider sponsoring my work on Patreon. Your avatar will appear on my major projects.
99
102
 
100
103
  <p align="center">
101
104
  <a href="https://www.patreon.com/ycs77">
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);
@@ -464,7 +507,6 @@ function selectComposeFragments({ selectedNode, configuredAgents, agentNotificat
464
507
  function renderProjectCompose(input) {
465
508
  const fragments = selectComposeFragments(input);
466
509
  const environment = {
467
- TERM: "xterm-256color",
468
510
  ...fragments.node?.environment,
469
511
  ...fragments.notification?.environment
470
512
  };
@@ -501,8 +543,10 @@ function renderProjectCompose(input) {
501
543
  },
502
544
  services: { devbox: {
503
545
  image: WORKSPACE_IMAGE,
546
+ hostname: "devbox",
504
547
  container_name: `devbox-${input.sandboxName}`,
505
548
  working_dir: `/workspace/${input.sandboxName}`,
549
+ ...input.ports.length === 0 ? {} : { ports: input.ports.map((port) => renderComposePortMapping(port)) },
506
550
  ...Object.keys(environment).length === 0 ? {} : { environment },
507
551
  volumes
508
552
  } },
@@ -527,7 +571,8 @@ function validatesProjectCompose(source, input) {
527
571
  if (metadata.version !== 1 || metadata.project_root !== input.projectRoot || metadata.compose_name !== input.sandboxName || metadata.sandbox_service !== "devbox") return false;
528
572
  const service = definition.services.devbox;
529
573
  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;
574
+ const ports = input.ports.map((port) => renderComposePortMapping(port));
575
+ 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
576
  } catch {
532
577
  return false;
533
578
  }
@@ -920,7 +965,7 @@ async function configureGlobalUnlocked(input = {}, locks) {
920
965
  for (const [root, localConfiguration] of localConfigurations) {
921
966
  if (localConfiguration.node === null || !removedNode.has(localConfiguration.node)) continue;
922
967
  let replacement = input.nextLocalConfigurations?.[root];
923
- if (replacement === void 0 && input.prompt?.editLocal) replacement = await input.prompt.editLocal(localConfiguration, catalog, nextCheck.value);
968
+ if (replacement === void 0 && input.prompt?.editLocal) replacement = await input.prompt.editLocal(localConfiguration, catalog, nextCheck.value, false);
924
969
  if (replacement === void 0) return failure({
925
970
  kind: "validation",
926
971
  code: "node-replacement-required",
@@ -1145,9 +1190,12 @@ async function runSandboxLifecycle(command, input = {}) {
1145
1190
  global: false,
1146
1191
  projectRoots: [projectRoot],
1147
1192
  signal: input.signal
1148
- }, () => runSandboxLifecycleUnlocked(command, input, projectRoot));
1193
+ }, () => runSandboxCommandUnlocked(command, input, projectRoot));
1194
+ }
1195
+ async function runSandboxShell(input = {}) {
1196
+ return runSandboxCommandUnlocked("sh", input, input.root ?? process.cwd());
1149
1197
  }
1150
- async function runSandboxLifecycleUnlocked(command, input, projectRoot) {
1198
+ async function runSandboxCommandUnlocked(command, input, projectRoot) {
1151
1199
  if (input.signal?.aborted) throw new InterruptedError();
1152
1200
  const rootCheck = await validateProjectRoot(projectRoot);
1153
1201
  if (!rootCheck.ok) return rootCheck;
@@ -1329,7 +1377,8 @@ async function composeStateWrite(input) {
1329
1377
  selectedNode: input.localConfiguration.node,
1330
1378
  configuredAgents: input.globalConfiguration.agent,
1331
1379
  agentNotifications: input.globalConfiguration.agent_notifications,
1332
- claudeHostConfiguration: claudeHostConfiguration.value
1380
+ claudeHostConfiguration: claudeHostConfiguration.value,
1381
+ ports: input.localConfiguration.ports
1333
1382
  });
1334
1383
  if (!rendered.ok) return rendered;
1335
1384
  const path = join(input.stateDirectory, "compose.yaml");
@@ -1485,18 +1534,20 @@ function createConfigurationPrompter({ signal, input, output }) {
1485
1534
  }));
1486
1535
  },
1487
1536
  selectConfigurationScope: async () => promptValue(p.select({
1488
- message: "Configuration scope",
1537
+ message: "Where should this configuration apply?",
1489
1538
  options: [{
1490
1539
  value: "local",
1491
- label: "Current Project"
1540
+ label: "Current project",
1541
+ hint: "Enabled when this project’s container starts."
1492
1542
  }, {
1493
1543
  value: "global",
1494
- label: "Global"
1544
+ label: "Workspace Docker image",
1545
+ hint: "Built into the shared image. It affects image size and disk usage."
1495
1546
  }],
1496
1547
  ...common
1497
1548
  })),
1498
1549
  editGlobal: async (configuration, catalog) => {
1499
- p.log.step(`${c.bold(c.cyan("Global configuration"))}\n${c.dim("Choose the runtimes and agents available to every Project.")}`, common);
1550
+ p.log.step(`${c.bold(c.cyan("Workspace Docker image"))}\n${c.dim("Add runtimes and agents to the shared image. They are available to every project and increase the image size.")}`, common);
1500
1551
  return {
1501
1552
  version: 1,
1502
1553
  node: await promptValue(p.multiselect({
@@ -1520,22 +1571,38 @@ function createConfigurationPrompter({ signal, input, output }) {
1520
1571
  }))
1521
1572
  };
1522
1573
  },
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);
1574
+ editLocal: async (configuration, catalog, globalConfiguration, editPorts = true) => {
1575
+ p.log.step(`${c.bold(c.cyan("Project configuration"))}\n${c.dim(editPorts ? "Choose the Node version and ports to use when this project’s container starts." : "Choose the Node version to use when this project’s container starts.")}`, common);
1576
+ const node = await promptValue(p.select({
1577
+ message: "Selected Node release line",
1578
+ options: [{
1579
+ value: null,
1580
+ label: "None"
1581
+ }, ...(globalConfiguration.node ?? []).map((value) => ({
1582
+ value,
1583
+ label: value
1584
+ }))],
1585
+ initialValue: configuration.node,
1586
+ ...common
1587
+ }));
1588
+ if (!editPorts) return {
1589
+ ...configuration,
1590
+ node
1591
+ };
1592
+ const ports = normalizePortMappings((await promptValue(p.multiline({
1593
+ message: "Published ports",
1594
+ initialValue: configuration.ports.join("\n"),
1595
+ validate: (value) => {
1596
+ const ports = normalizePortMappings((value ?? "").split("\n"));
1597
+ return ports.ok ? void 0 : ports.error.observed;
1598
+ },
1599
+ ...common
1600
+ }))).split("\n"));
1601
+ if (!ports.ok) throw new Error(ports.error.observed);
1525
1602
  return {
1526
1603
  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
- }))
1604
+ node,
1605
+ ports: ports.value
1539
1606
  };
1540
1607
  }
1541
1608
  };
@@ -1630,6 +1697,8 @@ async function buildWorkspaceUnlocked(input) {
1630
1697
  const parsedGlobal = parseGlobalConfiguration(globalSource, catalogCheck.value);
1631
1698
  if (!parsedGlobal.ok) return parsedGlobal;
1632
1699
  const globalConfiguration = parsedGlobal.value;
1700
+ const hostIdentity = currentHostIdentity();
1701
+ if (!hostIdentity.ok) return hostIdentity;
1633
1702
  const agents = globalConfiguration.agent.map((name) => ({
1634
1703
  name,
1635
1704
  recipe: PACKAGED_AGENTS[name]
@@ -1658,7 +1727,8 @@ async function buildWorkspaceUnlocked(input) {
1658
1727
  const buildResult = await (input.executeDockerBuild ?? runDockerBuild)({
1659
1728
  context: paths.buildContext,
1660
1729
  image: WORKSPACE_IMAGE,
1661
- noCache: input.noCache === true
1730
+ noCache: input.noCache === true,
1731
+ buildArgs: hostIdentity.value
1662
1732
  });
1663
1733
  if (!buildResult.ok) return buildResult;
1664
1734
  return success({ image: WORKSPACE_IMAGE });
@@ -1671,17 +1741,41 @@ function buildNodeRuntimeRecipe(globalConfiguration) {
1671
1741
  }
1672
1742
  return buildNodeRuntime;
1673
1743
  }
1744
+ function currentHostIdentity() {
1745
+ const userId = typeof process.getuid === "function" ? process.getuid() : void 0;
1746
+ const groupId = typeof process.getgid === "function" ? process.getgid() : void 0;
1747
+ if (typeof userId !== "number" || typeof groupId !== "number" || !Number.isSafeInteger(userId) || !Number.isSafeInteger(groupId) || userId <= 0 || groupId <= 0) return failure({
1748
+ kind: "validation",
1749
+ code: "invalid-host-identity",
1750
+ observed: "The invoking host process must have a non-root POSIX numeric UID and GID.",
1751
+ nextAction: "Run devbox build as the intended non-root host user."
1752
+ });
1753
+ return success({
1754
+ USER_ID: String(userId),
1755
+ GROUP_ID: String(groupId)
1756
+ });
1757
+ }
1674
1758
  function renderBuildDockerfile(input) {
1675
1759
  const lines = [
1676
1760
  "# Generated by devbox build. Machine-owned Workspace build context.",
1677
1761
  "FROM ubuntu:24.04",
1678
1762
  "",
1763
+ "ARG USER_ID",
1764
+ "ARG GROUP_ID",
1765
+ "",
1679
1766
  "ENV DEBIAN_FRONTEND=noninteractive",
1680
1767
  "ENV TZ=UTC",
1681
1768
  "ENV LANG=C.UTF-8",
1769
+ "ENV TERM=xterm-256color",
1770
+ "ENV EDITOR=vim",
1771
+ "ENV VISUAL=vim",
1682
1772
  "",
1683
1773
  "RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone",
1684
1774
  "",
1775
+ "RUN echo \"Acquire::http::Pipeline-Depth 0;\" > /etc/apt/apt.conf.d/99custom && \\",
1776
+ " echo \"Acquire::http::No-Cache true;\" >> /etc/apt/apt.conf.d/99custom && \\",
1777
+ " echo \"Acquire::BrokenProxy true;\" >> /etc/apt/apt.conf.d/99custom",
1778
+ "",
1685
1779
  "# Install the Base profile packages and build tools",
1686
1780
  "RUN set -eux \\",
1687
1781
  " && apt-get update && apt-get upgrade -y \\",
@@ -1697,8 +1791,8 @@ function renderBuildDockerfile(input) {
1697
1791
  "",
1698
1792
  "# Create the devbox user and group",
1699
1793
  "RUN userdel -r ubuntu \\",
1700
- " && groupadd -f -g 1000 devbox \\",
1701
- " && useradd -m -s /bin/bash -N -g 1000 -u 1000 devbox \\",
1794
+ " && groupadd -g \"$GROUP_ID\" devbox \\",
1795
+ " && useradd -m -s /bin/bash -N -g \"$GROUP_ID\" -u \"$USER_ID\" devbox \\",
1702
1796
  " && echo \"devbox ALL=(ALL) NOPASSWD:ALL\" > /etc/sudoers.d/devbox \\",
1703
1797
  " && chmod 0440 /etc/sudoers.d/devbox",
1704
1798
  ""
@@ -1767,11 +1861,7 @@ function selectBuildContextAssets(agents) {
1767
1861
  return assets;
1768
1862
  }
1769
1863
  function renderEntrypoint(input) {
1770
- const lines = [
1771
- "#!/bin/sh",
1772
- "set -eu",
1773
- ""
1774
- ];
1864
+ const lines = ["#!/usr/bin/env bash", ""];
1775
1865
  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
1866
  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
1867
  lines.push("if [ $# -gt 0 ]; then", " exec gosu devbox \"$@\"", "else", " exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf", "fi");
@@ -1834,7 +1924,7 @@ function renderNodeRuntimeStage(recipe) {
1834
1924
  " && export PATH=\"$NODE_RUNTIME_ROOT/bin:$PATH\" \\",
1835
1925
  " && npm install -g npm \\",
1836
1926
  " && npm uninstall -g corepack \\",
1837
- " && npm install -g yarn pnpm @antfu/ni \\",
1927
+ " && npm install -g yarn pnpm @antfu/ni --allow-scripts=pnpm,yarn \\",
1838
1928
  " && \"$NODE_RUNTIME_ROOT/bin/node\" --version \\",
1839
1929
  " && \"$NODE_RUNTIME_ROOT/bin/npm\" --version \\",
1840
1930
  " && rm -rf /tmp/node-source",
@@ -1845,6 +1935,10 @@ async function runDockerBuild(invocation, signal) {
1845
1935
  const build = spawn("docker", [
1846
1936
  "build",
1847
1937
  ...invocation.noCache ? ["--no-cache"] : [],
1938
+ "--build-arg",
1939
+ `USER_ID=${invocation.buildArgs.USER_ID}`,
1940
+ "--build-arg",
1941
+ `GROUP_ID=${invocation.buildArgs.GROUP_ID}`,
1848
1942
  "--tag",
1849
1943
  invocation.image,
1850
1944
  "--file",
@@ -1953,12 +2047,15 @@ function createCli(signal, interactive) {
1953
2047
  for (const [command, description] of [
1954
2048
  ["up", "Start the current Project Sandbox."],
1955
2049
  ["down", "Stop and remove the current Project Sandbox."],
1956
- ["stop", "Stop the current Project Sandbox."],
1957
- ["sh", "Open a shell in the current Project Sandbox."]
2050
+ ["stop", "Stop the current Project Sandbox."]
1958
2051
  ]) cli.command(command, description).action(async () => {
1959
2052
  const result = await runSandboxLifecycle(command, { signal });
1960
2053
  return result.ok ? success({}) : result;
1961
2054
  });
2055
+ cli.command("sh", "Open a shell in the current Project Sandbox.").action(async () => {
2056
+ const result = await runSandboxShell({ signal });
2057
+ return result.ok ? success({}) : result;
2058
+ });
1962
2059
  cli.help();
1963
2060
  return cli;
1964
2061
  }
@@ -1985,11 +2082,18 @@ async function main() {
1985
2082
  return 130;
1986
2083
  }
1987
2084
  if (error instanceof HostCommandError) return error.exitCode;
2085
+ if (isCacError(error)) {
2086
+ process.stderr.write(`${error.message}\n`);
2087
+ return 2;
2088
+ }
1988
2089
  throw error;
1989
2090
  } finally {
1990
2091
  process.off("SIGINT", interrupt);
1991
2092
  }
1992
2093
  }
2094
+ function isCacError(error) {
2095
+ return error instanceof Error && error.name === "CACError";
2096
+ }
1993
2097
  function interactiveFailure(observed, nextAction) {
1994
2098
  return failure({
1995
2099
  kind: "usage",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycs77/devbox",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "A Lucas Yang's opinionated devbox for AI agents",
5
5
  "keywords": [
6
6
  "agent",