@ycs77/devbox 0.5.1 → 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 +292 -299
  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
@@ -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.7.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
  };
@@ -50,7 +55,7 @@ const PACKAGED_AGENTS = {
50
55
  },
51
56
  supportsNotifications: true,
52
57
  notificationInstallationCommands: ["claude plugin marketplace add ycs77/claude-code-notifications", "claude plugin install notification-basic-wsl@ycs77-notifications"],
53
- supportsSkillInstallation: true
58
+ skillInstallationTarget: "claude-code"
54
59
  },
55
60
  codex: {
56
61
  home: {
@@ -62,7 +67,7 @@ const PACKAGED_AGENTS = {
62
67
  shell: "sh"
63
68
  },
64
69
  supportsNotifications: true,
65
- supportsSkillInstallation: true,
70
+ skillInstallationTarget: "codex",
66
71
  notificationInstallationCommands: ["codex plugin marketplace add ycs77/codex-notifications", "codex plugin add notification-basic-wsl@ycs77-notifications"]
67
72
  },
68
73
  agy: {
@@ -74,8 +79,7 @@ const PACKAGED_AGENTS = {
74
79
  url: "https://antigravity.google/cli/install.sh",
75
80
  shell: "bash"
76
81
  },
77
- supportsNotifications: false,
78
- supportsSkillInstallation: false
82
+ supportsNotifications: false
79
83
  },
80
84
  omp: {
81
85
  home: {
@@ -87,7 +91,7 @@ const PACKAGED_AGENTS = {
87
91
  shell: "sh"
88
92
  },
89
93
  supportsNotifications: true,
90
- supportsSkillInstallation: false,
94
+ skillInstallationTarget: "codex",
91
95
  notificationInstallationCommands: ["omp plugin marketplace add ycs77/omp-notifications", "omp plugin install notification-basic@ycs77-notifications"]
92
96
  }
93
97
  };
@@ -467,10 +471,7 @@ function isMissingFileError$1(error) {
467
471
  return typeof error === "object" && error !== null && error.code === "ENOENT";
468
472
  }
469
473
  //#endregion
470
- //#region src/workspace/image.ts
471
- const WORKSPACE_IMAGE = "devbox-workspace:latest";
472
- //#endregion
473
- //#region src/project/compose-fragments.ts
474
+ //#region src/agent/compose.ts
474
475
  const NOTIFICATION_FRAGMENT = {
475
476
  environment: { PULSE_SERVER: "unix:/tmp/pulse-socket" },
476
477
  volume: {
@@ -479,7 +480,7 @@ const NOTIFICATION_FRAGMENT = {
479
480
  readOnly: true
480
481
  }
481
482
  };
482
- function selectComposeFragments({ selectedNode, configuredAgents, agentNotifications }) {
483
+ function selectAgentComposeFragments(configuredAgents, notificationsEnabled) {
483
484
  const agents = [];
484
485
  let hasNotificationAgent = false;
485
486
  for (const agent of configuredAgents) {
@@ -497,18 +498,27 @@ function selectComposeFragments({ selectedNode, configuredAgents, agentNotificat
497
498
  }
498
499
  }
499
500
  return {
500
- node: selectedNode === null ? void 0 : { environment: { NODE_VERSION: selectedNode } },
501
501
  agents,
502
- notification: agentNotifications && hasNotificationAgent ? NOTIFICATION_FRAGMENT : void 0
502
+ notification: notificationsEnabled && hasNotificationAgent ? NOTIFICATION_FRAGMENT : void 0
503
503
  };
504
504
  }
505
505
  //#endregion
506
+ //#region src/runtimes/node/compose.ts
507
+ function selectNodeComposeFragment(selectedNode) {
508
+ return selectedNode === null ? void 0 : { environment: { NODE_VERSION: selectedNode } };
509
+ }
510
+ //#endregion
511
+ //#region src/workspace/image.ts
512
+ const WORKSPACE_IMAGE = "devbox-workspace:latest";
513
+ //#endregion
506
514
  //#region src/project/compose.ts
507
515
  function renderProjectCompose(input) {
508
- const fragments = selectComposeFragments(input);
516
+ const containerName = `devbox-${input.sandboxName}`;
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,51 +536,51 @@ 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 = {
537
- name: input.sandboxName,
547
+ name: containerName,
538
548
  "x-devbox": {
539
549
  version: 1,
540
550
  project_root: input.projectRoot,
541
- compose_name: input.sandboxName,
551
+ container_name: containerName,
542
552
  sandbox_service: "devbox"
543
553
  },
544
554
  services: { devbox: {
545
555
  image: WORKSPACE_IMAGE,
546
556
  hostname: "devbox",
547
- container_name: `devbox-${input.sandboxName}`,
557
+ container_name: containerName,
548
558
  working_dir: `/workspace/${input.sandboxName}`,
549
559
  ...input.ports.length === 0 ? {} : { ports: input.ports.map((port) => renderComposePortMapping(port)) },
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
  }])) }
557
567
  };
558
568
  const source = stringify(definition);
559
- return validatesProjectCompose(source, input) ? success(source) : failure({
569
+ return validatesProjectCompose(source, input, containerName) ? success(source) : failure({
560
570
  kind: "operational",
561
571
  code: "invalid-project-compose",
562
572
  observed: `Devbox generated an invalid Compose definition for Project ${input.projectRoot}.`,
563
573
  nextAction: "Run the command again. If the problem continues, report it as a Devbox bug."
564
574
  });
565
575
  }
566
- function validatesProjectCompose(source, input) {
576
+ function validatesProjectCompose(source, input, containerName) {
567
577
  try {
568
578
  const definition = parse$1(source);
569
- if (definition.name !== input.sandboxName) return false;
579
+ if (definition.name !== containerName) return false;
570
580
  const metadata = definition["x-devbox"];
571
- 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;
572
582
  const service = definition.services.devbox;
573
- 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;
574
584
  const ports = input.ports.map((port) => renderComposePortMapping(port));
575
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);
576
586
  } catch {
@@ -1050,13 +1060,20 @@ async function removeProjectUnlocked(input = {}) {
1050
1060
  const registration = registryCheck.value.projects[projectRoot];
1051
1061
  if (registration === void 0) return notRegistered(projectRoot);
1052
1062
  if (!input.yes) {
1053
- 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({
1054
1064
  root: projectRoot,
1055
1065
  removed: false
1056
1066
  });
1057
1067
  }
1058
1068
  if (input.signal?.aborted) throw new InterruptedError();
1059
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
+ ]);
1060
1077
  try {
1061
1078
  await rm(stateDirectory, {
1062
1079
  recursive: true,
@@ -1082,107 +1099,6 @@ async function removeProjectUnlocked(input = {}) {
1082
1099
  removed: true
1083
1100
  });
1084
1101
  }
1085
- async function cleanupMissingProjects(input = {}) {
1086
- return withStateLocks({
1087
- devboxHome: input.devboxHome ?? join(homedir(), ".devbox"),
1088
- global: true,
1089
- projectRoots: [],
1090
- signal: input.signal
1091
- }, async (locks) => {
1092
- const discovery = await discoverMissingProjects(input);
1093
- if (!discovery.ok) return discovery;
1094
- await locks.acquireProjectScopes(discovery.value.missingRoots);
1095
- return cleanupMissingProjectsUnlocked(input, discovery.value);
1096
- });
1097
- }
1098
- async function discoverMissingProjects(input) {
1099
- if (input.signal?.aborted) throw new InterruptedError();
1100
- const paths = devboxPaths(input.devboxHome);
1101
- const registryState = await readOptionalFile(paths.projectRegistry);
1102
- if (!registryState.ok) return registryState;
1103
- if (!registryState.value.exists) return success({
1104
- paths,
1105
- registry: {
1106
- version: 1,
1107
- projects: {}
1108
- },
1109
- missingRoots: []
1110
- });
1111
- const registryCheck = parseProjectRegistry(registryState.value.content);
1112
- if (!registryCheck.ok) return registryCheck;
1113
- const missingRoots = [];
1114
- for (const root of Object.keys(registryCheck.value.projects)) {
1115
- if (input.signal?.aborted) throw new InterruptedError();
1116
- const exists = await projectRootExists(root);
1117
- if (exists === void 0) return failure({
1118
- kind: "operational",
1119
- code: "project-root-observation-failed",
1120
- observed: `Devbox could not inspect Project root: ${root}.`,
1121
- nextAction: "Check access to the registered Project root and try cleanup again."
1122
- });
1123
- if (!exists) missingRoots.push(root);
1124
- }
1125
- return success({
1126
- paths,
1127
- registry: registryCheck.value,
1128
- missingRoots
1129
- });
1130
- }
1131
- async function cleanupMissingProjectsUnlocked(input, discovery) {
1132
- if (input.signal?.aborted) throw new InterruptedError();
1133
- if (discovery.missingRoots.length === 0) return success({
1134
- roots: [],
1135
- removed: false
1136
- });
1137
- if (!input.yes) {
1138
- if (!await (input.confirm ?? (async () => false))(`Remove Missing-root Project registrations?\n${discovery.missingRoots.map((root) => `- ${root}`).join("\n")}`)) return success({
1139
- roots: [],
1140
- removed: false
1141
- });
1142
- }
1143
- const removedRoots = [];
1144
- const projects = { ...discovery.registry.projects };
1145
- for (const root of discovery.missingRoots) {
1146
- const rootExists = await projectRootExists(root);
1147
- if (rootExists === void 0) return failure({
1148
- kind: "operational",
1149
- code: "project-root-observation-failed",
1150
- observed: `Devbox could not recheck Project root: ${root}.`,
1151
- nextAction: "Check access to the registered Project root and try cleanup again."
1152
- });
1153
- if (rootExists) continue;
1154
- const registration = projects[root];
1155
- if (registration === void 0) continue;
1156
- try {
1157
- await rm(projectStateDirectory(registration.identity, discovery.paths.home), {
1158
- recursive: true,
1159
- force: true
1160
- });
1161
- } catch {
1162
- return failure({
1163
- kind: "operational",
1164
- code: "missing-project-state-removal-failed",
1165
- observed: `Devbox could not remove Missing-root Project state for ${root}.`,
1166
- nextAction: "Check write access to ~/.devbox and try cleanup again."
1167
- });
1168
- }
1169
- delete projects[root];
1170
- removedRoots.push(root);
1171
- }
1172
- if (removedRoots.length === 0) return success({
1173
- roots: [],
1174
- removed: false
1175
- });
1176
- const written = await writeAtomically(discovery.paths.projectRegistry, serializeProjectRegistry({
1177
- version: 1,
1178
- projects
1179
- }));
1180
- if (!written.ok) return written;
1181
- return success({
1182
- roots: removedRoots,
1183
- removed: true
1184
- });
1185
- }
1186
1102
  async function runSandboxLifecycle(command, input = {}) {
1187
1103
  const projectRoot = input.root ?? process.cwd();
1188
1104
  return withStateLocks({
@@ -1301,14 +1217,6 @@ function allocateSandboxName(projectRoot, registry) {
1301
1217
  nextAction: "Remove stale Devbox state only through its supported command and try again."
1302
1218
  });
1303
1219
  }
1304
- async function projectRootExists(projectRoot) {
1305
- try {
1306
- return (await lstat(projectRoot)).isDirectory();
1307
- } catch (error) {
1308
- if (isMissingFileError(error)) return false;
1309
- return;
1310
- }
1311
- }
1312
1220
  async function readOptionalFile(path) {
1313
1221
  try {
1314
1222
  return success({
@@ -1537,17 +1445,15 @@ function createConfigurationPrompter({ signal, input, output }) {
1537
1445
  message: "Where should this configuration apply?",
1538
1446
  options: [{
1539
1447
  value: "local",
1540
- label: "Current project",
1541
- hint: "Enabled when this project’s container starts."
1448
+ label: "Current project"
1542
1449
  }, {
1543
1450
  value: "global",
1544
- label: "Workspace Docker image",
1545
- hint: "Built into the shared image. It affects image size and disk usage."
1451
+ label: "Workspace Docker image"
1546
1452
  }],
1547
1453
  ...common
1548
1454
  })),
1549
1455
  editGlobal: async (configuration, catalog) => {
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);
1456
+ p.log.step(c.bold(c.cyan("Workspace Docker image")), common);
1551
1457
  return {
1552
1458
  version: 1,
1553
1459
  node: await promptValue(p.multiselect({
@@ -1572,7 +1478,7 @@ function createConfigurationPrompter({ signal, input, output }) {
1572
1478
  };
1573
1479
  },
1574
1480
  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);
1481
+ p.log.step(c.bold(c.cyan("Project configuration")), common);
1576
1482
  const node = await promptValue(p.select({
1577
1483
  message: "Selected Node release line",
1578
1484
  options: [{
@@ -1590,7 +1496,7 @@ function createConfigurationPrompter({ signal, input, output }) {
1590
1496
  node
1591
1497
  };
1592
1498
  const ports = normalizePortMappings((await promptValue(p.multiline({
1593
- message: "Published ports",
1499
+ message: "Published ports" + c.dim(" (multiline; one mapping per line; press Enter twice to submit)"),
1594
1500
  initialValue: configuration.ports.join("\n"),
1595
1501
  validate: (value) => {
1596
1502
  const ports = normalizePortMappings((value ?? "").split("\n"));
@@ -1642,34 +1548,210 @@ function showLogo() {
1642
1548
  for (let i = 0; i < ASCII_ART.length; i++) console.log(`${COLORS[i]}${ASCII_ART[i]}${REST}`);
1643
1549
  }
1644
1550
  //#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
1551
+ //#region \0devbox-image-default:/home/runner/work/devbox/devbox/src/agent/image-defaults/.claude/settings.json.mjs
1649
1552
  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
1553
  //#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
1554
+ //#region \0devbox-image-default:/home/runner/work/devbox/devbox/src/agent/image-defaults/.omp/agent/config.yml.mjs
1655
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";
1656
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
1560
+ //#region src/agent/build.ts
1561
+ function createAgentWorkspaceContribution(input) {
1562
+ const agents = input.configuredAgents.map((name) => ({
1563
+ name,
1564
+ recipe: PACKAGED_AGENTS[name]
1565
+ }));
1566
+ const buildContextAssets = selectBuildContextAssets(agents);
1567
+ const skillAgents = [...new Set(agents.flatMap((agent) => agent.recipe.skillInstallationTarget === void 0 ? [] : [agent.recipe.skillInstallationTarget]))];
1568
+ return {
1569
+ buildContextAssets,
1570
+ dockerignoreEntries: selectDockerignoreEntries(buildContextAssets),
1571
+ installationDockerfileLines: renderAgentInstallations(agents, input.buildNodeRuntimeRoot, skillAgents),
1572
+ assetDockerfileLines: renderAgentAssets(agents),
1573
+ notificationDockerfileLines: renderAgentNotifications(agents, input.notificationsEnabled),
1574
+ entrypointSetup: renderAgentEntrypointSetup(agents)
1575
+ };
1576
+ }
1577
+ function renderAgentInstallations(agents, buildNodeRuntimeRoot, skillAgents) {
1578
+ const lines = [];
1579
+ for (const agent of agents) {
1580
+ const preInstallCommands = agent.recipe.installation.preInstallCommands ?? [];
1581
+ 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}`, "");
1582
+ }
1583
+ if (agents.length > 0) lines.push("# Set the PATH to include AI tools", "ENV PATH=\"/home/devbox/.local/bin:${PATH}\"", "");
1584
+ if (buildNodeRuntimeRoot !== void 0 && skillAgents.length > 0) {
1585
+ const skillAgentArguments = skillAgents.map((agent) => `-a ${agent}`).join(" ");
1586
+ 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`, "");
1587
+ }
1588
+ return lines;
1589
+ }
1590
+ function selectDockerignoreEntries(assets) {
1591
+ const entries = [];
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");
1595
+ return entries;
1596
+ }
1597
+ function selectBuildContextAssets(agents) {
1598
+ const assets = [];
1599
+ if (agents.length > 0) assets.push({
1600
+ relativePath: "AGENTS.md",
1601
+ content: AGENTS_md_default
1602
+ });
1603
+ if (agents.some((agent) => agent.name === "claude-code")) assets.push({
1604
+ relativePath: ".claude/settings.json",
1605
+ content: settings_json_default
1606
+ });
1607
+ if (agents.some((agent) => agent.name === "omp")) assets.push({
1608
+ relativePath: ".omp/agent/config.yml",
1609
+ content: config_yml_default
1610
+ });
1611
+ return assets;
1612
+ }
1613
+ function renderAgentAssets(agents) {
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");
1617
+ const hasOmp = agents.some((agent) => agent.name === "omp");
1618
+ if (!hasClaudeCode && !hasCodex && !hasAgy && !hasOmp) return [];
1619
+ const lines = ["# Copy AI dotfiles"];
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("");
1625
+ return lines;
1626
+ }
1627
+ function renderAgentNotifications(agents, notificationsEnabled) {
1628
+ const commands = notificationsEnabled ? agents.flatMap((agent) => agent.recipe.supportsNotifications ? agent.recipe.notificationInstallationCommands : []) : [];
1629
+ if (commands.length === 0) return [];
1630
+ return [
1631
+ "# Install Agent Notification Plugins",
1632
+ "USER devbox",
1633
+ "RUN set -eux \\",
1634
+ ...commands.map((command, index) => ` && ${command}${index < commands.length - 1 ? " \\" : ""}`),
1635
+ "USER root",
1636
+ ""
1637
+ ];
1638
+ }
1639
+ function renderAgentEntrypointSetup(agents) {
1640
+ if (!agents.some((agent) => agent.name === "agy")) return [];
1641
+ return [
1642
+ "# Link shared Agent Skills",
1643
+ "if [ ! -L \"/home/devbox/.gemini/skills\" ]; then",
1644
+ " ln -sfn /home/devbox/.agents/skills /home/devbox/.gemini/skills",
1645
+ " chown -h devbox:devbox /home/devbox/.gemini/skills",
1646
+ "fi",
1647
+ ""
1648
+ ];
1649
+ }
1650
+ //#endregion
1651
+ //#region src/runtimes/node/build.ts
1652
+ function createNodeWorkspaceContribution(configuredReleaseLines) {
1653
+ const runtimes = configuredReleaseLines.map((releaseLine) => PACKAGED_NODE_RECIPES[releaseLine]);
1654
+ const buildRuntime = selectBuildNodeRuntime(runtimes);
1655
+ return {
1656
+ buildNodeRuntimeRoot: buildRuntime?.runtimeRoot,
1657
+ runtimeInstallations: runtimes.flatMap(renderNodeRuntimeStage),
1658
+ sandboxUserSetup: buildRuntime === void 0 ? [] : [
1659
+ "# Configure pnpm for the non-root Sandbox user",
1660
+ "RUN mkdir -p /home/devbox/.config/pnpm /home/devbox/.pnpm-store \\",
1661
+ " && printf 'storeDir: /home/devbox/.pnpm-store\\n' > /home/devbox/.config/pnpm/config.yaml \\",
1662
+ " && chown devbox:devbox /home/devbox/.config/pnpm/config.yaml",
1663
+ ""
1664
+ ],
1665
+ entrypointSetup: renderNodeEntrypointSetup(runtimes)
1666
+ };
1667
+ }
1668
+ function selectBuildNodeRuntime(runtimes) {
1669
+ let buildRuntime;
1670
+ for (const runtime of runtimes) if (buildRuntime === void 0 || Number.parseFloat(runtime.releaseLine) > Number.parseFloat(buildRuntime.releaseLine)) buildRuntime = runtime;
1671
+ return buildRuntime;
1672
+ }
1673
+ function renderNodeEntrypointSetup(runtimes) {
1674
+ if (runtimes.length === 0) return [];
1675
+ return [
1676
+ "# Set the Node.js release line to use",
1677
+ "NODE_VERSION=\"${NODE_VERSION:-}\"",
1678
+ "if [ -n \"$NODE_VERSION\" ]; then",
1679
+ " case \"$NODE_VERSION\" in",
1680
+ " *[!0-9]*)",
1681
+ " echo \"NODE_VERSION must be a numeric release line: $NODE_VERSION\" >&2",
1682
+ " exit 1",
1683
+ " ;;",
1684
+ " esac",
1685
+ "",
1686
+ " NODE_RUNTIME_ROOT=\"/opt/devbox/runtimes/node/$NODE_VERSION\"",
1687
+ " if [ ! -x \"$NODE_RUNTIME_ROOT/bin/node\" ]; then",
1688
+ " echo \"Node.js release line $NODE_VERSION is not installed.\" >&2",
1689
+ " exit 1",
1690
+ " fi",
1691
+ "",
1692
+ " if ! grep -q \"# Devbox\" /etc/bash.bashrc; then",
1693
+ " printf \"\\n# Devbox\\nexport PATH=\\\"$NODE_RUNTIME_ROOT/bin:\\$PATH\\\"\\n\" >> /etc/bash.bashrc",
1694
+ " fi",
1695
+ "",
1696
+ " export PATH=\"$NODE_RUNTIME_ROOT/bin:$PATH\"",
1697
+ "fi",
1698
+ ""
1699
+ ];
1700
+ }
1701
+ function renderNodeRuntimeStage(recipe) {
1702
+ const keys = recipe.trustedReleaseKeys.map((key) => ` ${key}`).join(" \\\n");
1703
+ return [
1704
+ `# Install Node.js ${recipe.releaseLine}`,
1705
+ "RUN ARCH= OPENSSL_ARCH= && dpkgArch=\"$(dpkg --print-architecture)\" \\",
1706
+ " && case \"${dpkgArch##*-}\" in \\",
1707
+ " amd64) ARCH='x64' OPENSSL_ARCH='linux-x86_64';; \\",
1708
+ " ppc64el) ARCH='ppc64le' OPENSSL_ARCH='linux-ppc64le';; \\",
1709
+ " s390x) ARCH='s390x' OPENSSL_ARCH='linux*-s390x';; \\",
1710
+ " arm64) ARCH='arm64' OPENSSL_ARCH='linux-aarch64';; \\",
1711
+ " *) echo \"unsupported architecture\"; exit 1 ;; \\",
1712
+ " esac \\",
1713
+ " && set -eux \\",
1714
+ ` && NODE_RUNTIME_VERSION=${recipe.version} \\`,
1715
+ ` && NODE_RUNTIME_ROOT=${recipe.runtimeRoot} \\`,
1716
+ " && mkdir -p \"$NODE_RUNTIME_ROOT\" /tmp/node-source \\",
1717
+ " && cd /tmp/node-source \\",
1718
+ " && export GNUPGHOME=\"$(mktemp -d)\" \\",
1719
+ " && for key in \\",
1720
+ `${keys} \\`,
1721
+ " ; do \\",
1722
+ " { gpg --batch --no-options --keyserver hkps://keys.openpgp.org --recv-keys \"$key\" && gpg --batch --no-options --fingerprint \"$key\"; } || \\",
1723
+ " { gpg --batch --no-options --keyserver keyserver.ubuntu.com --recv-keys \"$key\" && gpg --batch --no-options --fingerprint \"$key\"; }; \\",
1724
+ " done \\",
1725
+ " && archive=\"node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\" \\",
1726
+ ` && curl -fsSLO --compressed "${recipe.distributionBaseUrl}/v\${NODE_RUNTIME_VERSION}/node-v\${NODE_RUNTIME_VERSION}-linux-\${ARCH}.tar.xz" \\`,
1727
+ ` && curl -fsSLO --compressed "${recipe.distributionBaseUrl}/v\${NODE_RUNTIME_VERSION}/SHASUMS256.txt.asc" \\`,
1728
+ " && gpg --batch --no-options --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \\",
1729
+ " && gpgconf --kill all \\",
1730
+ " && rm -rf \"$GNUPGHOME\" \\",
1731
+ " && grep \" node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\\$\" SHASUMS256.txt | sha256sum -c - \\",
1732
+ " && tar -xJf \"node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\" -C \"$NODE_RUNTIME_ROOT\" --strip-components=1 --no-same-owner \\",
1733
+ " && rm \"node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\" SHASUMS256.txt.asc SHASUMS256.txt \\",
1734
+ " && find \"$NODE_RUNTIME_ROOT/include/node/openssl/archs\" -mindepth 1 -maxdepth 1 ! -name \"$OPENSSL_ARCH\" -exec rm -rf {} \\; \\",
1735
+ " && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \\",
1736
+ " && export PATH=\"$NODE_RUNTIME_ROOT/bin:$PATH\" \\",
1737
+ " && npm install -g npm \\",
1738
+ " && npm uninstall -g corepack \\",
1739
+ " && npm install -g yarn pnpm @antfu/ni --allow-scripts=pnpm,yarn \\",
1740
+ " && \"$NODE_RUNTIME_ROOT/bin/node\" --version \\",
1741
+ " && \"$NODE_RUNTIME_ROOT/bin/npm\" --version \\",
1742
+ " && rm -rf /tmp/node-source",
1743
+ ""
1744
+ ];
1745
+ }
1746
+ //#endregion
1657
1747
  //#region src/workspace/build.ts
1658
1748
  const BASE_WORKSPACE_IMAGE_DEFAULTS = [{
1659
1749
  relativePath: ".bash_aliases",
1660
- content: _devbox_workspace_image_default__bash_aliases_default
1750
+ 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
1751
  }, {
1662
1752
  relativePath: ".gitconfig",
1663
- content: _devbox_workspace_image_default__gitconfig_default
1753
+ 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
1754
  }];
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
1755
  async function buildWorkspace(input = {}) {
1674
1756
  return withStateLocks({
1675
1757
  devboxHome: input.devboxHome ?? join(homedir(), ".devbox"),
@@ -1691,7 +1773,7 @@ async function buildWorkspaceUnlocked(input) {
1691
1773
  kind: "operational",
1692
1774
  code: "global-configuration-missing",
1693
1775
  observed: "Global configuration is required before building the Workspace image.",
1694
- nextAction: "Run devbox config to commit Global configuration before devbox build."
1776
+ nextAction: "Run devbox init to commit Global configuration before devbox build."
1695
1777
  });
1696
1778
  }
1697
1779
  const parsedGlobal = parseGlobalConfiguration(globalSource, catalogCheck.value);
@@ -1699,27 +1781,25 @@ async function buildWorkspaceUnlocked(input) {
1699
1781
  const globalConfiguration = parsedGlobal.value;
1700
1782
  const hostIdentity = currentHostIdentity();
1701
1783
  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]);
1784
+ const node = createNodeWorkspaceContribution(globalConfiguration.node);
1785
+ const agent = createAgentWorkspaceContribution({
1786
+ configuredAgents: globalConfiguration.agent,
1787
+ notificationsEnabled: globalConfiguration.agent_notifications,
1788
+ buildNodeRuntimeRoot: node.buildNodeRuntimeRoot
1789
+ });
1790
+ const buildContextAssets = [...BASE_WORKSPACE_IMAGE_DEFAULTS, ...agent.buildContextAssets];
1708
1791
  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)
1792
+ node,
1793
+ agent
1714
1794
  });
1715
1795
  await mkdir(paths.buildContext, { recursive: true });
1716
1796
  await seedBuildContextAssets(paths.buildContext, buildContextAssets);
1717
1797
  await Promise.all([
1718
1798
  writeFile(join(paths.buildContext, "Dockerfile"), dockerfile),
1719
- writeFile(join(paths.buildContext, ".dockerignore"), renderDockerignore(buildContextAssets)),
1799
+ writeFile(join(paths.buildContext, ".dockerignore"), renderDockerignore(buildContextAssets, agent.dockerignoreEntries)),
1720
1800
  writeFile(join(paths.buildContext, "entrypoint.sh"), renderEntrypoint({
1721
- nodeRuntimes,
1722
- agents
1801
+ node,
1802
+ agent
1723
1803
  })),
1724
1804
  writeFile(join(paths.buildContext, "supervisord.conf"), renderSupervisorConfiguration())
1725
1805
  ]);
@@ -1733,14 +1813,6 @@ async function buildWorkspaceUnlocked(input) {
1733
1813
  if (!buildResult.ok) return buildResult;
1734
1814
  return success({ image: WORKSPACE_IMAGE });
1735
1815
  }
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
1816
  function currentHostIdentity() {
1745
1817
  const userId = typeof process.getuid === "function" ? process.getuid() : void 0;
1746
1818
  const groupId = typeof process.getgid === "function" ? process.getgid() : void 0;
@@ -1797,51 +1869,26 @@ function renderBuildDockerfile(input) {
1797
1869
  " && chmod 0440 /etc/sudoers.d/devbox",
1798
1870
  ""
1799
1871
  ];
1800
- for (const recipe of input.nodeRuntimes) lines.push(...renderNodeRuntimeStage(recipe));
1872
+ lines.push(...input.node.runtimeInstallations);
1801
1873
  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
- }
1812
- 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", "");
1874
+ lines.push(...input.node.sandboxUserSetup);
1875
+ lines.push(...input.agent.installationDockerfileLines);
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", "");
1877
+ lines.push(...input.agent.assetDockerfileLines);
1878
+ lines.push(...input.agent.notificationDockerfileLines);
1830
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\"]");
1831
1880
  return `${lines.join("\n")}\n`;
1832
1881
  }
1833
- function renderDockerignore(assets) {
1834
- const lines = [
1882
+ function renderDockerignore(assets, additionalEntries) {
1883
+ return `${[
1835
1884
  "# Machine-owned Workspace build context",
1836
1885
  "*",
1837
1886
  "!Dockerfile",
1838
1887
  "!entrypoint.sh",
1839
1888
  "!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`;
1889
+ ...assets.map((asset) => `!${asset.relativePath}`),
1890
+ ...additionalEntries
1891
+ ].join("\n")}\n`;
1845
1892
  }
1846
1893
  async function seedBuildContextAssets(buildContext, assets) {
1847
1894
  await Promise.all(assets.map(async (asset) => {
@@ -1854,18 +1901,18 @@ async function seedBuildContextAssets(buildContext, assets) {
1854
1901
  }
1855
1902
  }));
1856
1903
  }
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
1904
  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`;
1905
+ return `${[
1906
+ "#!/usr/bin/env bash",
1907
+ "",
1908
+ ...input.node.entrypointSetup,
1909
+ ...input.agent.entrypointSetup,
1910
+ "if [ $# -gt 0 ]; then",
1911
+ " exec gosu devbox \"$@\"",
1912
+ "else",
1913
+ " exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf",
1914
+ "fi"
1915
+ ].join("\n")}\n`;
1869
1916
  }
1870
1917
  function renderSupervisorConfiguration() {
1871
1918
  return `${[
@@ -1886,51 +1933,6 @@ function renderSupervisorConfiguration() {
1886
1933
  "stderr_logfile=/dev/null"
1887
1934
  ].join("\n")}\n`;
1888
1935
  }
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
1936
  async function runDockerBuild(invocation, signal) {
1935
1937
  const build = spawn("docker", [
1936
1938
  "build",
@@ -1987,7 +1989,7 @@ function createCli(signal, interactive) {
1987
1989
  outro("Project is already registered.");
1988
1990
  return success({});
1989
1991
  }
1990
- 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.`);
1991
1993
  return success({});
1992
1994
  });
1993
1995
  cli.command("config", "Edit Global or Local configuration for the current Project.").action(async () => {
@@ -1999,7 +2001,7 @@ function createCli(signal, interactive) {
1999
2001
  prompt
2000
2002
  });
2001
2003
  if (!result.ok) return result;
2002
- 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.");
2003
2005
  return success({});
2004
2006
  }
2005
2007
  const result = await configureLocalProject({
@@ -2018,7 +2020,7 @@ function createCli(signal, interactive) {
2018
2020
  if (!result.ok) return result;
2019
2021
  return success({ message: `Built Workspace image: ${result.value.image}` });
2020
2022
  });
2021
- 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 = {}) => {
2022
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.");
2023
2025
  const result = await removeProject({
2024
2026
  signal,
@@ -2028,22 +2030,6 @@ function createCli(signal, interactive) {
2028
2030
  if (!result.ok) return result;
2029
2031
  return success({ message: result.value.removed ? `Removed Project: ${result.value.root}` : "Project removal was not changed." });
2030
2032
  });
2031
- 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 = {}) => {
2032
- if (!options.missingProjects) return failure({
2033
- kind: "usage",
2034
- code: "cleanup-mode-required",
2035
- observed: "cleanup requires --missing-projects.",
2036
- nextAction: "Run devbox cleanup --missing-projects."
2037
- });
2038
- 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.");
2039
- const result = await cleanupMissingProjects({
2040
- signal,
2041
- confirm: prompt.confirm,
2042
- yes: options.yes
2043
- });
2044
- if (!result.ok) return result;
2045
- return success({ message: result.value.removed ? `Removed Missing-root Projects: ${result.value.roots.join(", ")}` : "No Missing-root Projects were removed." });
2046
- });
2047
2033
  for (const [command, description] of [
2048
2034
  ["up", "Start the current Project Sandbox."],
2049
2035
  ["down", "Stop and remove the current Project Sandbox."],
@@ -2056,12 +2042,13 @@ function createCli(signal, interactive) {
2056
2042
  const result = await runSandboxShell({ signal });
2057
2043
  return result.ok ? success({}) : result;
2058
2044
  });
2045
+ cli.version(version);
2059
2046
  cli.help();
2060
2047
  return cli;
2061
2048
  }
2062
2049
  function present(result) {
2063
2050
  if (!result.ok) {
2064
- 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`);
2065
2052
  return result.error.kind === "usage" ? 2 : 1;
2066
2053
  }
2067
2054
  if (result.value.message !== void 0) process.stdout.write(`${result.value.message}\n`);
@@ -2094,6 +2081,12 @@ async function main() {
2094
2081
  function isCacError(error) {
2095
2082
  return error instanceof Error && error.name === "CACError";
2096
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
+ }
2097
2090
  function interactiveFailure(observed, nextAction) {
2098
2091
  return failure({
2099
2092
  kind: "usage",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycs77/devbox",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "description": "A Lucas Yang's opinionated devbox for AI agents",
5
5
  "keywords": [
6
6
  "agent",