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