@ycs77/devbox 0.6.0 → 0.7.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 +9 -4
  2. package/dist/cli.mjs +58 -167
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -47,11 +47,16 @@ From your project directory, initialize Devbox:
47
47
  devbox init
48
48
  ```
49
49
 
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.
50
+ **Global configuration** selects the Node.js versions and AI agents installed in the shared Workspace image. These choices apply to every Project; adding more makes the image larger and uses more disk space.
51
51
 
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.
52
+ **Local configuration** selects the Node.js version for the current Project. A Project can use only a version included in the Global configuration.
53
53
 
54
- Build the shared Workspace Image once after setup or whenever you change global configuration:
54
+ Published ports accept one mapping per line. The following are the supported port mapping formats:
55
+
56
+ - `5173:5173` maps host port `5173` to container port `5173`.
57
+ - `APP_PORT:5173:5173` maps the host port from the `APP_PORT` environment variable (default port: `5173`) to container port `5173`.
58
+
59
+ Build the shared Workspace image once after setup or whenever you change global configuration:
55
60
 
56
61
  ```bash
57
62
  devbox build
@@ -81,8 +86,8 @@ Run these commands from the initialized project directory:
81
86
  | `devbox sh` | Open a Bash shell in the running sandbox. |
82
87
  | `devbox stop` | Stop the sandbox and preserve it for a later restart. |
83
88
  | `devbox down` | Stop and remove the sandbox container. |
89
+ | `devbox rm` | Stop and remove the current Project's Sandbox, then remove its Devbox state. |
84
90
  | `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. |
86
91
 
87
92
  ## Troubleshooting
88
93
 
package/dist/cli.mjs CHANGED
@@ -11,7 +11,7 @@ import { promisify } from "node:util";
11
11
  import { parse as parse$1, parseDocument, stringify } from "yaml";
12
12
  import { once } from "node:events";
13
13
  //#region package.json
14
- var version = "0.6.0";
14
+ var version = "0.7.0";
15
15
  //#endregion
16
16
  //#region src/catalog/index.ts
17
17
  const NODE_RELEASE_KEYS = [
@@ -55,7 +55,7 @@ const PACKAGED_AGENTS = {
55
55
  },
56
56
  supportsNotifications: true,
57
57
  notificationInstallationCommands: ["claude plugin marketplace add ycs77/claude-code-notifications", "claude plugin install notification-basic-wsl@ycs77-notifications"],
58
- supportsSkillInstallation: true
58
+ skillInstallationTarget: "claude-code"
59
59
  },
60
60
  codex: {
61
61
  home: {
@@ -67,7 +67,7 @@ const PACKAGED_AGENTS = {
67
67
  shell: "sh"
68
68
  },
69
69
  supportsNotifications: true,
70
- supportsSkillInstallation: true,
70
+ skillInstallationTarget: "codex",
71
71
  notificationInstallationCommands: ["codex plugin marketplace add ycs77/codex-notifications", "codex plugin add notification-basic-wsl@ycs77-notifications"]
72
72
  },
73
73
  agy: {
@@ -79,8 +79,7 @@ const PACKAGED_AGENTS = {
79
79
  url: "https://antigravity.google/cli/install.sh",
80
80
  shell: "bash"
81
81
  },
82
- supportsNotifications: false,
83
- supportsSkillInstallation: false
82
+ supportsNotifications: false
84
83
  },
85
84
  omp: {
86
85
  home: {
@@ -92,7 +91,7 @@ const PACKAGED_AGENTS = {
92
91
  shell: "sh"
93
92
  },
94
93
  supportsNotifications: true,
95
- supportsSkillInstallation: false,
94
+ skillInstallationTarget: "codex",
96
95
  notificationInstallationCommands: ["omp plugin marketplace add ycs77/omp-notifications", "omp plugin install notification-basic@ycs77-notifications"]
97
96
  }
98
97
  };
@@ -514,6 +513,7 @@ const WORKSPACE_IMAGE = "devbox-workspace:latest";
514
513
  //#endregion
515
514
  //#region src/project/compose.ts
516
515
  function renderProjectCompose(input) {
516
+ const containerName = `devbox-${input.sandboxName}`;
517
517
  const node = selectNodeComposeFragment(input.selectedNode);
518
518
  const agent = selectAgentComposeFragments(input.configuredAgents, input.agentNotifications);
519
519
  const environment = {
@@ -544,17 +544,17 @@ function renderProjectCompose(input) {
544
544
  }]
545
545
  ];
546
546
  const definition = {
547
- name: input.sandboxName,
547
+ name: containerName,
548
548
  "x-devbox": {
549
549
  version: 1,
550
550
  project_root: input.projectRoot,
551
- compose_name: input.sandboxName,
551
+ container_name: containerName,
552
552
  sandbox_service: "devbox"
553
553
  },
554
554
  services: { devbox: {
555
555
  image: WORKSPACE_IMAGE,
556
556
  hostname: "devbox",
557
- container_name: `devbox-${input.sandboxName}`,
557
+ container_name: containerName,
558
558
  working_dir: `/workspace/${input.sandboxName}`,
559
559
  ...input.ports.length === 0 ? {} : { ports: input.ports.map((port) => renderComposePortMapping(port)) },
560
560
  ...Object.keys(environment).length === 0 ? {} : { environment },
@@ -566,21 +566,21 @@ function renderProjectCompose(input) {
566
566
  }])) }
567
567
  };
568
568
  const source = stringify(definition);
569
- return validatesProjectCompose(source, input) ? success(source) : failure({
569
+ return validatesProjectCompose(source, input, containerName) ? success(source) : failure({
570
570
  kind: "operational",
571
571
  code: "invalid-project-compose",
572
572
  observed: `Devbox generated an invalid Compose definition for Project ${input.projectRoot}.`,
573
573
  nextAction: "Run the command again. If the problem continues, report it as a Devbox bug."
574
574
  });
575
575
  }
576
- function validatesProjectCompose(source, input) {
576
+ function validatesProjectCompose(source, input, containerName) {
577
577
  try {
578
578
  const definition = parse$1(source);
579
- if (definition.name !== input.sandboxName) return false;
579
+ if (definition.name !== containerName) return false;
580
580
  const metadata = definition["x-devbox"];
581
- if (metadata.version !== 1 || metadata.project_root !== input.projectRoot || metadata.compose_name !== input.sandboxName || metadata.sandbox_service !== "devbox") return false;
581
+ if (metadata.version !== 1 || metadata.project_root !== input.projectRoot || metadata.container_name !== containerName || metadata.sandbox_service !== "devbox") return false;
582
582
  const service = definition.services.devbox;
583
- if (service.image !== "devbox-workspace:latest" || service.container_name !== `devbox-${input.sandboxName}` || service.working_dir !== `/workspace/${input.sandboxName}` || "build" in service) return false;
583
+ if (service.image !== "devbox-workspace:latest" || service.container_name !== containerName || service.working_dir !== `/workspace/${input.sandboxName}` || "build" in service) return false;
584
584
  const ports = input.ports.map((port) => renderComposePortMapping(port));
585
585
  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);
586
586
  } catch {
@@ -1060,13 +1060,20 @@ async function removeProjectUnlocked(input = {}) {
1060
1060
  const registration = registryCheck.value.projects[projectRoot];
1061
1061
  if (registration === void 0) return notRegistered(projectRoot);
1062
1062
  if (!input.yes) {
1063
- if (!await (input.confirm ?? (async () => false))(`Remove Project ${projectRoot} and its Devbox state?`)) return success({
1063
+ if (!await (input.confirm ?? (async () => false))(`Remove Project ${projectRoot}, its Sandbox, and Devbox state?`)) return success({
1064
1064
  root: projectRoot,
1065
1065
  removed: false
1066
1066
  });
1067
1067
  }
1068
1068
  if (input.signal?.aborted) throw new InterruptedError();
1069
1069
  const stateDirectory = projectStateDirectory(registration.identity, paths.home);
1070
+ const environment = input.environment ?? currentHostEnvironment();
1071
+ await (environment.runDirect ?? environment.run)("docker", [
1072
+ "compose",
1073
+ "--file",
1074
+ join(stateDirectory, "compose.yaml"),
1075
+ "down"
1076
+ ]);
1070
1077
  try {
1071
1078
  await rm(stateDirectory, {
1072
1079
  recursive: true,
@@ -1092,107 +1099,6 @@ async function removeProjectUnlocked(input = {}) {
1092
1099
  removed: true
1093
1100
  });
1094
1101
  }
1095
- async function cleanupMissingProjects(input = {}) {
1096
- return withStateLocks({
1097
- devboxHome: input.devboxHome ?? join(homedir(), ".devbox"),
1098
- global: true,
1099
- projectRoots: [],
1100
- signal: input.signal
1101
- }, async (locks) => {
1102
- const discovery = await discoverMissingProjects(input);
1103
- if (!discovery.ok) return discovery;
1104
- await locks.acquireProjectScopes(discovery.value.missingRoots);
1105
- return cleanupMissingProjectsUnlocked(input, discovery.value);
1106
- });
1107
- }
1108
- async function discoverMissingProjects(input) {
1109
- if (input.signal?.aborted) throw new InterruptedError();
1110
- const paths = devboxPaths(input.devboxHome);
1111
- const registryState = await readOptionalFile(paths.projectRegistry);
1112
- if (!registryState.ok) return registryState;
1113
- if (!registryState.value.exists) return success({
1114
- paths,
1115
- registry: {
1116
- version: 1,
1117
- projects: {}
1118
- },
1119
- missingRoots: []
1120
- });
1121
- const registryCheck = parseProjectRegistry(registryState.value.content);
1122
- if (!registryCheck.ok) return registryCheck;
1123
- const missingRoots = [];
1124
- for (const root of Object.keys(registryCheck.value.projects)) {
1125
- if (input.signal?.aborted) throw new InterruptedError();
1126
- const exists = await projectRootExists(root);
1127
- if (exists === void 0) return failure({
1128
- kind: "operational",
1129
- code: "project-root-observation-failed",
1130
- observed: `Devbox could not inspect Project root: ${root}.`,
1131
- nextAction: "Check access to the registered Project root and try cleanup again."
1132
- });
1133
- if (!exists) missingRoots.push(root);
1134
- }
1135
- return success({
1136
- paths,
1137
- registry: registryCheck.value,
1138
- missingRoots
1139
- });
1140
- }
1141
- async function cleanupMissingProjectsUnlocked(input, discovery) {
1142
- if (input.signal?.aborted) throw new InterruptedError();
1143
- if (discovery.missingRoots.length === 0) return success({
1144
- roots: [],
1145
- removed: false
1146
- });
1147
- if (!input.yes) {
1148
- if (!await (input.confirm ?? (async () => false))(`Remove Missing-root Project registrations?\n${discovery.missingRoots.map((root) => `- ${root}`).join("\n")}`)) return success({
1149
- roots: [],
1150
- removed: false
1151
- });
1152
- }
1153
- const removedRoots = [];
1154
- const projects = { ...discovery.registry.projects };
1155
- for (const root of discovery.missingRoots) {
1156
- const rootExists = await projectRootExists(root);
1157
- if (rootExists === void 0) return failure({
1158
- kind: "operational",
1159
- code: "project-root-observation-failed",
1160
- observed: `Devbox could not recheck Project root: ${root}.`,
1161
- nextAction: "Check access to the registered Project root and try cleanup again."
1162
- });
1163
- if (rootExists) continue;
1164
- const registration = projects[root];
1165
- if (registration === void 0) continue;
1166
- try {
1167
- await rm(projectStateDirectory(registration.identity, discovery.paths.home), {
1168
- recursive: true,
1169
- force: true
1170
- });
1171
- } catch {
1172
- return failure({
1173
- kind: "operational",
1174
- code: "missing-project-state-removal-failed",
1175
- observed: `Devbox could not remove Missing-root Project state for ${root}.`,
1176
- nextAction: "Check write access to ~/.devbox and try cleanup again."
1177
- });
1178
- }
1179
- delete projects[root];
1180
- removedRoots.push(root);
1181
- }
1182
- if (removedRoots.length === 0) return success({
1183
- roots: [],
1184
- removed: false
1185
- });
1186
- const written = await writeAtomically(discovery.paths.projectRegistry, serializeProjectRegistry({
1187
- version: 1,
1188
- projects
1189
- }));
1190
- if (!written.ok) return written;
1191
- return success({
1192
- roots: removedRoots,
1193
- removed: true
1194
- });
1195
- }
1196
1102
  async function runSandboxLifecycle(command, input = {}) {
1197
1103
  const projectRoot = input.root ?? process.cwd();
1198
1104
  return withStateLocks({
@@ -1311,14 +1217,6 @@ function allocateSandboxName(projectRoot, registry) {
1311
1217
  nextAction: "Remove stale Devbox state only through its supported command and try again."
1312
1218
  });
1313
1219
  }
1314
- async function projectRootExists(projectRoot) {
1315
- try {
1316
- return (await lstat(projectRoot)).isDirectory();
1317
- } catch (error) {
1318
- if (isMissingFileError(error)) return false;
1319
- return;
1320
- }
1321
- }
1322
1220
  async function readOptionalFile(path) {
1323
1221
  try {
1324
1222
  return success({
@@ -1547,17 +1445,15 @@ function createConfigurationPrompter({ signal, input, output }) {
1547
1445
  message: "Where should this configuration apply?",
1548
1446
  options: [{
1549
1447
  value: "local",
1550
- label: "Current project",
1551
- hint: "Enabled when this project’s container starts."
1448
+ label: "Current project"
1552
1449
  }, {
1553
1450
  value: "global",
1554
- label: "Workspace Docker image",
1555
- hint: "Built into the shared image. It affects image size and disk usage."
1451
+ label: "Workspace Docker image"
1556
1452
  }],
1557
1453
  ...common
1558
1454
  })),
1559
1455
  editGlobal: async (configuration, catalog) => {
1560
- 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);
1456
+ p.log.step(c.bold(c.cyan("Workspace Docker image")), common);
1561
1457
  return {
1562
1458
  version: 1,
1563
1459
  node: await promptValue(p.multiselect({
@@ -1582,7 +1478,7 @@ function createConfigurationPrompter({ signal, input, output }) {
1582
1478
  };
1583
1479
  },
1584
1480
  editLocal: async (configuration, catalog, globalConfiguration, editPorts = true) => {
1585
- 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);
1481
+ p.log.step(c.bold(c.cyan("Project configuration")), common);
1586
1482
  const node = await promptValue(p.select({
1587
1483
  message: "Selected Node release line",
1588
1484
  options: [{
@@ -1600,7 +1496,7 @@ function createConfigurationPrompter({ signal, input, output }) {
1600
1496
  node
1601
1497
  };
1602
1498
  const ports = normalizePortMappings((await promptValue(p.multiline({
1603
- message: "Published ports",
1499
+ message: "Published ports" + c.dim(" (multiline; one mapping per line; press Enter twice to submit)"),
1604
1500
  initialValue: configuration.ports.join("\n"),
1605
1501
  validate: (value) => {
1606
1502
  const ports = normalizePortMappings((value ?? "").split("\n"));
@@ -1658,6 +1554,9 @@ var settings_json_default = "{\n \"$schema\": \"https://json.schemastore.org/cl
1658
1554
  //#region \0devbox-image-default:/home/runner/work/devbox/devbox/src/agent/image-defaults/.omp/agent/config.yml.mjs
1659
1555
  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";
1660
1556
  //#endregion
1557
+ //#region \0devbox-image-default:/home/runner/work/devbox/devbox/src/agent/image-defaults/AGENTS.md.mjs
1558
+ var AGENTS_md_default = "# 協作規範\n\n## 語言\n\n- 一律使用繁體中文台灣用語產生對話及結果\n- 嚴禁使用簡體字,或是繁體字但中國大陸用語\n- 用詞要口語、常見,避免生硬或翻譯腔的詞彙;例如「根因」改用「原因」、「根本原因」、「主要原因」\n\n## 開發規範\n\n### 共用原則\n\n- 優先遵循專案既有的 `AGENTS.md`、文件、設定檔、lockfile、指令與程式碼慣例;專案沒有既有規範時,才使用本文件的預設選擇\n- 優先使用目前環境提供的專用工具;只有在專用工具無法完成工作或需要執行專案指令時,才使用適合的 CLI 工具\n- 執行會修改檔案、安裝套件或變更環境的指令前,確認目前位於預期的工作目錄\n- 可以在目前 workspace 內存取所需的上層目錄;存取 workspace 以外的檔案或目錄前,必須先取得使用者同意\n- 嚴禁執行任何會遞迴刪除根目錄的指令(例如 `rm -rf /`),也不得改寫或規避為效果相同的形式\n- 執行其他可能造成不可逆資料遺失的操作前,必須說明影響並取得使用者明確同意\n\n### JavaScript / TypeScript\n\n- 優先使用專案既有的 Node.js 版本設定與套件管理器\n- 專案未指定套件管理器且不存在 lockfile 時,使用 npm\n- 需要執行專案套件提供的 CLI 時,使用專案既有套件管理器對應的執行方式;使用 npm 時使用 npx\n- 不得擅自更換套件管理器、刪除 lockfile 或產生不同格式的 lockfile\n- TypeScript 格式規則\n - 陣列型別優先使用 `T[]`,避免使用 `Array<T>`\n - 字典型物件優先使用 `Record<string, T>`,避免使用 `{ [key: string]: T }`\n- Vue 3 程式碼變更前,先讀取 `/ycs77-vue` skill\n\n### PHP\n\n- 優先使用專案既有的 Composer scripts\n\n### 常見 CLI 工具\n\n- 版本控制操作使用 `git`\n- 所有 Git 操作都必須直接執行 `git` CLI\n- 禁止透過 Eval、JavaScript、Bun、Node.js、Python 或臨時程式碼間接包裝或呼叫 Git 指令\n- GitHub 相關操作優先使用 `gh` CLI\n\n## 安全\n\n- 在任何對話、指令輸出或程式碼中,不得揭露密碼、金鑰、Access Token、API Key 等機密資料\n- 不得讀取或輸出 `.env`、`.env.*` 等可能包含機密資料的檔案;不含真實機密的範本檔案(例如 `.env.example`)除外\n- 開發時需要使用機密資料,應以環境變數名稱或假值代稱,不得嘗試取得實際值\n";
1559
+ //#endregion
1661
1560
  //#region src/agent/build.ts
1662
1561
  function createAgentWorkspaceContribution(input) {
1663
1562
  const agents = input.configuredAgents.map((name) => ({
@@ -1665,7 +1564,7 @@ function createAgentWorkspaceContribution(input) {
1665
1564
  recipe: PACKAGED_AGENTS[name]
1666
1565
  }));
1667
1566
  const buildContextAssets = selectBuildContextAssets(agents);
1668
- const skillAgents = agents.filter((agent) => agent.recipe.supportsSkillInstallation).map((agent) => agent.name);
1567
+ const skillAgents = [...new Set(agents.flatMap((agent) => agent.recipe.skillInstallationTarget === void 0 ? [] : [agent.recipe.skillInstallationTarget]))];
1669
1568
  return {
1670
1569
  buildContextAssets,
1671
1570
  dockerignoreEntries: selectDockerignoreEntries(buildContextAssets),
@@ -1690,12 +1589,17 @@ function renderAgentInstallations(agents, buildNodeRuntimeRoot, skillAgents) {
1690
1589
  }
1691
1590
  function selectDockerignoreEntries(assets) {
1692
1591
  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");
1592
+ if (assets.some((asset) => asset.relativePath === "AGENTS.md")) entries.push("!AGENTS.md");
1593
+ if (assets.some((asset) => asset.relativePath.startsWith(".claude/"))) entries.push("!.claude", "!.claude/settings.json");
1594
+ if (assets.some((asset) => asset.relativePath.startsWith(".omp/agent/"))) entries.push("!.omp", "!.omp/agent", "!.omp/agent/config.yml");
1695
1595
  return entries;
1696
1596
  }
1697
1597
  function selectBuildContextAssets(agents) {
1698
1598
  const assets = [];
1599
+ if (agents.length > 0) assets.push({
1600
+ relativePath: "AGENTS.md",
1601
+ content: AGENTS_md_default
1602
+ });
1699
1603
  if (agents.some((agent) => agent.name === "claude-code")) assets.push({
1700
1604
  relativePath: ".claude/settings.json",
1701
1605
  content: settings_json_default
@@ -1708,19 +1612,16 @@ function selectBuildContextAssets(agents) {
1708
1612
  }
1709
1613
  function renderAgentAssets(agents) {
1710
1614
  const hasClaudeCode = agents.some((agent) => agent.name === "claude-code");
1615
+ const hasCodex = agents.some((agent) => agent.name === "codex");
1616
+ const hasAgy = agents.some((agent) => agent.name === "agy");
1711
1617
  const hasOmp = agents.some((agent) => agent.name === "omp");
1712
- if (!hasClaudeCode && !hasOmp) return [];
1713
- const ownershipCommands = [];
1618
+ if (!hasClaudeCode && !hasCodex && !hasAgy && !hasOmp) return [];
1714
1619
  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 && ")}`, "");
1620
+ if (hasClaudeCode) lines.push("COPY --chown=devbox:devbox .claude/settings.json /home/devbox/.claude/settings.json", "COPY --chown=devbox:devbox AGENTS.md /home/devbox/.claude/CLAUDE.md");
1621
+ if (hasCodex) lines.push("COPY --chown=devbox:devbox AGENTS.md /home/devbox/.codex/AGENTS.md");
1622
+ if (hasAgy) lines.push("COPY --chown=devbox:devbox AGENTS.md /home/devbox/.gemini/GEMINI.md");
1623
+ if (hasOmp) lines.push("COPY --chown=devbox:devbox .omp/agent/config.yml /home/devbox/.omp/agent/config.yml", "COPY --chown=devbox:devbox AGENTS.md /home/devbox/.omp/agent/AGENTS.md");
1624
+ lines.push("");
1724
1625
  return lines;
1725
1626
  }
1726
1627
  function renderAgentNotifications(agents, notificationsEnabled) {
@@ -1872,7 +1773,7 @@ async function buildWorkspaceUnlocked(input) {
1872
1773
  kind: "operational",
1873
1774
  code: "global-configuration-missing",
1874
1775
  observed: "Global configuration is required before building the Workspace image.",
1875
- nextAction: "Run devbox config to commit Global configuration before devbox build."
1776
+ nextAction: "Run devbox init to commit Global configuration before devbox build."
1876
1777
  });
1877
1778
  }
1878
1779
  const parsedGlobal = parseGlobalConfiguration(globalSource, catalogCheck.value);
@@ -1972,7 +1873,7 @@ function renderBuildDockerfile(input) {
1972
1873
  lines.push("WORKDIR /workspace", "USER devbox", "");
1973
1874
  lines.push(...input.node.sandboxUserSetup);
1974
1875
  lines.push(...input.agent.installationDockerfileLines);
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", "");
1876
+ lines.push("USER root", "", "# Copy dotfiles", "COPY --chown=devbox:devbox .bash_aliases /home/devbox/.bash_aliases", "COPY --chown=devbox:devbox .gitconfig /home/devbox/.gitconfig", "");
1976
1877
  lines.push(...input.agent.assetDockerfileLines);
1977
1878
  lines.push(...input.agent.notificationDockerfileLines);
1978
1879
  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\"]");
@@ -2088,7 +1989,7 @@ function createCli(signal, interactive) {
2088
1989
  outro("Project is already registered.");
2089
1990
  return success({});
2090
1991
  }
2091
- outro(`Next: run ${c.cyan(c.bold("devbox build"))} to prepare your workspace, then ${c.cyan(c.bold("devbox up"))} to start it.`);
1992
+ outro(`Next: run ${command("devbox build")} to prepare your workspace, then ${command("devbox up")} to start it.`);
2092
1993
  return success({});
2093
1994
  });
2094
1995
  cli.command("config", "Edit Global or Local configuration for the current Project.").action(async () => {
@@ -2100,7 +2001,7 @@ function createCli(signal, interactive) {
2100
2001
  prompt
2101
2002
  });
2102
2003
  if (!result.ok) return result;
2103
- outro(result.value.changed ? "Global configuration updated." : "Global configuration was not changed.");
2004
+ outro(result.value.changed ? `Global configuration updated. Run ${command("devbox build")} to rebuild the Workspace image.` : "Global configuration was not changed.");
2104
2005
  return success({});
2105
2006
  }
2106
2007
  const result = await configureLocalProject({
@@ -2119,7 +2020,7 @@ function createCli(signal, interactive) {
2119
2020
  if (!result.ok) return result;
2120
2021
  return success({ message: `Built Workspace image: ${result.value.image}` });
2121
2022
  });
2122
- cli.command("rm", "Remove the current registered Project and its state.").option("--yes", "Skip the confirmation prompt.").action(async (options = {}) => {
2023
+ cli.command("rm", "Remove the current Project Sandbox and Devbox state.").option("--yes", "Skip the confirmation prompt.").action(async (options = {}) => {
2123
2024
  if (!interactive && !options.yes) return interactiveFailure("devbox rm requires a TTY or --yes.", "Run devbox rm from a TTY, or pass --yes to confirm removal.");
2124
2025
  const result = await removeProject({
2125
2026
  signal,
@@ -2129,22 +2030,6 @@ function createCli(signal, interactive) {
2129
2030
  if (!result.ok) return result;
2130
2031
  return success({ message: result.value.removed ? `Removed Project: ${result.value.root}` : "Project removal was not changed." });
2131
2032
  });
2132
- cli.command("cleanup", "Remove explicitly selected disposable Devbox state.").option("--missing-projects", "Remove Missing-root Project registrations.").option("--yes", "Skip the confirmation prompt.").action(async (options = {}) => {
2133
- if (!options.missingProjects) return failure({
2134
- kind: "usage",
2135
- code: "cleanup-mode-required",
2136
- observed: "cleanup requires --missing-projects.",
2137
- nextAction: "Run devbox cleanup --missing-projects."
2138
- });
2139
- if (!interactive && !options.yes) return interactiveFailure("devbox cleanup --missing-projects requires a TTY or --yes.", "Run devbox cleanup --missing-projects from a TTY, or pass --yes to confirm removal.");
2140
- const result = await cleanupMissingProjects({
2141
- signal,
2142
- confirm: prompt.confirm,
2143
- yes: options.yes
2144
- });
2145
- if (!result.ok) return result;
2146
- return success({ message: result.value.removed ? `Removed Missing-root Projects: ${result.value.roots.join(", ")}` : "No Missing-root Projects were removed." });
2147
- });
2148
2033
  for (const [command, description] of [
2149
2034
  ["up", "Start the current Project Sandbox."],
2150
2035
  ["down", "Stop and remove the current Project Sandbox."],
@@ -2163,7 +2048,7 @@ function createCli(signal, interactive) {
2163
2048
  }
2164
2049
  function present(result) {
2165
2050
  if (!result.ok) {
2166
- process.stderr.write(`${result.error.observed}\n${result.error.nextAction}\n`);
2051
+ process.stderr.write(`${highlightCommands(result.error.observed)}\n${highlightCommands(result.error.nextAction)}\n`);
2167
2052
  return result.error.kind === "usage" ? 2 : 1;
2168
2053
  }
2169
2054
  if (result.value.message !== void 0) process.stdout.write(`${result.value.message}\n`);
@@ -2196,6 +2081,12 @@ async function main() {
2196
2081
  function isCacError(error) {
2197
2082
  return error instanceof Error && error.name === "CACError";
2198
2083
  }
2084
+ function command(value) {
2085
+ return c.cyan(c.bold(value));
2086
+ }
2087
+ function highlightCommands(message) {
2088
+ return message.replace(/\bdevbox (?:init|build|config|up|down|stop|sh|rm)\b/g, command);
2089
+ }
2199
2090
  function interactiveFailure(observed, nextAction) {
2200
2091
  return failure({
2201
2092
  kind: "usage",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycs77/devbox",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "A Lucas Yang's opinionated devbox for AI agents",
5
5
  "keywords": [
6
6
  "agent",