@ycs77/devbox 0.2.0 → 0.3.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 +5 -5
  2. package/dist/cli.mjs +195 -32
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # @ycs77/devbox
4
4
 
5
- **An AI-first, containerized development environment.**
5
+ **A containerized devbox for AI agents.**
6
6
 
7
7
  [![NPM version][ico-version]][link-npm]
8
8
  [![Software License][ico-license]](LICENSE)
@@ -17,13 +17,13 @@ English | [繁體中文](README-zh-TW.md)
17
17
 
18
18
  ## Overview
19
19
 
20
- Devbox gives each project an isolated Docker sandbox with Lucas's opinionated development setup. Choose a Node.js version and the AI agents you use, then work from a consistent environment without configuring containers yourself.
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.
21
21
 
22
22
  ## Features
23
23
 
24
- - **AI-first workspace:** built for AI-assisted development, with Claude Code, Codex, and more available during setup.
25
- - **Simple project lifecycle:** initialize, start, enter, stop, and remove a sandbox with a few commands.
26
- - **Fast Node.js switching:** configure Node.js 22 and 24, then select a version for each project.
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.
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
 
package/dist/cli.mjs CHANGED
@@ -1,5 +1,7 @@
1
- import { cac } from "cac";
2
1
  import * as p from "@clack/prompts";
2
+ import { intro, outro } from "@clack/prompts";
3
+ import { cac } from "cac";
4
+ import c from "picocolors";
3
5
  import { createHash, randomUUID } from "node:crypto";
4
6
  import { lstat, mkdir, readFile, rename, rm, rmdir, writeFile } from "node:fs/promises";
5
7
  import { homedir } from "node:os";
@@ -744,7 +746,7 @@ async function initializeProjectUnlocked(input = {}) {
744
746
  confirmed: true
745
747
  });
746
748
  }
747
- if (!await (input.confirm ?? input.prompt?.confirm ?? (async () => true))(`Register Project ${projectRoot} and write its configuration?`)) return success({
749
+ if (!await (input.confirm ?? input.prompt?.confirm ?? (async () => true))("Save configuration?")) return success({
748
750
  root: projectRoot,
749
751
  stateDirectory,
750
752
  sandboxName: name,
@@ -828,11 +830,13 @@ async function configureLocalProjectUnlocked(input = {}) {
828
830
  root: projectRoot,
829
831
  changed: false
830
832
  });
831
- const confirm = input.confirm ?? input.prompt?.confirm ?? (async () => true);
832
- const changeSummary = `Current: ${serializeLocalConfiguration(localCheck.value).trim()}\nNext: ${serializeLocalConfiguration(nextCheck.value).trim()}`;
833
- if (!await confirm(`Save Local configuration for ${projectRoot}?`, {
834
- title: "Local configuration changes",
835
- content: changeSummary
833
+ if (!await (input.confirm ?? input.prompt?.confirm ?? (async () => true))("Save Project configuration?", {
834
+ title: "Review configuration",
835
+ sections: [{
836
+ title: `Project: ${projectRoot}`,
837
+ current: serializeLocalConfiguration(localCheck.value).trim(),
838
+ next: serializeLocalConfiguration(nextCheck.value).trim()
839
+ }]
836
840
  })) return success({
837
841
  scope: "local",
838
842
  root: projectRoot,
@@ -926,15 +930,17 @@ async function configureGlobalUnlocked(input = {}, locks) {
926
930
  scope: "global",
927
931
  changed: false
928
932
  });
929
- const confirm = input.confirm ?? input.prompt?.confirm ?? (async () => true);
930
- const localChangeSummary = [...replacementConfigurations].map(([root, configuration]) => `Project ${root}:\nCurrent: ${serializeLocalConfiguration(localConfigurations.get(root)).trim()}\nNext: ${serializeLocalConfiguration(configuration).trim()}`).join("\n\n");
931
- if (!await confirm("Save Global configuration?", {
932
- title: "Global configuration changes",
933
- content: [
934
- `Current Global:\n${serializeGlobalConfiguration(currentConfiguration).trim()}`,
935
- `Next Global:\n${serializeGlobalConfiguration(nextCheck.value).trim()}`,
936
- localChangeSummary
937
- ].filter(Boolean).join("\n\n")
933
+ if (!await (input.confirm ?? input.prompt?.confirm ?? (async () => true))("Save Global configuration?", {
934
+ title: "Review configuration",
935
+ sections: [{
936
+ title: "Global configuration",
937
+ current: serializeGlobalConfiguration(currentConfiguration).trim(),
938
+ next: serializeGlobalConfiguration(nextCheck.value).trim()
939
+ }, ...[...replacementConfigurations].map(([root, configuration]) => ({
940
+ title: `Project: ${root}`,
941
+ current: serializeLocalConfiguration(localConfigurations.get(root)).trim(),
942
+ next: serializeLocalConfiguration(configuration).trim()
943
+ }))]
938
944
  })) return success({
939
945
  scope: "global",
940
946
  changed: false
@@ -1467,7 +1473,7 @@ function createConfigurationPrompter({ signal, input, output }) {
1467
1473
  };
1468
1474
  return {
1469
1475
  confirm: async (message, details) => {
1470
- if (details !== void 0) p.note(details.content, details.title, common);
1476
+ if (details !== void 0) p.note(renderConfirmationDetails(details), c.bold(c.cyan(details.title)), common);
1471
1477
  return promptValue(p.confirm({
1472
1478
  message,
1473
1479
  ...common
@@ -1485,6 +1491,7 @@ function createConfigurationPrompter({ signal, input, output }) {
1485
1491
  ...common
1486
1492
  })),
1487
1493
  editGlobal: async (configuration, catalog) => {
1494
+ p.log.step(`${c.bold(c.cyan("Global configuration"))}\n${c.dim("Choose the runtimes and agents available to every Project.")}`, common);
1488
1495
  return {
1489
1496
  version: 1,
1490
1497
  node: await promptValue(p.multiselect({
@@ -1509,6 +1516,7 @@ function createConfigurationPrompter({ signal, input, output }) {
1509
1516
  };
1510
1517
  },
1511
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);
1512
1520
  return {
1513
1521
  version: 1,
1514
1522
  node: await promptValue(p.select({
@@ -1527,13 +1535,69 @@ function createConfigurationPrompter({ signal, input, output }) {
1527
1535
  }
1528
1536
  };
1529
1537
  }
1538
+ function renderConfirmationDetails(details) {
1539
+ return details.sections.map((section) => [
1540
+ c.bold(section.title),
1541
+ `${c.yellow(c.dim("Current"))}\n${section.current}`,
1542
+ `${c.bold(c.green("New"))}\n${section.next}`
1543
+ ].join("\n\n")).join("\n\n");
1544
+ }
1530
1545
  async function promptValue(prompt) {
1531
1546
  const value = await prompt;
1532
1547
  if (p.isCancel(value)) throw new InterruptedError();
1533
1548
  return value;
1534
1549
  }
1535
1550
  //#endregion
1551
+ //#region src/logo.ts
1552
+ const ASCII_ART = [
1553
+ "██████╗ ███████╗██╗ ██╗██████╗ ██████╗ ██╗ ██╗",
1554
+ "██╔══██╗██╔════╝██║ ██║██╔══██╗██╔═══██╗╚██╗██╔╝",
1555
+ "██║ ██║█████╗ ██║ ██║██████╔╝██║ ██║ ╚███╔╝ ",
1556
+ "██║ ██║██╔══╝ ╚██╗ ██╔╝██╔══██╗██║ ██║ ██╔██╗ ",
1557
+ "██████╔╝███████╗ ╚████╔╝ ██████╔╝╚██████╔╝██╔╝ ██╗",
1558
+ "╚═════╝ ╚══════╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝"
1559
+ ];
1560
+ const COLORS = [
1561
+ `\x1B[38;5;51m`,
1562
+ `\x1B[38;5;45m`,
1563
+ `\x1B[38;5;39m`,
1564
+ `\x1B[38;5;32m`,
1565
+ `\x1B[38;5;25m`,
1566
+ `\x1B[38;5;26m`
1567
+ ];
1568
+ const REST = "\x1B[0m";
1569
+ function showLogo() {
1570
+ for (let i = 0; i < ASCII_ART.length; i++) console.log(`${COLORS[i]}${ASCII_ART[i]}${REST}`);
1571
+ }
1572
+ //#endregion
1573
+ //#region \0devbox-workspace-image-default:.bash_aliases.mjs
1574
+ 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";
1575
+ //#endregion
1576
+ //#region \0devbox-workspace-image-default:.claude/settings.json.mjs
1577
+ 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";
1578
+ //#endregion
1579
+ //#region \0devbox-workspace-image-default:.gitconfig.mjs
1580
+ 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";
1581
+ //#endregion
1582
+ //#region \0devbox-workspace-image-default:.omp/agent/config.yml.mjs
1583
+ 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";
1584
+ //#endregion
1536
1585
  //#region src/workspace/build.ts
1586
+ const BASE_WORKSPACE_IMAGE_DEFAULTS = [{
1587
+ relativePath: ".bash_aliases",
1588
+ content: _devbox_workspace_image_default__bash_aliases_default
1589
+ }, {
1590
+ relativePath: ".gitconfig",
1591
+ content: _devbox_workspace_image_default__gitconfig_default
1592
+ }];
1593
+ const CLAUDE_SETTINGS_DEFAULT = {
1594
+ relativePath: ".claude/settings.json",
1595
+ content: settings_json_default
1596
+ };
1597
+ const OMP_AGENT_CONFIG_DEFAULT = {
1598
+ relativePath: ".omp/agent/config.yml",
1599
+ content: config_yml_default
1600
+ };
1537
1601
  async function buildWorkspace(input = {}) {
1538
1602
  return withStateLocks({
1539
1603
  devboxHome: input.devboxHome ?? join(homedir(), ".devbox"),
@@ -1561,22 +1625,29 @@ async function buildWorkspaceUnlocked(input) {
1561
1625
  const parsedGlobal = parseGlobalConfiguration(globalSource, catalogCheck.value);
1562
1626
  if (!parsedGlobal.ok) return parsedGlobal;
1563
1627
  const globalConfiguration = parsedGlobal.value;
1628
+ const agents = globalConfiguration.agent.map((name) => ({
1629
+ name,
1630
+ recipe: PACKAGED_AGENTS[name]
1631
+ }));
1632
+ const buildContextAssets = selectBuildContextAssets(agents);
1633
+ const nodeRuntimes = globalConfiguration.node.map((releaseLine) => PACKAGED_NODE_RECIPES[releaseLine]);
1564
1634
  const dockerfile = renderBuildDockerfile({
1565
- nodeRuntimes: globalConfiguration.node.map((releaseLine) => PACKAGED_NODE_RECIPES[releaseLine]),
1635
+ nodeRuntimes,
1566
1636
  buildNodeRuntime: buildNodeRuntimeRecipe(globalConfiguration),
1567
- agents: globalConfiguration.agent.map((name) => ({
1568
- name,
1569
- recipe: PACKAGED_AGENTS[name]
1570
- })),
1637
+ agents,
1571
1638
  skillAgents: globalConfiguration.agent.filter((agent) => PACKAGED_AGENTS[agent]?.supportsSkillInstallation === true)
1572
1639
  });
1573
- await rm(paths.buildContext, {
1574
- recursive: true,
1575
- force: true
1576
- });
1577
1640
  await mkdir(paths.buildContext, { recursive: true });
1578
- await writeFile(join(paths.buildContext, "Dockerfile"), dockerfile);
1579
- await writeFile(join(paths.buildContext, ".dockerignore"), "# Machine-owned Workspace build context\n*\n!Dockerfile\n");
1641
+ await seedBuildContextAssets(paths.buildContext, buildContextAssets);
1642
+ await Promise.all([
1643
+ writeFile(join(paths.buildContext, "Dockerfile"), dockerfile),
1644
+ writeFile(join(paths.buildContext, ".dockerignore"), renderDockerignore(buildContextAssets)),
1645
+ writeFile(join(paths.buildContext, "entrypoint.sh"), renderEntrypoint({
1646
+ nodeRuntimes,
1647
+ agents
1648
+ })),
1649
+ writeFile(join(paths.buildContext, "supervisord.conf"), renderSupervisorConfiguration())
1650
+ ]);
1580
1651
  if (input.signal?.aborted) throw new InterruptedError();
1581
1652
  const buildResult = await (input.executeDockerBuild ?? runDockerBuild)({
1582
1653
  context: paths.buildContext,
@@ -1635,9 +1706,85 @@ function renderBuildDockerfile(input) {
1635
1706
  const skillAgentArguments = input.skillAgents.map((agent) => `-a ${agent}`).join(" ");
1636
1707
  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`, "");
1637
1708
  }
1638
- lines.push("USER root");
1709
+ 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", "");
1710
+ const hasClaudeCode = input.agents.some((agent) => agent.name === "claude-code");
1711
+ const hasOmp = input.agents.some((agent) => agent.name === "omp");
1712
+ if (hasClaudeCode || hasOmp) {
1713
+ const ownershipCommands = [];
1714
+ lines.push("# 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
+ }
1725
+ 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
+ return `${lines.join("\n")}\n`;
1727
+ }
1728
+ function renderDockerignore(assets) {
1729
+ const lines = [
1730
+ "# Machine-owned Workspace build context",
1731
+ "*",
1732
+ "!Dockerfile",
1733
+ "!entrypoint.sh",
1734
+ "!supervisord.conf",
1735
+ ...assets.map((asset) => `!${asset.relativePath}`)
1736
+ ];
1737
+ if (assets.some((asset) => asset.relativePath === ".claude/settings.json")) lines.push("!.claude", "!.claude/settings.json");
1738
+ if (assets.some((asset) => asset.relativePath === ".omp/agent/config.yml")) lines.push("!.omp", "!.omp/agent", "!.omp/agent/config.yml");
1639
1739
  return `${lines.join("\n")}\n`;
1640
1740
  }
1741
+ async function seedBuildContextAssets(buildContext, assets) {
1742
+ await Promise.all(assets.map(async (asset) => {
1743
+ const destination = join(buildContext, asset.relativePath);
1744
+ await mkdir(dirname(destination), { recursive: true });
1745
+ try {
1746
+ await writeFile(destination, asset.content, { flag: "wx" });
1747
+ } catch (error) {
1748
+ if (typeof error !== "object" || error === null || !("code" in error) || error.code !== "EEXIST") throw error;
1749
+ }
1750
+ }));
1751
+ }
1752
+ function selectBuildContextAssets(agents) {
1753
+ const assets = [...BASE_WORKSPACE_IMAGE_DEFAULTS];
1754
+ if (agents.some((agent) => agent.name === "claude-code")) assets.push(CLAUDE_SETTINGS_DEFAULT);
1755
+ if (agents.some((agent) => agent.name === "omp")) assets.push(OMP_AGENT_CONFIG_DEFAULT);
1756
+ return assets;
1757
+ }
1758
+ 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", "");
1765
+ 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
+ lines.push("if [ $# -gt 0 ]; then", " exec gosu devbox \"$@\"", "else", " exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf", "fi");
1767
+ return `${lines.join("\n")}\n`;
1768
+ }
1769
+ function renderSupervisorConfiguration() {
1770
+ return `${[
1771
+ "[supervisord]",
1772
+ "nodaemon=true",
1773
+ "user=root",
1774
+ "logfile=/var/log/supervisor/supervisord.log",
1775
+ "pidfile=/var/run/supervisord.pid",
1776
+ "",
1777
+ "[program:idle]",
1778
+ "command=/bin/sleep infinity",
1779
+ "autorestart=false",
1780
+ "startsecs=0",
1781
+ "stopsignal=TERM",
1782
+ "stopasgroup=true",
1783
+ "killasgroup=true",
1784
+ "stdout_logfile=/dev/null",
1785
+ "stderr_logfile=/dev/null"
1786
+ ].join("\n")}\n`;
1787
+ }
1641
1788
  function renderNodeRuntimeStage(recipe) {
1642
1789
  const keys = recipe.trustedReleaseKeys.map((key) => ` ${key}`).join(" \\\n");
1643
1790
  return [
@@ -1724,29 +1871,45 @@ function createCli(signal, interactive) {
1724
1871
  cli.command("").action(() => cli.outputHelp());
1725
1872
  cli.command("init", "Register the current directory as a Devbox Project.").action(async () => {
1726
1873
  if (!interactive) return interactiveFailure("devbox init requires an interactive terminal.", "Run devbox init from a TTY before it writes Project state.");
1874
+ console.log();
1875
+ showLogo();
1876
+ console.log();
1877
+ intro(c.bgCyan(" devbox "));
1727
1878
  const result = await initializeProject({
1728
1879
  signal,
1729
1880
  prompt
1730
1881
  });
1731
1882
  if (!result.ok) return result;
1732
- return success({ message: result.value.created ? `Registered Project: ${result.value.root}` : result.value.confirmed === false ? "Project registration was not changed." : `Project is already registered: ${result.value.root}` });
1883
+ if (result.value.confirmed === false) {
1884
+ outro("Project registration was not changed.");
1885
+ return success({});
1886
+ }
1887
+ if (!result.value.created) {
1888
+ outro("Project is already registered.");
1889
+ return success({});
1890
+ }
1891
+ outro(`Next: run ${c.cyan(c.bold("devbox build"))} to prepare your workspace, then ${c.cyan(c.bold("devbox up"))} to start it.`);
1892
+ return success({});
1733
1893
  });
1734
1894
  cli.command("config", "Edit Global or Local configuration for the current Project.").action(async () => {
1735
1895
  if (!interactive) return interactiveFailure("devbox config requires an interactive terminal.", "Run devbox config from a TTY, or edit the supported Global or Local YAML directly.");
1896
+ intro(c.bgCyan(" devbox "));
1736
1897
  if (await prompt.selectConfigurationScope() === "global") {
1737
1898
  const result = await configureGlobal({
1738
1899
  signal,
1739
1900
  prompt
1740
1901
  });
1741
1902
  if (!result.ok) return result;
1742
- return success({ message: result.value.changed ? "Global configuration updated." : "Global configuration was not changed." });
1903
+ outro(result.value.changed ? "Global configuration updated." : "Global configuration was not changed.");
1904
+ return success({});
1743
1905
  }
1744
1906
  const result = await configureLocalProject({
1745
1907
  signal,
1746
1908
  prompt
1747
1909
  });
1748
1910
  if (!result.ok) return result;
1749
- return success({ message: result.value.changed ? "Local configuration updated." : "Local configuration was not changed." });
1911
+ outro(result.value.changed ? "Local configuration updated." : "Local configuration was not changed.");
1912
+ return success({});
1750
1913
  });
1751
1914
  cli.command("build", "Build the shared Workspace image from Global configuration.").option("--cache, --no-cache", "Use Docker layer cache.", { default: true }).action(async (options = {}) => {
1752
1915
  const result = await buildWorkspace({
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ycs77/devbox",
3
- "version": "0.2.0",
4
- "description": "AI-first containerized development environment for Node.js projects",
3
+ "version": "0.3.0",
4
+ "description": "A containerized devbox for AI agents",
5
5
  "keywords": [
6
6
  "agent",
7
7
  "agentic",
@@ -35,6 +35,7 @@
35
35
  "dependencies": {
36
36
  "@clack/prompts": "^1.7.0",
37
37
  "cac": "^7.0.0",
38
+ "picocolors": "^1.1.1",
38
39
  "yaml": "^2.9.0"
39
40
  },
40
41
  "devDependencies": {