@ycs77/devbox 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -15
- package/dist/cli.mjs +276 -157
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# @ycs77/devbox
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**An opinionated devbox for AI agents.**
|
|
6
6
|
|
|
7
7
|
[![NPM version][ico-version]][link-npm]
|
|
8
8
|
[![Software License][ico-license]](LICENSE)
|
|
@@ -17,20 +17,20 @@ English | [繁體中文](README-zh-TW.md)
|
|
|
17
17
|
|
|
18
18
|
## Overview
|
|
19
19
|
|
|
20
|
-
Devbox gives each project an isolated Docker sandbox
|
|
20
|
+
Devbox gives each project an isolated Docker sandbox for AI agents, preconfigured with Lucas's development preferences.
|
|
21
21
|
|
|
22
22
|
## Features
|
|
23
23
|
|
|
24
24
|
- **Container environment for AI agents:** choose Claude Code, Codex, and more during setup, then use them inside the sandbox.
|
|
25
|
-
- **Simple
|
|
26
|
-
- **Flexible Node.js
|
|
25
|
+
- **Simple project sandbox management:** initialize, start, enter, stop, and remove a sandbox with a few commands.
|
|
26
|
+
- **Flexible Node.js versions:** choose the versions to include, then select one for each project.
|
|
27
27
|
- **Selective builds:** build only the Node.js versions and AI agents you choose.
|
|
28
28
|
- **No project configuration files:** keep Devbox configuration local to your machine, not in your repository.
|
|
29
29
|
|
|
30
30
|
## Requirements
|
|
31
31
|
|
|
32
|
-
- **Node.js 22 or later:**
|
|
33
|
-
- **Docker with Docker Compose:**
|
|
32
|
+
- **Node.js 22 or later:** required to install and run the CLI with npm.
|
|
33
|
+
- **Docker with Docker Compose:** required to run the development sandbox.
|
|
34
34
|
- **Windows:** use Devbox from a **WSL2** Linux distribution with Docker Desktop integration enabled.
|
|
35
35
|
|
|
36
36
|
## Getting started
|
|
@@ -47,9 +47,11 @@ From your project directory, initialize Devbox:
|
|
|
47
47
|
devbox init
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
The interactive setup lets you choose the Node.js
|
|
50
|
+
The interactive setup lets you choose the Node.js versions and AI agents to include, then select a Node.js version for the current project.
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
Global configuration controls what goes into the shared Workspace Image. Each Node.js version or AI agent you add makes the image larger and uses more disk space. Projects can use only the Node.js versions included in the image.
|
|
53
|
+
|
|
54
|
+
Build the shared Workspace Image once after setup or whenever you change global configuration:
|
|
53
55
|
|
|
54
56
|
```bash
|
|
55
57
|
devbox build
|
|
@@ -67,9 +69,9 @@ Open a shell inside the running sandbox and start working with your AI agent:
|
|
|
67
69
|
devbox sh
|
|
68
70
|
```
|
|
69
71
|
|
|
70
|
-
|
|
72
|
+
If you selected an agent during setup, run `claude`, `codex`, `agy`, or `omp` inside the shell.
|
|
71
73
|
|
|
72
|
-
##
|
|
74
|
+
## Commands
|
|
73
75
|
|
|
74
76
|
Run these commands from the initialized project directory:
|
|
75
77
|
|
|
@@ -79,23 +81,24 @@ Run these commands from the initialized project directory:
|
|
|
79
81
|
| `devbox sh` | Open a Bash shell in the running sandbox. |
|
|
80
82
|
| `devbox stop` | Stop the sandbox and preserve it for a later restart. |
|
|
81
83
|
| `devbox down` | Stop and remove the sandbox container. |
|
|
82
|
-
| `devbox config` | Change
|
|
84
|
+
| `devbox config` | Change global settings or this project's configuration. |
|
|
85
|
+
| `devbox cleanup --missing-projects` | Remove registrations for projects whose root directory no longer exists. |
|
|
83
86
|
|
|
84
87
|
## Troubleshooting
|
|
85
88
|
|
|
86
|
-
###
|
|
89
|
+
### Devbox reports that a command marker is in use
|
|
87
90
|
|
|
88
|
-
An interrupted
|
|
91
|
+
An interrupted command or sudden system shutdown can leave a command marker behind. First confirm that no other Devbox command is still running, then remove the stale marker and retry:
|
|
89
92
|
|
|
90
93
|
```bash
|
|
91
94
|
rm -rf ~/.devbox/locks/<marker>
|
|
92
95
|
```
|
|
93
96
|
|
|
94
|
-
|
|
97
|
+
A marker is a directory named `global` or `project-<hash>`. Do not remove a marker while its Devbox command is still running.
|
|
95
98
|
|
|
96
99
|
## Sponsor
|
|
97
100
|
|
|
98
|
-
If
|
|
101
|
+
If Devbox has helped you, consider sponsoring my work on Patreon. Your avatar will appear on my major projects.
|
|
99
102
|
|
|
100
103
|
<p align="center">
|
|
101
104
|
<a href="https://www.patreon.com/ycs77">
|
package/dist/cli.mjs
CHANGED
|
@@ -10,6 +10,9 @@ import { execFile, spawn } from "node:child_process";
|
|
|
10
10
|
import { promisify } from "node:util";
|
|
11
11
|
import { parse as parse$1, parseDocument, stringify } from "yaml";
|
|
12
12
|
import { once } from "node:events";
|
|
13
|
+
//#region package.json
|
|
14
|
+
var version = "0.6.0";
|
|
15
|
+
//#endregion
|
|
13
16
|
//#region src/catalog/index.ts
|
|
14
17
|
const NODE_RELEASE_KEYS = [
|
|
15
18
|
"5BE8A3F6C8A5C01D106C0AD820B1A390B168D356",
|
|
@@ -27,12 +30,14 @@ const PACKAGED_NODE_RECIPES = {
|
|
|
27
30
|
releaseLine: "24",
|
|
28
31
|
version: "24.19.0",
|
|
29
32
|
runtimeRoot: "/opt/devbox/runtimes/node/24",
|
|
33
|
+
distributionBaseUrl: "https://nodejs.org/dist",
|
|
30
34
|
trustedReleaseKeys: NODE_RELEASE_KEYS
|
|
31
35
|
},
|
|
32
36
|
"22": {
|
|
33
37
|
releaseLine: "22",
|
|
34
38
|
version: "22.23.2",
|
|
35
39
|
runtimeRoot: "/opt/devbox/runtimes/node/22",
|
|
40
|
+
distributionBaseUrl: "https://nodejs.org/dist",
|
|
36
41
|
trustedReleaseKeys: NODE_RELEASE_KEYS
|
|
37
42
|
}
|
|
38
43
|
};
|
|
@@ -467,10 +472,7 @@ function isMissingFileError$1(error) {
|
|
|
467
472
|
return typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
468
473
|
}
|
|
469
474
|
//#endregion
|
|
470
|
-
//#region src/
|
|
471
|
-
const WORKSPACE_IMAGE = "devbox-workspace:latest";
|
|
472
|
-
//#endregion
|
|
473
|
-
//#region src/project/compose-fragments.ts
|
|
475
|
+
//#region src/agent/compose.ts
|
|
474
476
|
const NOTIFICATION_FRAGMENT = {
|
|
475
477
|
environment: { PULSE_SERVER: "unix:/tmp/pulse-socket" },
|
|
476
478
|
volume: {
|
|
@@ -479,7 +481,7 @@ const NOTIFICATION_FRAGMENT = {
|
|
|
479
481
|
readOnly: true
|
|
480
482
|
}
|
|
481
483
|
};
|
|
482
|
-
function
|
|
484
|
+
function selectAgentComposeFragments(configuredAgents, notificationsEnabled) {
|
|
483
485
|
const agents = [];
|
|
484
486
|
let hasNotificationAgent = false;
|
|
485
487
|
for (const agent of configuredAgents) {
|
|
@@ -497,19 +499,26 @@ function selectComposeFragments({ selectedNode, configuredAgents, agentNotificat
|
|
|
497
499
|
}
|
|
498
500
|
}
|
|
499
501
|
return {
|
|
500
|
-
node: selectedNode === null ? void 0 : { environment: { NODE_VERSION: selectedNode } },
|
|
501
502
|
agents,
|
|
502
|
-
notification:
|
|
503
|
+
notification: notificationsEnabled && hasNotificationAgent ? NOTIFICATION_FRAGMENT : void 0
|
|
503
504
|
};
|
|
504
505
|
}
|
|
505
506
|
//#endregion
|
|
507
|
+
//#region src/runtimes/node/compose.ts
|
|
508
|
+
function selectNodeComposeFragment(selectedNode) {
|
|
509
|
+
return selectedNode === null ? void 0 : { environment: { NODE_VERSION: selectedNode } };
|
|
510
|
+
}
|
|
511
|
+
//#endregion
|
|
512
|
+
//#region src/workspace/image.ts
|
|
513
|
+
const WORKSPACE_IMAGE = "devbox-workspace:latest";
|
|
514
|
+
//#endregion
|
|
506
515
|
//#region src/project/compose.ts
|
|
507
516
|
function renderProjectCompose(input) {
|
|
508
|
-
const
|
|
517
|
+
const node = selectNodeComposeFragment(input.selectedNode);
|
|
518
|
+
const agent = selectAgentComposeFragments(input.configuredAgents, input.agentNotifications);
|
|
509
519
|
const environment = {
|
|
510
|
-
|
|
511
|
-
...
|
|
512
|
-
...fragments.notification?.environment
|
|
520
|
+
...node?.environment,
|
|
521
|
+
...agent.notification?.environment
|
|
513
522
|
};
|
|
514
523
|
const volumes = [
|
|
515
524
|
{
|
|
@@ -517,7 +526,7 @@ function renderProjectCompose(input) {
|
|
|
517
526
|
source: input.projectRoot,
|
|
518
527
|
target: `/workspace/${input.sandboxName}`
|
|
519
528
|
},
|
|
520
|
-
...
|
|
529
|
+
...agent.agents.map((fragment) => ({
|
|
521
530
|
type: "volume",
|
|
522
531
|
source: fragment.volume.name,
|
|
523
532
|
target: fragment.volume.target
|
|
@@ -527,11 +536,11 @@ function renderProjectCompose(input) {
|
|
|
527
536
|
source: input.claudeHostConfiguration,
|
|
528
537
|
target: "/home/devbox/.claude.json"
|
|
529
538
|
}],
|
|
530
|
-
...
|
|
539
|
+
...agent.notification === void 0 ? [] : [{
|
|
531
540
|
type: "bind",
|
|
532
|
-
source:
|
|
533
|
-
target:
|
|
534
|
-
read_only:
|
|
541
|
+
source: agent.notification.volume.source,
|
|
542
|
+
target: agent.notification.volume.target,
|
|
543
|
+
read_only: agent.notification.volume.readOnly
|
|
535
544
|
}]
|
|
536
545
|
];
|
|
537
546
|
const definition = {
|
|
@@ -551,7 +560,7 @@ function renderProjectCompose(input) {
|
|
|
551
560
|
...Object.keys(environment).length === 0 ? {} : { environment },
|
|
552
561
|
volumes
|
|
553
562
|
} },
|
|
554
|
-
...
|
|
563
|
+
...agent.agents.length === 0 ? {} : { volumes: Object.fromEntries(agent.agents.map((fragment) => [fragment.volume.name, {
|
|
555
564
|
name: fragment.volume.name,
|
|
556
565
|
external: fragment.volume.external
|
|
557
566
|
}])) }
|
|
@@ -1191,9 +1200,12 @@ async function runSandboxLifecycle(command, input = {}) {
|
|
|
1191
1200
|
global: false,
|
|
1192
1201
|
projectRoots: [projectRoot],
|
|
1193
1202
|
signal: input.signal
|
|
1194
|
-
}, () =>
|
|
1203
|
+
}, () => runSandboxCommandUnlocked(command, input, projectRoot));
|
|
1195
1204
|
}
|
|
1196
|
-
async function
|
|
1205
|
+
async function runSandboxShell(input = {}) {
|
|
1206
|
+
return runSandboxCommandUnlocked("sh", input, input.root ?? process.cwd());
|
|
1207
|
+
}
|
|
1208
|
+
async function runSandboxCommandUnlocked(command, input, projectRoot) {
|
|
1197
1209
|
if (input.signal?.aborted) throw new InterruptedError();
|
|
1198
1210
|
const rootCheck = await validateProjectRoot(projectRoot);
|
|
1199
1211
|
if (!rootCheck.ok) return rootCheck;
|
|
@@ -1532,18 +1544,20 @@ function createConfigurationPrompter({ signal, input, output }) {
|
|
|
1532
1544
|
}));
|
|
1533
1545
|
},
|
|
1534
1546
|
selectConfigurationScope: async () => promptValue(p.select({
|
|
1535
|
-
message: "
|
|
1547
|
+
message: "Where should this configuration apply?",
|
|
1536
1548
|
options: [{
|
|
1537
1549
|
value: "local",
|
|
1538
|
-
label: "Current
|
|
1550
|
+
label: "Current project",
|
|
1551
|
+
hint: "Enabled when this project’s container starts."
|
|
1539
1552
|
}, {
|
|
1540
1553
|
value: "global",
|
|
1541
|
-
label: "
|
|
1554
|
+
label: "Workspace Docker image",
|
|
1555
|
+
hint: "Built into the shared image. It affects image size and disk usage."
|
|
1542
1556
|
}],
|
|
1543
1557
|
...common
|
|
1544
1558
|
})),
|
|
1545
1559
|
editGlobal: async (configuration, catalog) => {
|
|
1546
|
-
p.log.step(`${c.bold(c.cyan("
|
|
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);
|
|
1547
1561
|
return {
|
|
1548
1562
|
version: 1,
|
|
1549
1563
|
node: await promptValue(p.multiselect({
|
|
@@ -1568,7 +1582,7 @@ function createConfigurationPrompter({ signal, input, output }) {
|
|
|
1568
1582
|
};
|
|
1569
1583
|
},
|
|
1570
1584
|
editLocal: async (configuration, catalog, globalConfiguration, editPorts = true) => {
|
|
1571
|
-
p.log.step(`${c.bold(c.cyan("Project configuration"))}\n${c.dim(editPorts ? "Choose the
|
|
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);
|
|
1572
1586
|
const node = await promptValue(p.select({
|
|
1573
1587
|
message: "Selected Node release line",
|
|
1574
1588
|
options: [{
|
|
@@ -1638,34 +1652,205 @@ function showLogo() {
|
|
|
1638
1652
|
for (let i = 0; i < ASCII_ART.length; i++) console.log(`${COLORS[i]}${ASCII_ART[i]}${REST}`);
|
|
1639
1653
|
}
|
|
1640
1654
|
//#endregion
|
|
1641
|
-
//#region \0devbox-
|
|
1642
|
-
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";
|
|
1643
|
-
//#endregion
|
|
1644
|
-
//#region \0devbox-workspace-image-default:.claude/settings.json.mjs
|
|
1655
|
+
//#region \0devbox-image-default:/home/runner/work/devbox/devbox/src/agent/image-defaults/.claude/settings.json.mjs
|
|
1645
1656
|
var settings_json_default = "{\n \"$schema\": \"https://json.schemastore.org/claude-code-settings.json\",\n \"env\": {\n \"CLAUDE_CODE_USE_POWERSHELL_TOOL\": \"0\"\n },\n \"attribution\": {\n \"commit\": \"\",\n \"pr\": \"\"\n },\n \"includeGitInstructions\": false,\n \"autoMemoryEnabled\": false\n}\n";
|
|
1646
1657
|
//#endregion
|
|
1647
|
-
//#region \0devbox-
|
|
1648
|
-
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";
|
|
1649
|
-
//#endregion
|
|
1650
|
-
//#region \0devbox-workspace-image-default:.omp/agent/config.yml.mjs
|
|
1658
|
+
//#region \0devbox-image-default:/home/runner/work/devbox/devbox/src/agent/image-defaults/.omp/agent/config.yml.mjs
|
|
1651
1659
|
var config_yml_default = "setupVersion: 2\n\n# Basic\nmodelRoles:\n default: openai-codex/gpt-5.6-terra:high\n smol: openai-codex/gpt-5.6-luna:high\n slow: openai-codex/gpt-5.6-sol:max\n plan: openai-codex/gpt-5.6-sol:max\nsymbolPreset: unicode\n\n# Theme\ntheme:\n dark: titanium\n light: light\ncomposer:\n shape: claude\n\n# Auto QA\ndev:\n autoqa: false\n autoqaConsent: denied\n";
|
|
1652
1660
|
//#endregion
|
|
1661
|
+
//#region src/agent/build.ts
|
|
1662
|
+
function createAgentWorkspaceContribution(input) {
|
|
1663
|
+
const agents = input.configuredAgents.map((name) => ({
|
|
1664
|
+
name,
|
|
1665
|
+
recipe: PACKAGED_AGENTS[name]
|
|
1666
|
+
}));
|
|
1667
|
+
const buildContextAssets = selectBuildContextAssets(agents);
|
|
1668
|
+
const skillAgents = agents.filter((agent) => agent.recipe.supportsSkillInstallation).map((agent) => agent.name);
|
|
1669
|
+
return {
|
|
1670
|
+
buildContextAssets,
|
|
1671
|
+
dockerignoreEntries: selectDockerignoreEntries(buildContextAssets),
|
|
1672
|
+
installationDockerfileLines: renderAgentInstallations(agents, input.buildNodeRuntimeRoot, skillAgents),
|
|
1673
|
+
assetDockerfileLines: renderAgentAssets(agents),
|
|
1674
|
+
notificationDockerfileLines: renderAgentNotifications(agents, input.notificationsEnabled),
|
|
1675
|
+
entrypointSetup: renderAgentEntrypointSetup(agents)
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
function renderAgentInstallations(agents, buildNodeRuntimeRoot, skillAgents) {
|
|
1679
|
+
const lines = [];
|
|
1680
|
+
for (const agent of agents) {
|
|
1681
|
+
const preInstallCommands = agent.recipe.installation.preInstallCommands ?? [];
|
|
1682
|
+
lines.push(`# Install ${agent.name}`, `RUN mkdir -p ${agent.recipe.home.target} \\`, ...preInstallCommands.map((command) => ` && ${command} \\`), ` && curl -fsSL ${agent.recipe.installation.url} | ${agent.recipe.installation.shell}`, "");
|
|
1683
|
+
}
|
|
1684
|
+
if (agents.length > 0) lines.push("# Set the PATH to include AI tools", "ENV PATH=\"/home/devbox/.local/bin:${PATH}\"", "");
|
|
1685
|
+
if (buildNodeRuntimeRoot !== void 0 && skillAgents.length > 0) {
|
|
1686
|
+
const skillAgentArguments = skillAgents.map((agent) => `-a ${agent}`).join(" ");
|
|
1687
|
+
lines.push("# Install Agent Skills", "RUN set -eux \\", " && mkdir -p /home/devbox/.agents/skills \\", ` && export PATH="${buildNodeRuntimeRoot}/bin:$PATH" \\`, ` && npx -y skills add ycs77/skills -g ${skillAgentArguments} -s '*' -y`, "");
|
|
1688
|
+
}
|
|
1689
|
+
return lines;
|
|
1690
|
+
}
|
|
1691
|
+
function selectDockerignoreEntries(assets) {
|
|
1692
|
+
const entries = [];
|
|
1693
|
+
if (assets.some((asset) => asset.relativePath === ".claude/settings.json")) entries.push("!.claude", "!.claude/settings.json");
|
|
1694
|
+
if (assets.some((asset) => asset.relativePath === ".omp/agent/config.yml")) entries.push("!.omp", "!.omp/agent", "!.omp/agent/config.yml");
|
|
1695
|
+
return entries;
|
|
1696
|
+
}
|
|
1697
|
+
function selectBuildContextAssets(agents) {
|
|
1698
|
+
const assets = [];
|
|
1699
|
+
if (agents.some((agent) => agent.name === "claude-code")) assets.push({
|
|
1700
|
+
relativePath: ".claude/settings.json",
|
|
1701
|
+
content: settings_json_default
|
|
1702
|
+
});
|
|
1703
|
+
if (agents.some((agent) => agent.name === "omp")) assets.push({
|
|
1704
|
+
relativePath: ".omp/agent/config.yml",
|
|
1705
|
+
content: config_yml_default
|
|
1706
|
+
});
|
|
1707
|
+
return assets;
|
|
1708
|
+
}
|
|
1709
|
+
function renderAgentAssets(agents) {
|
|
1710
|
+
const hasClaudeCode = agents.some((agent) => agent.name === "claude-code");
|
|
1711
|
+
const hasOmp = agents.some((agent) => agent.name === "omp");
|
|
1712
|
+
if (!hasClaudeCode && !hasOmp) return [];
|
|
1713
|
+
const ownershipCommands = [];
|
|
1714
|
+
const lines = ["# Copy AI dotfiles"];
|
|
1715
|
+
if (hasClaudeCode) {
|
|
1716
|
+
lines.push("COPY .claude/settings.json /home/devbox/.claude/settings.json");
|
|
1717
|
+
ownershipCommands.push("chown devbox:devbox /home/devbox/.claude/settings.json");
|
|
1718
|
+
}
|
|
1719
|
+
if (hasOmp) {
|
|
1720
|
+
lines.push("COPY .omp/agent/config.yml /home/devbox/.omp/agent/config.yml");
|
|
1721
|
+
ownershipCommands.push("chown devbox:devbox /home/devbox/.omp/agent", "chown devbox:devbox /home/devbox/.omp/agent/config.yml");
|
|
1722
|
+
}
|
|
1723
|
+
lines.push(`RUN ${ownershipCommands.join(" \\\n && ")}`, "");
|
|
1724
|
+
return lines;
|
|
1725
|
+
}
|
|
1726
|
+
function renderAgentNotifications(agents, notificationsEnabled) {
|
|
1727
|
+
const commands = notificationsEnabled ? agents.flatMap((agent) => agent.recipe.supportsNotifications ? agent.recipe.notificationInstallationCommands : []) : [];
|
|
1728
|
+
if (commands.length === 0) return [];
|
|
1729
|
+
return [
|
|
1730
|
+
"# Install Agent Notification Plugins",
|
|
1731
|
+
"USER devbox",
|
|
1732
|
+
"RUN set -eux \\",
|
|
1733
|
+
...commands.map((command, index) => ` && ${command}${index < commands.length - 1 ? " \\" : ""}`),
|
|
1734
|
+
"USER root",
|
|
1735
|
+
""
|
|
1736
|
+
];
|
|
1737
|
+
}
|
|
1738
|
+
function renderAgentEntrypointSetup(agents) {
|
|
1739
|
+
if (!agents.some((agent) => agent.name === "agy")) return [];
|
|
1740
|
+
return [
|
|
1741
|
+
"# Link shared Agent Skills",
|
|
1742
|
+
"if [ ! -L \"/home/devbox/.gemini/skills\" ]; then",
|
|
1743
|
+
" ln -sfn /home/devbox/.agents/skills /home/devbox/.gemini/skills",
|
|
1744
|
+
" chown -h devbox:devbox /home/devbox/.gemini/skills",
|
|
1745
|
+
"fi",
|
|
1746
|
+
""
|
|
1747
|
+
];
|
|
1748
|
+
}
|
|
1749
|
+
//#endregion
|
|
1750
|
+
//#region src/runtimes/node/build.ts
|
|
1751
|
+
function createNodeWorkspaceContribution(configuredReleaseLines) {
|
|
1752
|
+
const runtimes = configuredReleaseLines.map((releaseLine) => PACKAGED_NODE_RECIPES[releaseLine]);
|
|
1753
|
+
const buildRuntime = selectBuildNodeRuntime(runtimes);
|
|
1754
|
+
return {
|
|
1755
|
+
buildNodeRuntimeRoot: buildRuntime?.runtimeRoot,
|
|
1756
|
+
runtimeInstallations: runtimes.flatMap(renderNodeRuntimeStage),
|
|
1757
|
+
sandboxUserSetup: buildRuntime === void 0 ? [] : [
|
|
1758
|
+
"# Configure pnpm for the non-root Sandbox user",
|
|
1759
|
+
"RUN mkdir -p /home/devbox/.config/pnpm /home/devbox/.pnpm-store \\",
|
|
1760
|
+
" && printf 'storeDir: /home/devbox/.pnpm-store\\n' > /home/devbox/.config/pnpm/config.yaml \\",
|
|
1761
|
+
" && chown devbox:devbox /home/devbox/.config/pnpm/config.yaml",
|
|
1762
|
+
""
|
|
1763
|
+
],
|
|
1764
|
+
entrypointSetup: renderNodeEntrypointSetup(runtimes)
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
function selectBuildNodeRuntime(runtimes) {
|
|
1768
|
+
let buildRuntime;
|
|
1769
|
+
for (const runtime of runtimes) if (buildRuntime === void 0 || Number.parseFloat(runtime.releaseLine) > Number.parseFloat(buildRuntime.releaseLine)) buildRuntime = runtime;
|
|
1770
|
+
return buildRuntime;
|
|
1771
|
+
}
|
|
1772
|
+
function renderNodeEntrypointSetup(runtimes) {
|
|
1773
|
+
if (runtimes.length === 0) return [];
|
|
1774
|
+
return [
|
|
1775
|
+
"# Set the Node.js release line to use",
|
|
1776
|
+
"NODE_VERSION=\"${NODE_VERSION:-}\"",
|
|
1777
|
+
"if [ -n \"$NODE_VERSION\" ]; then",
|
|
1778
|
+
" case \"$NODE_VERSION\" in",
|
|
1779
|
+
" *[!0-9]*)",
|
|
1780
|
+
" echo \"NODE_VERSION must be a numeric release line: $NODE_VERSION\" >&2",
|
|
1781
|
+
" exit 1",
|
|
1782
|
+
" ;;",
|
|
1783
|
+
" esac",
|
|
1784
|
+
"",
|
|
1785
|
+
" NODE_RUNTIME_ROOT=\"/opt/devbox/runtimes/node/$NODE_VERSION\"",
|
|
1786
|
+
" if [ ! -x \"$NODE_RUNTIME_ROOT/bin/node\" ]; then",
|
|
1787
|
+
" echo \"Node.js release line $NODE_VERSION is not installed.\" >&2",
|
|
1788
|
+
" exit 1",
|
|
1789
|
+
" fi",
|
|
1790
|
+
"",
|
|
1791
|
+
" if ! grep -q \"# Devbox\" /etc/bash.bashrc; then",
|
|
1792
|
+
" printf \"\\n# Devbox\\nexport PATH=\\\"$NODE_RUNTIME_ROOT/bin:\\$PATH\\\"\\n\" >> /etc/bash.bashrc",
|
|
1793
|
+
" fi",
|
|
1794
|
+
"",
|
|
1795
|
+
" export PATH=\"$NODE_RUNTIME_ROOT/bin:$PATH\"",
|
|
1796
|
+
"fi",
|
|
1797
|
+
""
|
|
1798
|
+
];
|
|
1799
|
+
}
|
|
1800
|
+
function renderNodeRuntimeStage(recipe) {
|
|
1801
|
+
const keys = recipe.trustedReleaseKeys.map((key) => ` ${key}`).join(" \\\n");
|
|
1802
|
+
return [
|
|
1803
|
+
`# Install Node.js ${recipe.releaseLine}`,
|
|
1804
|
+
"RUN ARCH= OPENSSL_ARCH= && dpkgArch=\"$(dpkg --print-architecture)\" \\",
|
|
1805
|
+
" && case \"${dpkgArch##*-}\" in \\",
|
|
1806
|
+
" amd64) ARCH='x64' OPENSSL_ARCH='linux-x86_64';; \\",
|
|
1807
|
+
" ppc64el) ARCH='ppc64le' OPENSSL_ARCH='linux-ppc64le';; \\",
|
|
1808
|
+
" s390x) ARCH='s390x' OPENSSL_ARCH='linux*-s390x';; \\",
|
|
1809
|
+
" arm64) ARCH='arm64' OPENSSL_ARCH='linux-aarch64';; \\",
|
|
1810
|
+
" *) echo \"unsupported architecture\"; exit 1 ;; \\",
|
|
1811
|
+
" esac \\",
|
|
1812
|
+
" && set -eux \\",
|
|
1813
|
+
` && NODE_RUNTIME_VERSION=${recipe.version} \\`,
|
|
1814
|
+
` && NODE_RUNTIME_ROOT=${recipe.runtimeRoot} \\`,
|
|
1815
|
+
" && mkdir -p \"$NODE_RUNTIME_ROOT\" /tmp/node-source \\",
|
|
1816
|
+
" && cd /tmp/node-source \\",
|
|
1817
|
+
" && export GNUPGHOME=\"$(mktemp -d)\" \\",
|
|
1818
|
+
" && for key in \\",
|
|
1819
|
+
`${keys} \\`,
|
|
1820
|
+
" ; do \\",
|
|
1821
|
+
" { gpg --batch --no-options --keyserver hkps://keys.openpgp.org --recv-keys \"$key\" && gpg --batch --no-options --fingerprint \"$key\"; } || \\",
|
|
1822
|
+
" { gpg --batch --no-options --keyserver keyserver.ubuntu.com --recv-keys \"$key\" && gpg --batch --no-options --fingerprint \"$key\"; }; \\",
|
|
1823
|
+
" done \\",
|
|
1824
|
+
" && archive=\"node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\" \\",
|
|
1825
|
+
` && curl -fsSLO --compressed "${recipe.distributionBaseUrl}/v\${NODE_RUNTIME_VERSION}/node-v\${NODE_RUNTIME_VERSION}-linux-\${ARCH}.tar.xz" \\`,
|
|
1826
|
+
` && curl -fsSLO --compressed "${recipe.distributionBaseUrl}/v\${NODE_RUNTIME_VERSION}/SHASUMS256.txt.asc" \\`,
|
|
1827
|
+
" && gpg --batch --no-options --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \\",
|
|
1828
|
+
" && gpgconf --kill all \\",
|
|
1829
|
+
" && rm -rf \"$GNUPGHOME\" \\",
|
|
1830
|
+
" && grep \" node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\\$\" SHASUMS256.txt | sha256sum -c - \\",
|
|
1831
|
+
" && tar -xJf \"node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\" -C \"$NODE_RUNTIME_ROOT\" --strip-components=1 --no-same-owner \\",
|
|
1832
|
+
" && rm \"node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\" SHASUMS256.txt.asc SHASUMS256.txt \\",
|
|
1833
|
+
" && find \"$NODE_RUNTIME_ROOT/include/node/openssl/archs\" -mindepth 1 -maxdepth 1 ! -name \"$OPENSSL_ARCH\" -exec rm -rf {} \\; \\",
|
|
1834
|
+
" && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \\",
|
|
1835
|
+
" && export PATH=\"$NODE_RUNTIME_ROOT/bin:$PATH\" \\",
|
|
1836
|
+
" && npm install -g npm \\",
|
|
1837
|
+
" && npm uninstall -g corepack \\",
|
|
1838
|
+
" && npm install -g yarn pnpm @antfu/ni --allow-scripts=pnpm,yarn \\",
|
|
1839
|
+
" && \"$NODE_RUNTIME_ROOT/bin/node\" --version \\",
|
|
1840
|
+
" && \"$NODE_RUNTIME_ROOT/bin/npm\" --version \\",
|
|
1841
|
+
" && rm -rf /tmp/node-source",
|
|
1842
|
+
""
|
|
1843
|
+
];
|
|
1844
|
+
}
|
|
1845
|
+
//#endregion
|
|
1653
1846
|
//#region src/workspace/build.ts
|
|
1654
1847
|
const BASE_WORKSPACE_IMAGE_DEFAULTS = [{
|
|
1655
1848
|
relativePath: ".bash_aliases",
|
|
1656
|
-
content:
|
|
1849
|
+
content: "alias ..='cd ..'\nalias ...='cd ../..'\nalias c='clear'\nalias g='git'\nalias ccc='claude --dangerously-skip-permissions'\nalias ccx='codex --sandbox workspace-write --ask-for-approval never'\nalias ggg='agy --dangerously-skip-permissions'\n\nfunction mkcd() {\n mkdir \"$1\"\n cd \"$1\"\n}\n\n# Laravel Artisan\nalias pa='php artisan'\nalias pav='php artisan serve'\nalias pam='php artisan migrate'\nalias pams='php artisan migrate --seed'\nalias pamr='php artisan migrate:rollback'\nalias pamf='php artisan migrate:fresh'\nalias pamfs='php artisan migrate:fresh --seed'\nalias pas='php artisan db:seed'\nalias par='php artisan route:list'\nalias paq='php artisan queue:work'\nalias pah='php artisan horizon'\nalias pat='php artisan tinker'\nalias phpunit='./vendor/bin/phpunit --colors=always'\nalias pu='phpunit'\nalias pest='./vendor/bin/pest --colors=always'\nalias p='pest'\nalias pint='./vendor/bin/pint'\nalias phpstan='./vendor/bin/phpstan analyse --ansi'\nalias rector='./vendor/bin/rector'\n\n# NPM (requires @antfu/ni)\nalias nv='nr dev' # nd exists in @antfu/ni\nalias nb='nr build'\nalias nll='nr lint' # nl is a Linux command\nalias nlf='nr lint --fix'\nalias nf='nr fmt'\nalias nfc='nr fmt --check'\nalias nc='nr check'\nalias nt='nr test'\nalias ntr='nr test --run'\n"
|
|
1657
1850
|
}, {
|
|
1658
1851
|
relativePath: ".gitconfig",
|
|
1659
|
-
content:
|
|
1852
|
+
content: "[core]\n autocrlf = input\n fileMode = false\n pager = less -r\n quotepath = false\n[init]\n defaultBranch = main\n[color]\n diff = auto\n status = auto\n branch = auto\n[alias]\n cf = config\n cfl = config --list\n s = status\n a = add .\n l = log --oneline --graph\n la = log --oneline --graph --all\n last = log -1 HEAD\n c = commit\n cm = commit -m\n ca = commit --amend -m\n can = commit --amend --no-edit\n co = checkout\n cp = cherry-pick\n b = branch\n m = merge\n mnf = merge --no-ff\n rs = reset\n rs0h = reset HEAD --hard\n rs1 = reset HEAD~1\n rs1h = reset HEAD~1 --hard\n rb = rebase\n rbo = rebase --onto\n rbi = rebase -i\n rbc = rebase --continue\n rba = rebase --abort\n rm = remote\n r = remote -v\n ra = remote add\n rr = remote remove\n rp = remote prune\n f = fetch\n fp = fetch --prune\n p = push\n pl = pull\n"
|
|
1660
1853
|
}];
|
|
1661
|
-
const CLAUDE_SETTINGS_DEFAULT = {
|
|
1662
|
-
relativePath: ".claude/settings.json",
|
|
1663
|
-
content: settings_json_default
|
|
1664
|
-
};
|
|
1665
|
-
const OMP_AGENT_CONFIG_DEFAULT = {
|
|
1666
|
-
relativePath: ".omp/agent/config.yml",
|
|
1667
|
-
content: config_yml_default
|
|
1668
|
-
};
|
|
1669
1854
|
async function buildWorkspace(input = {}) {
|
|
1670
1855
|
return withStateLocks({
|
|
1671
1856
|
devboxHome: input.devboxHome ?? join(homedir(), ".devbox"),
|
|
@@ -1695,27 +1880,25 @@ async function buildWorkspaceUnlocked(input) {
|
|
|
1695
1880
|
const globalConfiguration = parsedGlobal.value;
|
|
1696
1881
|
const hostIdentity = currentHostIdentity();
|
|
1697
1882
|
if (!hostIdentity.ok) return hostIdentity;
|
|
1698
|
-
const
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1883
|
+
const node = createNodeWorkspaceContribution(globalConfiguration.node);
|
|
1884
|
+
const agent = createAgentWorkspaceContribution({
|
|
1885
|
+
configuredAgents: globalConfiguration.agent,
|
|
1886
|
+
notificationsEnabled: globalConfiguration.agent_notifications,
|
|
1887
|
+
buildNodeRuntimeRoot: node.buildNodeRuntimeRoot
|
|
1888
|
+
});
|
|
1889
|
+
const buildContextAssets = [...BASE_WORKSPACE_IMAGE_DEFAULTS, ...agent.buildContextAssets];
|
|
1704
1890
|
const dockerfile = renderBuildDockerfile({
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
agents,
|
|
1708
|
-
agentNotifications: globalConfiguration.agent_notifications,
|
|
1709
|
-
skillAgents: globalConfiguration.agent.filter((agent) => PACKAGED_AGENTS[agent]?.supportsSkillInstallation === true)
|
|
1891
|
+
node,
|
|
1892
|
+
agent
|
|
1710
1893
|
});
|
|
1711
1894
|
await mkdir(paths.buildContext, { recursive: true });
|
|
1712
1895
|
await seedBuildContextAssets(paths.buildContext, buildContextAssets);
|
|
1713
1896
|
await Promise.all([
|
|
1714
1897
|
writeFile(join(paths.buildContext, "Dockerfile"), dockerfile),
|
|
1715
|
-
writeFile(join(paths.buildContext, ".dockerignore"), renderDockerignore(buildContextAssets)),
|
|
1898
|
+
writeFile(join(paths.buildContext, ".dockerignore"), renderDockerignore(buildContextAssets, agent.dockerignoreEntries)),
|
|
1716
1899
|
writeFile(join(paths.buildContext, "entrypoint.sh"), renderEntrypoint({
|
|
1717
|
-
|
|
1718
|
-
|
|
1900
|
+
node,
|
|
1901
|
+
agent
|
|
1719
1902
|
})),
|
|
1720
1903
|
writeFile(join(paths.buildContext, "supervisord.conf"), renderSupervisorConfiguration())
|
|
1721
1904
|
]);
|
|
@@ -1729,14 +1912,6 @@ async function buildWorkspaceUnlocked(input) {
|
|
|
1729
1912
|
if (!buildResult.ok) return buildResult;
|
|
1730
1913
|
return success({ image: WORKSPACE_IMAGE });
|
|
1731
1914
|
}
|
|
1732
|
-
function buildNodeRuntimeRecipe(globalConfiguration) {
|
|
1733
|
-
let buildNodeRuntime;
|
|
1734
|
-
for (const releaseLine of globalConfiguration.node) {
|
|
1735
|
-
const recipe = PACKAGED_NODE_RECIPES[releaseLine];
|
|
1736
|
-
if (buildNodeRuntime === void 0 || Number.parseFloat(releaseLine) > Number.parseFloat(buildNodeRuntime.releaseLine)) buildNodeRuntime = recipe;
|
|
1737
|
-
}
|
|
1738
|
-
return buildNodeRuntime;
|
|
1739
|
-
}
|
|
1740
1915
|
function currentHostIdentity() {
|
|
1741
1916
|
const userId = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
1742
1917
|
const groupId = typeof process.getgid === "function" ? process.getgid() : void 0;
|
|
@@ -1762,6 +1937,9 @@ function renderBuildDockerfile(input) {
|
|
|
1762
1937
|
"ENV DEBIAN_FRONTEND=noninteractive",
|
|
1763
1938
|
"ENV TZ=UTC",
|
|
1764
1939
|
"ENV LANG=C.UTF-8",
|
|
1940
|
+
"ENV TERM=xterm-256color",
|
|
1941
|
+
"ENV EDITOR=vim",
|
|
1942
|
+
"ENV VISUAL=vim",
|
|
1765
1943
|
"",
|
|
1766
1944
|
"RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone",
|
|
1767
1945
|
"",
|
|
@@ -1790,51 +1968,26 @@ function renderBuildDockerfile(input) {
|
|
|
1790
1968
|
" && chmod 0440 /etc/sudoers.d/devbox",
|
|
1791
1969
|
""
|
|
1792
1970
|
];
|
|
1793
|
-
|
|
1971
|
+
lines.push(...input.node.runtimeInstallations);
|
|
1794
1972
|
lines.push("WORKDIR /workspace", "USER devbox", "");
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
const preInstallCommands = agent.recipe.installation.preInstallCommands ?? [];
|
|
1798
|
-
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}`, "");
|
|
1799
|
-
}
|
|
1800
|
-
if (input.agents.length > 0) lines.push("# Set the PATH to include AI tools", "ENV PATH=\"/home/devbox/.local/bin:${PATH}\"", "");
|
|
1801
|
-
if (input.buildNodeRuntime !== void 0 && input.skillAgents.length > 0) {
|
|
1802
|
-
const skillAgentArguments = input.skillAgents.map((agent) => `-a ${agent}`).join(" ");
|
|
1803
|
-
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`, "");
|
|
1804
|
-
}
|
|
1973
|
+
lines.push(...input.node.sandboxUserSetup);
|
|
1974
|
+
lines.push(...input.agent.installationDockerfileLines);
|
|
1805
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", "");
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
if (hasClaudeCode || hasOmp) {
|
|
1809
|
-
const ownershipCommands = [];
|
|
1810
|
-
lines.push("# Copy AI dotfiles");
|
|
1811
|
-
if (hasClaudeCode) {
|
|
1812
|
-
lines.push("COPY .claude/settings.json /home/devbox/.claude/settings.json");
|
|
1813
|
-
ownershipCommands.push("chown devbox:devbox /home/devbox/.claude/settings.json");
|
|
1814
|
-
}
|
|
1815
|
-
if (hasOmp) {
|
|
1816
|
-
lines.push("COPY .omp/agent/config.yml /home/devbox/.omp/agent/config.yml");
|
|
1817
|
-
ownershipCommands.push("chown devbox:devbox /home/devbox/.omp/agent", "chown devbox:devbox /home/devbox/.omp/agent/config.yml");
|
|
1818
|
-
}
|
|
1819
|
-
lines.push(`RUN ${ownershipCommands.join(" \\\n && ")}`, "");
|
|
1820
|
-
}
|
|
1821
|
-
const notificationInstallationCommands = input.agentNotifications ? input.agents.flatMap((agent) => agent.recipe.supportsNotifications ? agent.recipe.notificationInstallationCommands : []) : [];
|
|
1822
|
-
if (notificationInstallationCommands.length > 0) lines.push("# Install Agent Notification Plugins", "USER devbox", "RUN set -eux \\", ...notificationInstallationCommands.map((command, index) => ` && ${command}${index < notificationInstallationCommands.length - 1 ? " \\" : ""}`), "USER root", "");
|
|
1976
|
+
lines.push(...input.agent.assetDockerfileLines);
|
|
1977
|
+
lines.push(...input.agent.notificationDockerfileLines);
|
|
1823
1978
|
lines.push("COPY entrypoint.sh /usr/local/bin/entrypoint.sh", "COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf", "RUN chmod +x /usr/local/bin/entrypoint.sh", "", "ENTRYPOINT [\"/usr/local/bin/entrypoint.sh\"]");
|
|
1824
1979
|
return `${lines.join("\n")}\n`;
|
|
1825
1980
|
}
|
|
1826
|
-
function renderDockerignore(assets) {
|
|
1827
|
-
|
|
1981
|
+
function renderDockerignore(assets, additionalEntries) {
|
|
1982
|
+
return `${[
|
|
1828
1983
|
"# Machine-owned Workspace build context",
|
|
1829
1984
|
"*",
|
|
1830
1985
|
"!Dockerfile",
|
|
1831
1986
|
"!entrypoint.sh",
|
|
1832
1987
|
"!supervisord.conf",
|
|
1833
|
-
...assets.map((asset) => `!${asset.relativePath}`)
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
if (assets.some((asset) => asset.relativePath === ".omp/agent/config.yml")) lines.push("!.omp", "!.omp/agent", "!.omp/agent/config.yml");
|
|
1837
|
-
return `${lines.join("\n")}\n`;
|
|
1988
|
+
...assets.map((asset) => `!${asset.relativePath}`),
|
|
1989
|
+
...additionalEntries
|
|
1990
|
+
].join("\n")}\n`;
|
|
1838
1991
|
}
|
|
1839
1992
|
async function seedBuildContextAssets(buildContext, assets) {
|
|
1840
1993
|
await Promise.all(assets.map(async (asset) => {
|
|
@@ -1847,18 +2000,18 @@ async function seedBuildContextAssets(buildContext, assets) {
|
|
|
1847
2000
|
}
|
|
1848
2001
|
}));
|
|
1849
2002
|
}
|
|
1850
|
-
function selectBuildContextAssets(agents) {
|
|
1851
|
-
const assets = [...BASE_WORKSPACE_IMAGE_DEFAULTS];
|
|
1852
|
-
if (agents.some((agent) => agent.name === "claude-code")) assets.push(CLAUDE_SETTINGS_DEFAULT);
|
|
1853
|
-
if (agents.some((agent) => agent.name === "omp")) assets.push(OMP_AGENT_CONFIG_DEFAULT);
|
|
1854
|
-
return assets;
|
|
1855
|
-
}
|
|
1856
2003
|
function renderEntrypoint(input) {
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
2004
|
+
return `${[
|
|
2005
|
+
"#!/usr/bin/env bash",
|
|
2006
|
+
"",
|
|
2007
|
+
...input.node.entrypointSetup,
|
|
2008
|
+
...input.agent.entrypointSetup,
|
|
2009
|
+
"if [ $# -gt 0 ]; then",
|
|
2010
|
+
" exec gosu devbox \"$@\"",
|
|
2011
|
+
"else",
|
|
2012
|
+
" exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf",
|
|
2013
|
+
"fi"
|
|
2014
|
+
].join("\n")}\n`;
|
|
1862
2015
|
}
|
|
1863
2016
|
function renderSupervisorConfiguration() {
|
|
1864
2017
|
return `${[
|
|
@@ -1879,51 +2032,6 @@ function renderSupervisorConfiguration() {
|
|
|
1879
2032
|
"stderr_logfile=/dev/null"
|
|
1880
2033
|
].join("\n")}\n`;
|
|
1881
2034
|
}
|
|
1882
|
-
function renderNodeRuntimeStage(recipe) {
|
|
1883
|
-
const keys = recipe.trustedReleaseKeys.map((key) => ` ${key}`).join(" \\\n");
|
|
1884
|
-
return [
|
|
1885
|
-
`# Install Node.js ${recipe.releaseLine}`,
|
|
1886
|
-
"RUN ARCH= OPENSSL_ARCH= && dpkgArch=\"$(dpkg --print-architecture)\" \\",
|
|
1887
|
-
" && case \"${dpkgArch##*-}\" in \\",
|
|
1888
|
-
" amd64) ARCH='x64' OPENSSL_ARCH='linux-x86_64';; \\",
|
|
1889
|
-
" ppc64el) ARCH='ppc64le' OPENSSL_ARCH='linux-ppc64le';; \\",
|
|
1890
|
-
" s390x) ARCH='s390x' OPENSSL_ARCH='linux*-s390x';; \\",
|
|
1891
|
-
" arm64) ARCH='arm64' OPENSSL_ARCH='linux-aarch64';; \\",
|
|
1892
|
-
" *) echo \"unsupported architecture\"; exit 1 ;; \\",
|
|
1893
|
-
" esac \\",
|
|
1894
|
-
" && set -eux \\",
|
|
1895
|
-
` && NODE_RUNTIME_VERSION=${recipe.version} \\`,
|
|
1896
|
-
` && NODE_RUNTIME_ROOT=${recipe.runtimeRoot} \\`,
|
|
1897
|
-
" && mkdir -p \"$NODE_RUNTIME_ROOT\" /tmp/node-source \\",
|
|
1898
|
-
" && cd /tmp/node-source \\",
|
|
1899
|
-
" && export GNUPGHOME=\"$(mktemp -d)\" \\",
|
|
1900
|
-
" && for key in \\",
|
|
1901
|
-
`${keys} \\`,
|
|
1902
|
-
" ; do \\",
|
|
1903
|
-
" { gpg --batch --no-options --keyserver hkps://keys.openpgp.org --recv-keys \"$key\" && gpg --batch --no-options --fingerprint \"$key\"; } || \\",
|
|
1904
|
-
" { gpg --batch --no-options --keyserver keyserver.ubuntu.com --recv-keys \"$key\" && gpg --batch --no-options --fingerprint \"$key\"; }; \\",
|
|
1905
|
-
" done \\",
|
|
1906
|
-
" && archive=\"node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\" \\",
|
|
1907
|
-
" && curl -fsSLO --compressed \"https://nodejs.org/dist/v${NODE_RUNTIME_VERSION}/node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\" \\",
|
|
1908
|
-
" && curl -fsSLO --compressed \"https://nodejs.org/dist/v${NODE_RUNTIME_VERSION}/SHASUMS256.txt.asc\" \\",
|
|
1909
|
-
" && gpg --batch --no-options --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \\",
|
|
1910
|
-
" && gpgconf --kill all \\",
|
|
1911
|
-
" && rm -rf \"$GNUPGHOME\" \\",
|
|
1912
|
-
" && grep \" node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\\$\" SHASUMS256.txt | sha256sum -c - \\",
|
|
1913
|
-
" && tar -xJf \"node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\" -C \"$NODE_RUNTIME_ROOT\" --strip-components=1 --no-same-owner \\",
|
|
1914
|
-
" && rm \"node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\" SHASUMS256.txt.asc SHASUMS256.txt \\",
|
|
1915
|
-
" && find \"$NODE_RUNTIME_ROOT/include/node/openssl/archs\" -mindepth 1 -maxdepth 1 ! -name \"$OPENSSL_ARCH\" -exec rm -rf {} \\; \\",
|
|
1916
|
-
" && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \\",
|
|
1917
|
-
" && export PATH=\"$NODE_RUNTIME_ROOT/bin:$PATH\" \\",
|
|
1918
|
-
" && npm install -g npm \\",
|
|
1919
|
-
" && npm uninstall -g corepack \\",
|
|
1920
|
-
" && npm install -g yarn pnpm @antfu/ni --allow-scripts=pnpm,yarn \\",
|
|
1921
|
-
" && \"$NODE_RUNTIME_ROOT/bin/node\" --version \\",
|
|
1922
|
-
" && \"$NODE_RUNTIME_ROOT/bin/npm\" --version \\",
|
|
1923
|
-
" && rm -rf /tmp/node-source",
|
|
1924
|
-
""
|
|
1925
|
-
];
|
|
1926
|
-
}
|
|
1927
2035
|
async function runDockerBuild(invocation, signal) {
|
|
1928
2036
|
const build = spawn("docker", [
|
|
1929
2037
|
"build",
|
|
@@ -2040,12 +2148,16 @@ function createCli(signal, interactive) {
|
|
|
2040
2148
|
for (const [command, description] of [
|
|
2041
2149
|
["up", "Start the current Project Sandbox."],
|
|
2042
2150
|
["down", "Stop and remove the current Project Sandbox."],
|
|
2043
|
-
["stop", "Stop the current Project Sandbox."]
|
|
2044
|
-
["sh", "Open a shell in the current Project Sandbox."]
|
|
2151
|
+
["stop", "Stop the current Project Sandbox."]
|
|
2045
2152
|
]) cli.command(command, description).action(async () => {
|
|
2046
2153
|
const result = await runSandboxLifecycle(command, { signal });
|
|
2047
2154
|
return result.ok ? success({}) : result;
|
|
2048
2155
|
});
|
|
2156
|
+
cli.command("sh", "Open a shell in the current Project Sandbox.").action(async () => {
|
|
2157
|
+
const result = await runSandboxShell({ signal });
|
|
2158
|
+
return result.ok ? success({}) : result;
|
|
2159
|
+
});
|
|
2160
|
+
cli.version(version);
|
|
2049
2161
|
cli.help();
|
|
2050
2162
|
return cli;
|
|
2051
2163
|
}
|
|
@@ -2072,11 +2184,18 @@ async function main() {
|
|
|
2072
2184
|
return 130;
|
|
2073
2185
|
}
|
|
2074
2186
|
if (error instanceof HostCommandError) return error.exitCode;
|
|
2187
|
+
if (isCacError(error)) {
|
|
2188
|
+
process.stderr.write(`${error.message}\n`);
|
|
2189
|
+
return 2;
|
|
2190
|
+
}
|
|
2075
2191
|
throw error;
|
|
2076
2192
|
} finally {
|
|
2077
2193
|
process.off("SIGINT", interrupt);
|
|
2078
2194
|
}
|
|
2079
2195
|
}
|
|
2196
|
+
function isCacError(error) {
|
|
2197
|
+
return error instanceof Error && error.name === "CACError";
|
|
2198
|
+
}
|
|
2080
2199
|
function interactiveFailure(observed, nextAction) {
|
|
2081
2200
|
return failure({
|
|
2082
2201
|
kind: "usage",
|