@ycs77/devbox 0.5.1 → 0.6.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 +249 -147
  2. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -10,6 +10,9 @@ import { execFile, spawn } from "node:child_process";
10
10
  import { promisify } from "node:util";
11
11
  import { parse as parse$1, parseDocument, stringify } from "yaml";
12
12
  import { once } from "node:events";
13
+ //#region package.json
14
+ var version = "0.6.0";
15
+ //#endregion
13
16
  //#region src/catalog/index.ts
14
17
  const NODE_RELEASE_KEYS = [
15
18
  "5BE8A3F6C8A5C01D106C0AD820B1A390B168D356",
@@ -27,12 +30,14 @@ const PACKAGED_NODE_RECIPES = {
27
30
  releaseLine: "24",
28
31
  version: "24.19.0",
29
32
  runtimeRoot: "/opt/devbox/runtimes/node/24",
33
+ distributionBaseUrl: "https://nodejs.org/dist",
30
34
  trustedReleaseKeys: NODE_RELEASE_KEYS
31
35
  },
32
36
  "22": {
33
37
  releaseLine: "22",
34
38
  version: "22.23.2",
35
39
  runtimeRoot: "/opt/devbox/runtimes/node/22",
40
+ distributionBaseUrl: "https://nodejs.org/dist",
36
41
  trustedReleaseKeys: NODE_RELEASE_KEYS
37
42
  }
38
43
  };
@@ -467,10 +472,7 @@ function isMissingFileError$1(error) {
467
472
  return typeof error === "object" && error !== null && error.code === "ENOENT";
468
473
  }
469
474
  //#endregion
470
- //#region src/workspace/image.ts
471
- const WORKSPACE_IMAGE = "devbox-workspace:latest";
472
- //#endregion
473
- //#region src/project/compose-fragments.ts
475
+ //#region src/agent/compose.ts
474
476
  const NOTIFICATION_FRAGMENT = {
475
477
  environment: { PULSE_SERVER: "unix:/tmp/pulse-socket" },
476
478
  volume: {
@@ -479,7 +481,7 @@ const NOTIFICATION_FRAGMENT = {
479
481
  readOnly: true
480
482
  }
481
483
  };
482
- function selectComposeFragments({ selectedNode, configuredAgents, agentNotifications }) {
484
+ function selectAgentComposeFragments(configuredAgents, notificationsEnabled) {
483
485
  const agents = [];
484
486
  let hasNotificationAgent = false;
485
487
  for (const agent of configuredAgents) {
@@ -497,18 +499,26 @@ function selectComposeFragments({ selectedNode, configuredAgents, agentNotificat
497
499
  }
498
500
  }
499
501
  return {
500
- node: selectedNode === null ? void 0 : { environment: { NODE_VERSION: selectedNode } },
501
502
  agents,
502
- notification: agentNotifications && hasNotificationAgent ? NOTIFICATION_FRAGMENT : void 0
503
+ notification: notificationsEnabled && hasNotificationAgent ? NOTIFICATION_FRAGMENT : void 0
503
504
  };
504
505
  }
505
506
  //#endregion
507
+ //#region src/runtimes/node/compose.ts
508
+ function selectNodeComposeFragment(selectedNode) {
509
+ return selectedNode === null ? void 0 : { environment: { NODE_VERSION: selectedNode } };
510
+ }
511
+ //#endregion
512
+ //#region src/workspace/image.ts
513
+ const WORKSPACE_IMAGE = "devbox-workspace:latest";
514
+ //#endregion
506
515
  //#region src/project/compose.ts
507
516
  function renderProjectCompose(input) {
508
- const fragments = selectComposeFragments(input);
517
+ const node = selectNodeComposeFragment(input.selectedNode);
518
+ const agent = selectAgentComposeFragments(input.configuredAgents, input.agentNotifications);
509
519
  const environment = {
510
- ...fragments.node?.environment,
511
- ...fragments.notification?.environment
520
+ ...node?.environment,
521
+ ...agent.notification?.environment
512
522
  };
513
523
  const volumes = [
514
524
  {
@@ -516,7 +526,7 @@ function renderProjectCompose(input) {
516
526
  source: input.projectRoot,
517
527
  target: `/workspace/${input.sandboxName}`
518
528
  },
519
- ...fragments.agents.map((fragment) => ({
529
+ ...agent.agents.map((fragment) => ({
520
530
  type: "volume",
521
531
  source: fragment.volume.name,
522
532
  target: fragment.volume.target
@@ -526,11 +536,11 @@ function renderProjectCompose(input) {
526
536
  source: input.claudeHostConfiguration,
527
537
  target: "/home/devbox/.claude.json"
528
538
  }],
529
- ...fragments.notification === void 0 ? [] : [{
539
+ ...agent.notification === void 0 ? [] : [{
530
540
  type: "bind",
531
- source: fragments.notification.volume.source,
532
- target: fragments.notification.volume.target,
533
- read_only: fragments.notification.volume.readOnly
541
+ source: agent.notification.volume.source,
542
+ target: agent.notification.volume.target,
543
+ read_only: agent.notification.volume.readOnly
534
544
  }]
535
545
  ];
536
546
  const definition = {
@@ -550,7 +560,7 @@ function renderProjectCompose(input) {
550
560
  ...Object.keys(environment).length === 0 ? {} : { environment },
551
561
  volumes
552
562
  } },
553
- ...fragments.agents.length === 0 ? {} : { volumes: Object.fromEntries(fragments.agents.map((fragment) => [fragment.volume.name, {
563
+ ...agent.agents.length === 0 ? {} : { volumes: Object.fromEntries(agent.agents.map((fragment) => [fragment.volume.name, {
554
564
  name: fragment.volume.name,
555
565
  external: fragment.volume.external
556
566
  }])) }
@@ -1642,34 +1652,205 @@ function showLogo() {
1642
1652
  for (let i = 0; i < ASCII_ART.length; i++) console.log(`${COLORS[i]}${ASCII_ART[i]}${REST}`);
1643
1653
  }
1644
1654
  //#endregion
1645
- //#region \0devbox-workspace-image-default:.bash_aliases.mjs
1646
- var _devbox_workspace_image_default__bash_aliases_default = "alias ..='cd ..'\nalias ...='cd ../..'\nalias c='clear'\nalias g='git'\nalias ccc='claude --dangerously-skip-permissions'\nalias ccx='codex --sandbox workspace-write --ask-for-approval never'\nalias ggg='agy --dangerously-skip-permissions'\n\nfunction mkcd() {\n mkdir \"$1\"\n cd \"$1\"\n}\n\n# Laravel Artisan\nalias pa='php artisan'\nalias pav='php artisan serve'\nalias pam='php artisan migrate'\nalias pams='php artisan migrate --seed'\nalias pamr='php artisan migrate:rollback'\nalias pamf='php artisan migrate:fresh'\nalias pamfs='php artisan migrate:fresh --seed'\nalias pas='php artisan db:seed'\nalias par='php artisan route:list'\nalias paq='php artisan queue:work'\nalias pah='php artisan horizon'\nalias pat='php artisan tinker'\nalias phpunit='./vendor/bin/phpunit --colors=always'\nalias pu='phpunit'\nalias pest='./vendor/bin/pest --colors=always'\nalias p='pest'\nalias pint='./vendor/bin/pint'\nalias phpstan='./vendor/bin/phpstan analyse --ansi'\nalias rector='./vendor/bin/rector'\n\n# NPM (requires @antfu/ni)\nalias nv='nr dev' # nd exists in @antfu/ni\nalias nb='nr build'\nalias nll='nr lint' # nl is a Linux command\nalias nlf='nr lint --fix'\nalias nf='nr fmt'\nalias nfc='nr fmt --check'\nalias nc='nr check'\nalias nt='nr test'\nalias ntr='nr test --run'\n";
1647
- //#endregion
1648
- //#region \0devbox-workspace-image-default:.claude/settings.json.mjs
1655
+ //#region \0devbox-image-default:/home/runner/work/devbox/devbox/src/agent/image-defaults/.claude/settings.json.mjs
1649
1656
  var settings_json_default = "{\n \"$schema\": \"https://json.schemastore.org/claude-code-settings.json\",\n \"env\": {\n \"CLAUDE_CODE_USE_POWERSHELL_TOOL\": \"0\"\n },\n \"attribution\": {\n \"commit\": \"\",\n \"pr\": \"\"\n },\n \"includeGitInstructions\": false,\n \"autoMemoryEnabled\": false\n}\n";
1650
1657
  //#endregion
1651
- //#region \0devbox-workspace-image-default:.gitconfig.mjs
1652
- var _devbox_workspace_image_default__gitconfig_default = "[core]\n autocrlf = input\n fileMode = false\n pager = less -r\n quotepath = false\n[init]\n defaultBranch = main\n[color]\n diff = auto\n status = auto\n branch = auto\n[alias]\n cf = config\n cfl = config --list\n s = status\n a = add .\n l = log --oneline --graph\n la = log --oneline --graph --all\n last = log -1 HEAD\n c = commit\n cm = commit -m\n ca = commit --amend -m\n can = commit --amend --no-edit\n co = checkout\n cp = cherry-pick\n b = branch\n m = merge\n mnf = merge --no-ff\n rs = reset\n rs0h = reset HEAD --hard\n rs1 = reset HEAD~1\n rs1h = reset HEAD~1 --hard\n rb = rebase\n rbo = rebase --onto\n rbi = rebase -i\n rbc = rebase --continue\n rba = rebase --abort\n rm = remote\n r = remote -v\n ra = remote add\n rr = remote remove\n rp = remote prune\n f = fetch\n fp = fetch --prune\n p = push\n pl = pull\n";
1653
- //#endregion
1654
- //#region \0devbox-workspace-image-default:.omp/agent/config.yml.mjs
1658
+ //#region \0devbox-image-default:/home/runner/work/devbox/devbox/src/agent/image-defaults/.omp/agent/config.yml.mjs
1655
1659
  var config_yml_default = "setupVersion: 2\n\n# Basic\nmodelRoles:\n default: openai-codex/gpt-5.6-terra:high\n smol: openai-codex/gpt-5.6-luna:high\n slow: openai-codex/gpt-5.6-sol:max\n plan: openai-codex/gpt-5.6-sol:max\nsymbolPreset: unicode\n\n# Theme\ntheme:\n dark: titanium\n light: light\ncomposer:\n shape: claude\n\n# Auto QA\ndev:\n autoqa: false\n autoqaConsent: denied\n";
1656
1660
  //#endregion
1661
+ //#region src/agent/build.ts
1662
+ function createAgentWorkspaceContribution(input) {
1663
+ const agents = input.configuredAgents.map((name) => ({
1664
+ name,
1665
+ recipe: PACKAGED_AGENTS[name]
1666
+ }));
1667
+ const buildContextAssets = selectBuildContextAssets(agents);
1668
+ const skillAgents = agents.filter((agent) => agent.recipe.supportsSkillInstallation).map((agent) => agent.name);
1669
+ return {
1670
+ buildContextAssets,
1671
+ dockerignoreEntries: selectDockerignoreEntries(buildContextAssets),
1672
+ installationDockerfileLines: renderAgentInstallations(agents, input.buildNodeRuntimeRoot, skillAgents),
1673
+ assetDockerfileLines: renderAgentAssets(agents),
1674
+ notificationDockerfileLines: renderAgentNotifications(agents, input.notificationsEnabled),
1675
+ entrypointSetup: renderAgentEntrypointSetup(agents)
1676
+ };
1677
+ }
1678
+ function renderAgentInstallations(agents, buildNodeRuntimeRoot, skillAgents) {
1679
+ const lines = [];
1680
+ for (const agent of agents) {
1681
+ const preInstallCommands = agent.recipe.installation.preInstallCommands ?? [];
1682
+ 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}`, "");
1683
+ }
1684
+ if (agents.length > 0) lines.push("# Set the PATH to include AI tools", "ENV PATH=\"/home/devbox/.local/bin:${PATH}\"", "");
1685
+ if (buildNodeRuntimeRoot !== void 0 && skillAgents.length > 0) {
1686
+ const skillAgentArguments = skillAgents.map((agent) => `-a ${agent}`).join(" ");
1687
+ lines.push("# Install Agent Skills", "RUN set -eux \\", " && mkdir -p /home/devbox/.agents/skills \\", ` && export PATH="${buildNodeRuntimeRoot}/bin:$PATH" \\`, ` && npx -y skills add ycs77/skills -g ${skillAgentArguments} -s '*' -y`, "");
1688
+ }
1689
+ return lines;
1690
+ }
1691
+ function selectDockerignoreEntries(assets) {
1692
+ const entries = [];
1693
+ if (assets.some((asset) => asset.relativePath === ".claude/settings.json")) entries.push("!.claude", "!.claude/settings.json");
1694
+ if (assets.some((asset) => asset.relativePath === ".omp/agent/config.yml")) entries.push("!.omp", "!.omp/agent", "!.omp/agent/config.yml");
1695
+ return entries;
1696
+ }
1697
+ function selectBuildContextAssets(agents) {
1698
+ const assets = [];
1699
+ if (agents.some((agent) => agent.name === "claude-code")) assets.push({
1700
+ relativePath: ".claude/settings.json",
1701
+ content: settings_json_default
1702
+ });
1703
+ if (agents.some((agent) => agent.name === "omp")) assets.push({
1704
+ relativePath: ".omp/agent/config.yml",
1705
+ content: config_yml_default
1706
+ });
1707
+ return assets;
1708
+ }
1709
+ function renderAgentAssets(agents) {
1710
+ const hasClaudeCode = agents.some((agent) => agent.name === "claude-code");
1711
+ const hasOmp = agents.some((agent) => agent.name === "omp");
1712
+ if (!hasClaudeCode && !hasOmp) return [];
1713
+ const ownershipCommands = [];
1714
+ const lines = ["# Copy AI dotfiles"];
1715
+ if (hasClaudeCode) {
1716
+ lines.push("COPY .claude/settings.json /home/devbox/.claude/settings.json");
1717
+ ownershipCommands.push("chown devbox:devbox /home/devbox/.claude/settings.json");
1718
+ }
1719
+ if (hasOmp) {
1720
+ lines.push("COPY .omp/agent/config.yml /home/devbox/.omp/agent/config.yml");
1721
+ ownershipCommands.push("chown devbox:devbox /home/devbox/.omp/agent", "chown devbox:devbox /home/devbox/.omp/agent/config.yml");
1722
+ }
1723
+ lines.push(`RUN ${ownershipCommands.join(" \\\n && ")}`, "");
1724
+ return lines;
1725
+ }
1726
+ function renderAgentNotifications(agents, notificationsEnabled) {
1727
+ const commands = notificationsEnabled ? agents.flatMap((agent) => agent.recipe.supportsNotifications ? agent.recipe.notificationInstallationCommands : []) : [];
1728
+ if (commands.length === 0) return [];
1729
+ return [
1730
+ "# Install Agent Notification Plugins",
1731
+ "USER devbox",
1732
+ "RUN set -eux \\",
1733
+ ...commands.map((command, index) => ` && ${command}${index < commands.length - 1 ? " \\" : ""}`),
1734
+ "USER root",
1735
+ ""
1736
+ ];
1737
+ }
1738
+ function renderAgentEntrypointSetup(agents) {
1739
+ if (!agents.some((agent) => agent.name === "agy")) return [];
1740
+ return [
1741
+ "# Link shared Agent Skills",
1742
+ "if [ ! -L \"/home/devbox/.gemini/skills\" ]; then",
1743
+ " ln -sfn /home/devbox/.agents/skills /home/devbox/.gemini/skills",
1744
+ " chown -h devbox:devbox /home/devbox/.gemini/skills",
1745
+ "fi",
1746
+ ""
1747
+ ];
1748
+ }
1749
+ //#endregion
1750
+ //#region src/runtimes/node/build.ts
1751
+ function createNodeWorkspaceContribution(configuredReleaseLines) {
1752
+ const runtimes = configuredReleaseLines.map((releaseLine) => PACKAGED_NODE_RECIPES[releaseLine]);
1753
+ const buildRuntime = selectBuildNodeRuntime(runtimes);
1754
+ return {
1755
+ buildNodeRuntimeRoot: buildRuntime?.runtimeRoot,
1756
+ runtimeInstallations: runtimes.flatMap(renderNodeRuntimeStage),
1757
+ sandboxUserSetup: buildRuntime === void 0 ? [] : [
1758
+ "# Configure pnpm for the non-root Sandbox user",
1759
+ "RUN mkdir -p /home/devbox/.config/pnpm /home/devbox/.pnpm-store \\",
1760
+ " && printf 'storeDir: /home/devbox/.pnpm-store\\n' > /home/devbox/.config/pnpm/config.yaml \\",
1761
+ " && chown devbox:devbox /home/devbox/.config/pnpm/config.yaml",
1762
+ ""
1763
+ ],
1764
+ entrypointSetup: renderNodeEntrypointSetup(runtimes)
1765
+ };
1766
+ }
1767
+ function selectBuildNodeRuntime(runtimes) {
1768
+ let buildRuntime;
1769
+ for (const runtime of runtimes) if (buildRuntime === void 0 || Number.parseFloat(runtime.releaseLine) > Number.parseFloat(buildRuntime.releaseLine)) buildRuntime = runtime;
1770
+ return buildRuntime;
1771
+ }
1772
+ function renderNodeEntrypointSetup(runtimes) {
1773
+ if (runtimes.length === 0) return [];
1774
+ return [
1775
+ "# Set the Node.js release line to use",
1776
+ "NODE_VERSION=\"${NODE_VERSION:-}\"",
1777
+ "if [ -n \"$NODE_VERSION\" ]; then",
1778
+ " case \"$NODE_VERSION\" in",
1779
+ " *[!0-9]*)",
1780
+ " echo \"NODE_VERSION must be a numeric release line: $NODE_VERSION\" >&2",
1781
+ " exit 1",
1782
+ " ;;",
1783
+ " esac",
1784
+ "",
1785
+ " NODE_RUNTIME_ROOT=\"/opt/devbox/runtimes/node/$NODE_VERSION\"",
1786
+ " if [ ! -x \"$NODE_RUNTIME_ROOT/bin/node\" ]; then",
1787
+ " echo \"Node.js release line $NODE_VERSION is not installed.\" >&2",
1788
+ " exit 1",
1789
+ " fi",
1790
+ "",
1791
+ " if ! grep -q \"# Devbox\" /etc/bash.bashrc; then",
1792
+ " printf \"\\n# Devbox\\nexport PATH=\\\"$NODE_RUNTIME_ROOT/bin:\\$PATH\\\"\\n\" >> /etc/bash.bashrc",
1793
+ " fi",
1794
+ "",
1795
+ " export PATH=\"$NODE_RUNTIME_ROOT/bin:$PATH\"",
1796
+ "fi",
1797
+ ""
1798
+ ];
1799
+ }
1800
+ function renderNodeRuntimeStage(recipe) {
1801
+ const keys = recipe.trustedReleaseKeys.map((key) => ` ${key}`).join(" \\\n");
1802
+ return [
1803
+ `# Install Node.js ${recipe.releaseLine}`,
1804
+ "RUN ARCH= OPENSSL_ARCH= && dpkgArch=\"$(dpkg --print-architecture)\" \\",
1805
+ " && case \"${dpkgArch##*-}\" in \\",
1806
+ " amd64) ARCH='x64' OPENSSL_ARCH='linux-x86_64';; \\",
1807
+ " ppc64el) ARCH='ppc64le' OPENSSL_ARCH='linux-ppc64le';; \\",
1808
+ " s390x) ARCH='s390x' OPENSSL_ARCH='linux*-s390x';; \\",
1809
+ " arm64) ARCH='arm64' OPENSSL_ARCH='linux-aarch64';; \\",
1810
+ " *) echo \"unsupported architecture\"; exit 1 ;; \\",
1811
+ " esac \\",
1812
+ " && set -eux \\",
1813
+ ` && NODE_RUNTIME_VERSION=${recipe.version} \\`,
1814
+ ` && NODE_RUNTIME_ROOT=${recipe.runtimeRoot} \\`,
1815
+ " && mkdir -p \"$NODE_RUNTIME_ROOT\" /tmp/node-source \\",
1816
+ " && cd /tmp/node-source \\",
1817
+ " && export GNUPGHOME=\"$(mktemp -d)\" \\",
1818
+ " && for key in \\",
1819
+ `${keys} \\`,
1820
+ " ; do \\",
1821
+ " { gpg --batch --no-options --keyserver hkps://keys.openpgp.org --recv-keys \"$key\" && gpg --batch --no-options --fingerprint \"$key\"; } || \\",
1822
+ " { gpg --batch --no-options --keyserver keyserver.ubuntu.com --recv-keys \"$key\" && gpg --batch --no-options --fingerprint \"$key\"; }; \\",
1823
+ " done \\",
1824
+ " && archive=\"node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\" \\",
1825
+ ` && curl -fsSLO --compressed "${recipe.distributionBaseUrl}/v\${NODE_RUNTIME_VERSION}/node-v\${NODE_RUNTIME_VERSION}-linux-\${ARCH}.tar.xz" \\`,
1826
+ ` && curl -fsSLO --compressed "${recipe.distributionBaseUrl}/v\${NODE_RUNTIME_VERSION}/SHASUMS256.txt.asc" \\`,
1827
+ " && gpg --batch --no-options --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \\",
1828
+ " && gpgconf --kill all \\",
1829
+ " && rm -rf \"$GNUPGHOME\" \\",
1830
+ " && grep \" node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\\$\" SHASUMS256.txt | sha256sum -c - \\",
1831
+ " && tar -xJf \"node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\" -C \"$NODE_RUNTIME_ROOT\" --strip-components=1 --no-same-owner \\",
1832
+ " && rm \"node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\" SHASUMS256.txt.asc SHASUMS256.txt \\",
1833
+ " && find \"$NODE_RUNTIME_ROOT/include/node/openssl/archs\" -mindepth 1 -maxdepth 1 ! -name \"$OPENSSL_ARCH\" -exec rm -rf {} \\; \\",
1834
+ " && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \\",
1835
+ " && export PATH=\"$NODE_RUNTIME_ROOT/bin:$PATH\" \\",
1836
+ " && npm install -g npm \\",
1837
+ " && npm uninstall -g corepack \\",
1838
+ " && npm install -g yarn pnpm @antfu/ni --allow-scripts=pnpm,yarn \\",
1839
+ " && \"$NODE_RUNTIME_ROOT/bin/node\" --version \\",
1840
+ " && \"$NODE_RUNTIME_ROOT/bin/npm\" --version \\",
1841
+ " && rm -rf /tmp/node-source",
1842
+ ""
1843
+ ];
1844
+ }
1845
+ //#endregion
1657
1846
  //#region src/workspace/build.ts
1658
1847
  const BASE_WORKSPACE_IMAGE_DEFAULTS = [{
1659
1848
  relativePath: ".bash_aliases",
1660
- content: _devbox_workspace_image_default__bash_aliases_default
1849
+ content: "alias ..='cd ..'\nalias ...='cd ../..'\nalias c='clear'\nalias g='git'\nalias ccc='claude --dangerously-skip-permissions'\nalias ccx='codex --sandbox workspace-write --ask-for-approval never'\nalias ggg='agy --dangerously-skip-permissions'\n\nfunction mkcd() {\n mkdir \"$1\"\n cd \"$1\"\n}\n\n# Laravel Artisan\nalias pa='php artisan'\nalias pav='php artisan serve'\nalias pam='php artisan migrate'\nalias pams='php artisan migrate --seed'\nalias pamr='php artisan migrate:rollback'\nalias pamf='php artisan migrate:fresh'\nalias pamfs='php artisan migrate:fresh --seed'\nalias pas='php artisan db:seed'\nalias par='php artisan route:list'\nalias paq='php artisan queue:work'\nalias pah='php artisan horizon'\nalias pat='php artisan tinker'\nalias phpunit='./vendor/bin/phpunit --colors=always'\nalias pu='phpunit'\nalias pest='./vendor/bin/pest --colors=always'\nalias p='pest'\nalias pint='./vendor/bin/pint'\nalias phpstan='./vendor/bin/phpstan analyse --ansi'\nalias rector='./vendor/bin/rector'\n\n# NPM (requires @antfu/ni)\nalias nv='nr dev' # nd exists in @antfu/ni\nalias nb='nr build'\nalias nll='nr lint' # nl is a Linux command\nalias nlf='nr lint --fix'\nalias nf='nr fmt'\nalias nfc='nr fmt --check'\nalias nc='nr check'\nalias nt='nr test'\nalias ntr='nr test --run'\n"
1661
1850
  }, {
1662
1851
  relativePath: ".gitconfig",
1663
- content: _devbox_workspace_image_default__gitconfig_default
1852
+ content: "[core]\n autocrlf = input\n fileMode = false\n pager = less -r\n quotepath = false\n[init]\n defaultBranch = main\n[color]\n diff = auto\n status = auto\n branch = auto\n[alias]\n cf = config\n cfl = config --list\n s = status\n a = add .\n l = log --oneline --graph\n la = log --oneline --graph --all\n last = log -1 HEAD\n c = commit\n cm = commit -m\n ca = commit --amend -m\n can = commit --amend --no-edit\n co = checkout\n cp = cherry-pick\n b = branch\n m = merge\n mnf = merge --no-ff\n rs = reset\n rs0h = reset HEAD --hard\n rs1 = reset HEAD~1\n rs1h = reset HEAD~1 --hard\n rb = rebase\n rbo = rebase --onto\n rbi = rebase -i\n rbc = rebase --continue\n rba = rebase --abort\n rm = remote\n r = remote -v\n ra = remote add\n rr = remote remove\n rp = remote prune\n f = fetch\n fp = fetch --prune\n p = push\n pl = pull\n"
1664
1853
  }];
1665
- const CLAUDE_SETTINGS_DEFAULT = {
1666
- relativePath: ".claude/settings.json",
1667
- content: settings_json_default
1668
- };
1669
- const OMP_AGENT_CONFIG_DEFAULT = {
1670
- relativePath: ".omp/agent/config.yml",
1671
- content: config_yml_default
1672
- };
1673
1854
  async function buildWorkspace(input = {}) {
1674
1855
  return withStateLocks({
1675
1856
  devboxHome: input.devboxHome ?? join(homedir(), ".devbox"),
@@ -1699,27 +1880,25 @@ async function buildWorkspaceUnlocked(input) {
1699
1880
  const globalConfiguration = parsedGlobal.value;
1700
1881
  const hostIdentity = currentHostIdentity();
1701
1882
  if (!hostIdentity.ok) return hostIdentity;
1702
- const agents = globalConfiguration.agent.map((name) => ({
1703
- name,
1704
- recipe: PACKAGED_AGENTS[name]
1705
- }));
1706
- const buildContextAssets = selectBuildContextAssets(agents);
1707
- const nodeRuntimes = globalConfiguration.node.map((releaseLine) => PACKAGED_NODE_RECIPES[releaseLine]);
1883
+ const node = createNodeWorkspaceContribution(globalConfiguration.node);
1884
+ const agent = createAgentWorkspaceContribution({
1885
+ configuredAgents: globalConfiguration.agent,
1886
+ notificationsEnabled: globalConfiguration.agent_notifications,
1887
+ buildNodeRuntimeRoot: node.buildNodeRuntimeRoot
1888
+ });
1889
+ const buildContextAssets = [...BASE_WORKSPACE_IMAGE_DEFAULTS, ...agent.buildContextAssets];
1708
1890
  const dockerfile = renderBuildDockerfile({
1709
- nodeRuntimes,
1710
- buildNodeRuntime: buildNodeRuntimeRecipe(globalConfiguration),
1711
- agents,
1712
- agentNotifications: globalConfiguration.agent_notifications,
1713
- skillAgents: globalConfiguration.agent.filter((agent) => PACKAGED_AGENTS[agent]?.supportsSkillInstallation === true)
1891
+ node,
1892
+ agent
1714
1893
  });
1715
1894
  await mkdir(paths.buildContext, { recursive: true });
1716
1895
  await seedBuildContextAssets(paths.buildContext, buildContextAssets);
1717
1896
  await Promise.all([
1718
1897
  writeFile(join(paths.buildContext, "Dockerfile"), dockerfile),
1719
- writeFile(join(paths.buildContext, ".dockerignore"), renderDockerignore(buildContextAssets)),
1898
+ writeFile(join(paths.buildContext, ".dockerignore"), renderDockerignore(buildContextAssets, agent.dockerignoreEntries)),
1720
1899
  writeFile(join(paths.buildContext, "entrypoint.sh"), renderEntrypoint({
1721
- nodeRuntimes,
1722
- agents
1900
+ node,
1901
+ agent
1723
1902
  })),
1724
1903
  writeFile(join(paths.buildContext, "supervisord.conf"), renderSupervisorConfiguration())
1725
1904
  ]);
@@ -1733,14 +1912,6 @@ async function buildWorkspaceUnlocked(input) {
1733
1912
  if (!buildResult.ok) return buildResult;
1734
1913
  return success({ image: WORKSPACE_IMAGE });
1735
1914
  }
1736
- function buildNodeRuntimeRecipe(globalConfiguration) {
1737
- let buildNodeRuntime;
1738
- for (const releaseLine of globalConfiguration.node) {
1739
- const recipe = PACKAGED_NODE_RECIPES[releaseLine];
1740
- if (buildNodeRuntime === void 0 || Number.parseFloat(releaseLine) > Number.parseFloat(buildNodeRuntime.releaseLine)) buildNodeRuntime = recipe;
1741
- }
1742
- return buildNodeRuntime;
1743
- }
1744
1915
  function currentHostIdentity() {
1745
1916
  const userId = typeof process.getuid === "function" ? process.getuid() : void 0;
1746
1917
  const groupId = typeof process.getgid === "function" ? process.getgid() : void 0;
@@ -1797,51 +1968,26 @@ function renderBuildDockerfile(input) {
1797
1968
  " && chmod 0440 /etc/sudoers.d/devbox",
1798
1969
  ""
1799
1970
  ];
1800
- for (const recipe of input.nodeRuntimes) lines.push(...renderNodeRuntimeStage(recipe));
1971
+ lines.push(...input.node.runtimeInstallations);
1801
1972
  lines.push("WORKDIR /workspace", "USER devbox", "");
1802
- 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", "");
1803
- for (const agent of input.agents) {
1804
- const preInstallCommands = agent.recipe.installation.preInstallCommands ?? [];
1805
- 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}`, "");
1806
- }
1807
- if (input.agents.length > 0) lines.push("# Set the PATH to include AI tools", "ENV PATH=\"/home/devbox/.local/bin:${PATH}\"", "");
1808
- if (input.buildNodeRuntime !== void 0 && input.skillAgents.length > 0) {
1809
- const skillAgentArguments = input.skillAgents.map((agent) => `-a ${agent}`).join(" ");
1810
- lines.push("# Install Agent Skills", "RUN set -eux \\", " && mkdir -p /home/devbox/.agents/skills \\", ` && export PATH="${input.buildNodeRuntime.runtimeRoot}/bin:$PATH" \\`, ` && npx -y skills add ycs77/skills -g ${skillAgentArguments} -s '*' -y`, "");
1811
- }
1973
+ lines.push(...input.node.sandboxUserSetup);
1974
+ lines.push(...input.agent.installationDockerfileLines);
1812
1975
  lines.push("USER root", "", "# Copy dotfiles", "COPY .bash_aliases /home/devbox/.bash_aliases", "COPY .gitconfig /home/devbox/.gitconfig", "RUN chown devbox:devbox /home/devbox/.bash_aliases \\", " && chown devbox:devbox /home/devbox/.gitconfig", "");
1813
- const hasClaudeCode = input.agents.some((agent) => agent.name === "claude-code");
1814
- const hasOmp = input.agents.some((agent) => agent.name === "omp");
1815
- if (hasClaudeCode || hasOmp) {
1816
- const ownershipCommands = [];
1817
- lines.push("# Copy AI dotfiles");
1818
- if (hasClaudeCode) {
1819
- lines.push("COPY .claude/settings.json /home/devbox/.claude/settings.json");
1820
- ownershipCommands.push("chown devbox:devbox /home/devbox/.claude/settings.json");
1821
- }
1822
- if (hasOmp) {
1823
- lines.push("COPY .omp/agent/config.yml /home/devbox/.omp/agent/config.yml");
1824
- ownershipCommands.push("chown devbox:devbox /home/devbox/.omp/agent", "chown devbox:devbox /home/devbox/.omp/agent/config.yml");
1825
- }
1826
- lines.push(`RUN ${ownershipCommands.join(" \\\n && ")}`, "");
1827
- }
1828
- const notificationInstallationCommands = input.agentNotifications ? input.agents.flatMap((agent) => agent.recipe.supportsNotifications ? agent.recipe.notificationInstallationCommands : []) : [];
1829
- 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", "");
1976
+ lines.push(...input.agent.assetDockerfileLines);
1977
+ lines.push(...input.agent.notificationDockerfileLines);
1830
1978
  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\"]");
1831
1979
  return `${lines.join("\n")}\n`;
1832
1980
  }
1833
- function renderDockerignore(assets) {
1834
- const lines = [
1981
+ function renderDockerignore(assets, additionalEntries) {
1982
+ return `${[
1835
1983
  "# Machine-owned Workspace build context",
1836
1984
  "*",
1837
1985
  "!Dockerfile",
1838
1986
  "!entrypoint.sh",
1839
1987
  "!supervisord.conf",
1840
- ...assets.map((asset) => `!${asset.relativePath}`)
1841
- ];
1842
- if (assets.some((asset) => asset.relativePath === ".claude/settings.json")) lines.push("!.claude", "!.claude/settings.json");
1843
- if (assets.some((asset) => asset.relativePath === ".omp/agent/config.yml")) lines.push("!.omp", "!.omp/agent", "!.omp/agent/config.yml");
1844
- return `${lines.join("\n")}\n`;
1988
+ ...assets.map((asset) => `!${asset.relativePath}`),
1989
+ ...additionalEntries
1990
+ ].join("\n")}\n`;
1845
1991
  }
1846
1992
  async function seedBuildContextAssets(buildContext, assets) {
1847
1993
  await Promise.all(assets.map(async (asset) => {
@@ -1854,18 +2000,18 @@ async function seedBuildContextAssets(buildContext, assets) {
1854
2000
  }
1855
2001
  }));
1856
2002
  }
1857
- function selectBuildContextAssets(agents) {
1858
- const assets = [...BASE_WORKSPACE_IMAGE_DEFAULTS];
1859
- if (agents.some((agent) => agent.name === "claude-code")) assets.push(CLAUDE_SETTINGS_DEFAULT);
1860
- if (agents.some((agent) => agent.name === "omp")) assets.push(OMP_AGENT_CONFIG_DEFAULT);
1861
- return assets;
1862
- }
1863
2003
  function renderEntrypoint(input) {
1864
- const lines = ["#!/usr/bin/env bash", ""];
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", "");
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", "");
1867
- lines.push("if [ $# -gt 0 ]; then", " exec gosu devbox \"$@\"", "else", " exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf", "fi");
1868
- return `${lines.join("\n")}\n`;
2004
+ return `${[
2005
+ "#!/usr/bin/env bash",
2006
+ "",
2007
+ ...input.node.entrypointSetup,
2008
+ ...input.agent.entrypointSetup,
2009
+ "if [ $# -gt 0 ]; then",
2010
+ " exec gosu devbox \"$@\"",
2011
+ "else",
2012
+ " exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf",
2013
+ "fi"
2014
+ ].join("\n")}\n`;
1869
2015
  }
1870
2016
  function renderSupervisorConfiguration() {
1871
2017
  return `${[
@@ -1886,51 +2032,6 @@ function renderSupervisorConfiguration() {
1886
2032
  "stderr_logfile=/dev/null"
1887
2033
  ].join("\n")}\n`;
1888
2034
  }
1889
- function renderNodeRuntimeStage(recipe) {
1890
- const keys = recipe.trustedReleaseKeys.map((key) => ` ${key}`).join(" \\\n");
1891
- return [
1892
- `# Install Node.js ${recipe.releaseLine}`,
1893
- "RUN ARCH= OPENSSL_ARCH= && dpkgArch=\"$(dpkg --print-architecture)\" \\",
1894
- " && case \"${dpkgArch##*-}\" in \\",
1895
- " amd64) ARCH='x64' OPENSSL_ARCH='linux-x86_64';; \\",
1896
- " ppc64el) ARCH='ppc64le' OPENSSL_ARCH='linux-ppc64le';; \\",
1897
- " s390x) ARCH='s390x' OPENSSL_ARCH='linux*-s390x';; \\",
1898
- " arm64) ARCH='arm64' OPENSSL_ARCH='linux-aarch64';; \\",
1899
- " *) echo \"unsupported architecture\"; exit 1 ;; \\",
1900
- " esac \\",
1901
- " && set -eux \\",
1902
- ` && NODE_RUNTIME_VERSION=${recipe.version} \\`,
1903
- ` && NODE_RUNTIME_ROOT=${recipe.runtimeRoot} \\`,
1904
- " && mkdir -p \"$NODE_RUNTIME_ROOT\" /tmp/node-source \\",
1905
- " && cd /tmp/node-source \\",
1906
- " && export GNUPGHOME=\"$(mktemp -d)\" \\",
1907
- " && for key in \\",
1908
- `${keys} \\`,
1909
- " ; do \\",
1910
- " { gpg --batch --no-options --keyserver hkps://keys.openpgp.org --recv-keys \"$key\" && gpg --batch --no-options --fingerprint \"$key\"; } || \\",
1911
- " { gpg --batch --no-options --keyserver keyserver.ubuntu.com --recv-keys \"$key\" && gpg --batch --no-options --fingerprint \"$key\"; }; \\",
1912
- " done \\",
1913
- " && archive=\"node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\" \\",
1914
- " && curl -fsSLO --compressed \"https://nodejs.org/dist/v${NODE_RUNTIME_VERSION}/node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\" \\",
1915
- " && curl -fsSLO --compressed \"https://nodejs.org/dist/v${NODE_RUNTIME_VERSION}/SHASUMS256.txt.asc\" \\",
1916
- " && gpg --batch --no-options --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \\",
1917
- " && gpgconf --kill all \\",
1918
- " && rm -rf \"$GNUPGHOME\" \\",
1919
- " && grep \" node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\\$\" SHASUMS256.txt | sha256sum -c - \\",
1920
- " && tar -xJf \"node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\" -C \"$NODE_RUNTIME_ROOT\" --strip-components=1 --no-same-owner \\",
1921
- " && rm \"node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\" SHASUMS256.txt.asc SHASUMS256.txt \\",
1922
- " && find \"$NODE_RUNTIME_ROOT/include/node/openssl/archs\" -mindepth 1 -maxdepth 1 ! -name \"$OPENSSL_ARCH\" -exec rm -rf {} \\; \\",
1923
- " && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \\",
1924
- " && export PATH=\"$NODE_RUNTIME_ROOT/bin:$PATH\" \\",
1925
- " && npm install -g npm \\",
1926
- " && npm uninstall -g corepack \\",
1927
- " && npm install -g yarn pnpm @antfu/ni --allow-scripts=pnpm,yarn \\",
1928
- " && \"$NODE_RUNTIME_ROOT/bin/node\" --version \\",
1929
- " && \"$NODE_RUNTIME_ROOT/bin/npm\" --version \\",
1930
- " && rm -rf /tmp/node-source",
1931
- ""
1932
- ];
1933
- }
1934
2035
  async function runDockerBuild(invocation, signal) {
1935
2036
  const build = spawn("docker", [
1936
2037
  "build",
@@ -2056,6 +2157,7 @@ function createCli(signal, interactive) {
2056
2157
  const result = await runSandboxShell({ signal });
2057
2158
  return result.ok ? success({}) : result;
2058
2159
  });
2160
+ cli.version(version);
2059
2161
  cli.help();
2060
2162
  return cli;
2061
2163
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycs77/devbox",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "A Lucas Yang's opinionated devbox for AI agents",
5
5
  "keywords": [
6
6
  "agent",