@ycs77/devbox 0.2.0 → 0.3.1
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 +5 -5
- package/dist/cli.mjs +200 -43
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# @ycs77/devbox
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**A containerized devbox for AI agents.**
|
|
6
6
|
|
|
7
7
|
[![NPM version][ico-version]][link-npm]
|
|
8
8
|
[![Software License][ico-license]](LICENSE)
|
|
@@ -17,13 +17,13 @@ English | [繁體中文](README-zh-TW.md)
|
|
|
17
17
|
|
|
18
18
|
## Overview
|
|
19
19
|
|
|
20
|
-
Devbox gives each project an isolated Docker sandbox with Lucas's opinionated development setup. Choose
|
|
20
|
+
Devbox gives each project an isolated Docker sandbox designed for AI agents, with Lucas's opinionated development setup. Choose the Node.js release lines and AI agents you use, then work from a consistent container without configuring it yourself.
|
|
21
21
|
|
|
22
22
|
## Features
|
|
23
23
|
|
|
24
|
-
- **
|
|
25
|
-
- **Simple project lifecycle:** initialize, start, enter, stop, and remove a sandbox with a few commands.
|
|
26
|
-
- **
|
|
24
|
+
- **Container environment for AI agents:** choose Claude Code, Codex, and more during setup, then use them inside the sandbox.
|
|
25
|
+
- **Simple setup and project lifecycle:** initialize, start, enter, stop, and remove a sandbox with a few commands.
|
|
26
|
+
- **Flexible Node.js environments:** configure Node.js 22 and 24, then select a version for each project.
|
|
27
27
|
- **Selective builds:** build only the Node.js versions and AI agents you choose.
|
|
28
28
|
- **No project configuration files:** keep Devbox configuration local to your machine, not in your repository.
|
|
29
29
|
|
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { cac } from "cac";
|
|
2
1
|
import * as p from "@clack/prompts";
|
|
2
|
+
import { intro, outro } from "@clack/prompts";
|
|
3
|
+
import { cac } from "cac";
|
|
4
|
+
import c from "picocolors";
|
|
3
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
6
|
import { lstat, mkdir, readFile, rename, rm, rmdir, writeFile } from "node:fs/promises";
|
|
5
7
|
import { homedir } from "node:os";
|
|
@@ -744,7 +746,7 @@ async function initializeProjectUnlocked(input = {}) {
|
|
|
744
746
|
confirmed: true
|
|
745
747
|
});
|
|
746
748
|
}
|
|
747
|
-
if (!await (input.confirm ?? input.prompt?.confirm ?? (async () => true))(
|
|
749
|
+
if (!await (input.confirm ?? input.prompt?.confirm ?? (async () => true))("Save configuration?")) return success({
|
|
748
750
|
root: projectRoot,
|
|
749
751
|
stateDirectory,
|
|
750
752
|
sandboxName: name,
|
|
@@ -828,11 +830,13 @@ async function configureLocalProjectUnlocked(input = {}) {
|
|
|
828
830
|
root: projectRoot,
|
|
829
831
|
changed: false
|
|
830
832
|
});
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
833
|
+
if (!await (input.confirm ?? input.prompt?.confirm ?? (async () => true))("Save Project configuration?", {
|
|
834
|
+
title: "Review configuration",
|
|
835
|
+
sections: [{
|
|
836
|
+
title: `Project: ${projectRoot}`,
|
|
837
|
+
current: serializeLocalConfiguration(localCheck.value).trim(),
|
|
838
|
+
next: serializeLocalConfiguration(nextCheck.value).trim()
|
|
839
|
+
}]
|
|
836
840
|
})) return success({
|
|
837
841
|
scope: "local",
|
|
838
842
|
root: projectRoot,
|
|
@@ -926,15 +930,17 @@ async function configureGlobalUnlocked(input = {}, locks) {
|
|
|
926
930
|
scope: "global",
|
|
927
931
|
changed: false
|
|
928
932
|
});
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
933
|
+
if (!await (input.confirm ?? input.prompt?.confirm ?? (async () => true))("Save Global configuration?", {
|
|
934
|
+
title: "Review configuration",
|
|
935
|
+
sections: [{
|
|
936
|
+
title: "Global configuration",
|
|
937
|
+
current: serializeGlobalConfiguration(currentConfiguration).trim(),
|
|
938
|
+
next: serializeGlobalConfiguration(nextCheck.value).trim()
|
|
939
|
+
}, ...[...replacementConfigurations].map(([root, configuration]) => ({
|
|
940
|
+
title: `Project: ${root}`,
|
|
941
|
+
current: serializeLocalConfiguration(localConfigurations.get(root)).trim(),
|
|
942
|
+
next: serializeLocalConfiguration(configuration).trim()
|
|
943
|
+
}))]
|
|
938
944
|
})) return success({
|
|
939
945
|
scope: "global",
|
|
940
946
|
changed: false
|
|
@@ -1467,7 +1473,7 @@ function createConfigurationPrompter({ signal, input, output }) {
|
|
|
1467
1473
|
};
|
|
1468
1474
|
return {
|
|
1469
1475
|
confirm: async (message, details) => {
|
|
1470
|
-
if (details !== void 0) p.note(details
|
|
1476
|
+
if (details !== void 0) p.note(renderConfirmationDetails(details), c.bold(c.cyan(details.title)), common);
|
|
1471
1477
|
return promptValue(p.confirm({
|
|
1472
1478
|
message,
|
|
1473
1479
|
...common
|
|
@@ -1485,6 +1491,7 @@ function createConfigurationPrompter({ signal, input, output }) {
|
|
|
1485
1491
|
...common
|
|
1486
1492
|
})),
|
|
1487
1493
|
editGlobal: async (configuration, catalog) => {
|
|
1494
|
+
p.log.step(`${c.bold(c.cyan("Global configuration"))}\n${c.dim("Choose the runtimes and agents available to every Project.")}`, common);
|
|
1488
1495
|
return {
|
|
1489
1496
|
version: 1,
|
|
1490
1497
|
node: await promptValue(p.multiselect({
|
|
@@ -1509,6 +1516,7 @@ function createConfigurationPrompter({ signal, input, output }) {
|
|
|
1509
1516
|
};
|
|
1510
1517
|
},
|
|
1511
1518
|
editLocal: async (configuration, catalog, globalConfiguration) => {
|
|
1519
|
+
p.log.step(`${c.bold(c.cyan("Project configuration"))}\n${c.dim("Choose the runtime for this Project.")}`, common);
|
|
1512
1520
|
return {
|
|
1513
1521
|
version: 1,
|
|
1514
1522
|
node: await promptValue(p.select({
|
|
@@ -1527,13 +1535,69 @@ function createConfigurationPrompter({ signal, input, output }) {
|
|
|
1527
1535
|
}
|
|
1528
1536
|
};
|
|
1529
1537
|
}
|
|
1538
|
+
function renderConfirmationDetails(details) {
|
|
1539
|
+
return details.sections.map((section) => [
|
|
1540
|
+
c.bold(section.title),
|
|
1541
|
+
`${c.yellow(c.dim("Current"))}\n${section.current}`,
|
|
1542
|
+
`${c.bold(c.green("New"))}\n${section.next}`
|
|
1543
|
+
].join("\n\n")).join("\n\n");
|
|
1544
|
+
}
|
|
1530
1545
|
async function promptValue(prompt) {
|
|
1531
1546
|
const value = await prompt;
|
|
1532
1547
|
if (p.isCancel(value)) throw new InterruptedError();
|
|
1533
1548
|
return value;
|
|
1534
1549
|
}
|
|
1535
1550
|
//#endregion
|
|
1551
|
+
//#region src/logo.ts
|
|
1552
|
+
const ASCII_ART = [
|
|
1553
|
+
"██████╗ ███████╗██╗ ██╗██████╗ ██████╗ ██╗ ██╗",
|
|
1554
|
+
"██╔══██╗██╔════╝██║ ██║██╔══██╗██╔═══██╗╚██╗██╔╝",
|
|
1555
|
+
"██║ ██║█████╗ ██║ ██║██████╔╝██║ ██║ ╚███╔╝ ",
|
|
1556
|
+
"██║ ██║██╔══╝ ╚██╗ ██╔╝██╔══██╗██║ ██║ ██╔██╗ ",
|
|
1557
|
+
"██████╔╝███████╗ ╚████╔╝ ██████╔╝╚██████╔╝██╔╝ ██╗",
|
|
1558
|
+
"╚═════╝ ╚══════╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝"
|
|
1559
|
+
];
|
|
1560
|
+
const COLORS = [
|
|
1561
|
+
`\x1B[38;5;51m`,
|
|
1562
|
+
`\x1B[38;5;45m`,
|
|
1563
|
+
`\x1B[38;5;39m`,
|
|
1564
|
+
`\x1B[38;5;32m`,
|
|
1565
|
+
`\x1B[38;5;25m`,
|
|
1566
|
+
`\x1B[38;5;26m`
|
|
1567
|
+
];
|
|
1568
|
+
const REST = "\x1B[0m";
|
|
1569
|
+
function showLogo() {
|
|
1570
|
+
for (let i = 0; i < ASCII_ART.length; i++) console.log(`${COLORS[i]}${ASCII_ART[i]}${REST}`);
|
|
1571
|
+
}
|
|
1572
|
+
//#endregion
|
|
1573
|
+
//#region \0devbox-workspace-image-default:.bash_aliases.mjs
|
|
1574
|
+
var _devbox_workspace_image_default__bash_aliases_default = "alias ..='cd ..'\nalias ...='cd ../..'\nalias c='clear'\nalias g='git'\nalias ccc='claude --dangerously-skip-permissions'\nalias ccx='codex --sandbox workspace-write --ask-for-approval never'\nalias ggg='agy --dangerously-skip-permissions'\n\nfunction mkcd() {\n mkdir \"$1\"\n cd \"$1\"\n}\n\n# Laravel Artisan\nalias pa='php artisan'\nalias pav='php artisan serve'\nalias pam='php artisan migrate'\nalias pams='php artisan migrate --seed'\nalias pamr='php artisan migrate:rollback'\nalias pamf='php artisan migrate:fresh'\nalias pamfs='php artisan migrate:fresh --seed'\nalias pas='php artisan db:seed'\nalias par='php artisan route:list'\nalias paq='php artisan queue:work'\nalias pah='php artisan horizon'\nalias pat='php artisan tinker'\nalias phpunit='./vendor/bin/phpunit --colors=always'\nalias pu='phpunit'\nalias pest='./vendor/bin/pest --colors=always'\nalias p='pest'\nalias pint='./vendor/bin/pint'\nalias phpstan='./vendor/bin/phpstan analyse --ansi'\nalias rector='./vendor/bin/rector'\n\n# NPM (requires @antfu/ni)\nalias nv='nr dev' # nd exists in @antfu/ni\nalias nb='nr build'\nalias nll='nr lint' # nl is a Linux command\nalias nlf='nr lint --fix'\nalias nf='nr fmt'\nalias nfc='nr fmt --check'\nalias nc='nr check'\nalias nt='nr test'\nalias ntr='nr test --run'\n";
|
|
1575
|
+
//#endregion
|
|
1576
|
+
//#region \0devbox-workspace-image-default:.claude/settings.json.mjs
|
|
1577
|
+
var settings_json_default = "{\n \"$schema\": \"https://json.schemastore.org/claude-code-settings.json\",\n \"env\": {\n \"CLAUDE_CODE_USE_POWERSHELL_TOOL\": \"0\"\n },\n \"attribution\": {\n \"commit\": \"\",\n \"pr\": \"\"\n },\n \"includeGitInstructions\": false,\n \"autoMemoryEnabled\": false\n}\n";
|
|
1578
|
+
//#endregion
|
|
1579
|
+
//#region \0devbox-workspace-image-default:.gitconfig.mjs
|
|
1580
|
+
var _devbox_workspace_image_default__gitconfig_default = "[core]\n autocrlf = input\n fileMode = false\n pager = less -r\n quotepath = false\n[init]\n defaultBranch = main\n[color]\n diff = auto\n status = auto\n branch = auto\n[alias]\n cf = config\n cfl = config --list\n s = status\n a = add .\n l = log --oneline --graph\n la = log --oneline --graph --all\n last = log -1 HEAD\n c = commit\n cm = commit -m\n ca = commit --amend -m\n can = commit --amend --no-edit\n co = checkout\n cp = cherry-pick\n b = branch\n m = merge\n mnf = merge --no-ff\n rs = reset\n rs0h = reset HEAD --hard\n rs1 = reset HEAD~1\n rs1h = reset HEAD~1 --hard\n rb = rebase\n rbo = rebase --onto\n rbi = rebase -i\n rbc = rebase --continue\n rba = rebase --abort\n rm = remote\n r = remote -v\n ra = remote add\n rr = remote remove\n rp = remote prune\n f = fetch\n fp = fetch --prune\n p = push\n pl = pull\n";
|
|
1581
|
+
//#endregion
|
|
1582
|
+
//#region \0devbox-workspace-image-default:.omp/agent/config.yml.mjs
|
|
1583
|
+
var config_yml_default = "setupVersion: 2\n\n# Basic\nmodelRoles:\n default: openai-codex/gpt-5.6-terra:high\n smol: openai-codex/gpt-5.6-luna:high\n slow: openai-codex/gpt-5.6-sol:max\n plan: openai-codex/gpt-5.6-sol:max\nsymbolPreset: unicode\n\n# Theme\ntheme:\n dark: titanium\n light: light\ncomposer:\n shape: claude\n\n# Auto QA\ndev:\n autoqa: false\n autoqaConsent: denied\n";
|
|
1584
|
+
//#endregion
|
|
1536
1585
|
//#region src/workspace/build.ts
|
|
1586
|
+
const BASE_WORKSPACE_IMAGE_DEFAULTS = [{
|
|
1587
|
+
relativePath: ".bash_aliases",
|
|
1588
|
+
content: _devbox_workspace_image_default__bash_aliases_default
|
|
1589
|
+
}, {
|
|
1590
|
+
relativePath: ".gitconfig",
|
|
1591
|
+
content: _devbox_workspace_image_default__gitconfig_default
|
|
1592
|
+
}];
|
|
1593
|
+
const CLAUDE_SETTINGS_DEFAULT = {
|
|
1594
|
+
relativePath: ".claude/settings.json",
|
|
1595
|
+
content: settings_json_default
|
|
1596
|
+
};
|
|
1597
|
+
const OMP_AGENT_CONFIG_DEFAULT = {
|
|
1598
|
+
relativePath: ".omp/agent/config.yml",
|
|
1599
|
+
content: config_yml_default
|
|
1600
|
+
};
|
|
1537
1601
|
async function buildWorkspace(input = {}) {
|
|
1538
1602
|
return withStateLocks({
|
|
1539
1603
|
devboxHome: input.devboxHome ?? join(homedir(), ".devbox"),
|
|
@@ -1561,22 +1625,29 @@ async function buildWorkspaceUnlocked(input) {
|
|
|
1561
1625
|
const parsedGlobal = parseGlobalConfiguration(globalSource, catalogCheck.value);
|
|
1562
1626
|
if (!parsedGlobal.ok) return parsedGlobal;
|
|
1563
1627
|
const globalConfiguration = parsedGlobal.value;
|
|
1628
|
+
const agents = globalConfiguration.agent.map((name) => ({
|
|
1629
|
+
name,
|
|
1630
|
+
recipe: PACKAGED_AGENTS[name]
|
|
1631
|
+
}));
|
|
1632
|
+
const buildContextAssets = selectBuildContextAssets(agents);
|
|
1633
|
+
const nodeRuntimes = globalConfiguration.node.map((releaseLine) => PACKAGED_NODE_RECIPES[releaseLine]);
|
|
1564
1634
|
const dockerfile = renderBuildDockerfile({
|
|
1565
|
-
nodeRuntimes
|
|
1635
|
+
nodeRuntimes,
|
|
1566
1636
|
buildNodeRuntime: buildNodeRuntimeRecipe(globalConfiguration),
|
|
1567
|
-
agents
|
|
1568
|
-
name,
|
|
1569
|
-
recipe: PACKAGED_AGENTS[name]
|
|
1570
|
-
})),
|
|
1637
|
+
agents,
|
|
1571
1638
|
skillAgents: globalConfiguration.agent.filter((agent) => PACKAGED_AGENTS[agent]?.supportsSkillInstallation === true)
|
|
1572
1639
|
});
|
|
1573
|
-
await rm(paths.buildContext, {
|
|
1574
|
-
recursive: true,
|
|
1575
|
-
force: true
|
|
1576
|
-
});
|
|
1577
1640
|
await mkdir(paths.buildContext, { recursive: true });
|
|
1578
|
-
await
|
|
1579
|
-
await
|
|
1641
|
+
await seedBuildContextAssets(paths.buildContext, buildContextAssets);
|
|
1642
|
+
await Promise.all([
|
|
1643
|
+
writeFile(join(paths.buildContext, "Dockerfile"), dockerfile),
|
|
1644
|
+
writeFile(join(paths.buildContext, ".dockerignore"), renderDockerignore(buildContextAssets)),
|
|
1645
|
+
writeFile(join(paths.buildContext, "entrypoint.sh"), renderEntrypoint({
|
|
1646
|
+
nodeRuntimes,
|
|
1647
|
+
agents
|
|
1648
|
+
})),
|
|
1649
|
+
writeFile(join(paths.buildContext, "supervisord.conf"), renderSupervisorConfiguration())
|
|
1650
|
+
]);
|
|
1580
1651
|
if (input.signal?.aborted) throw new InterruptedError();
|
|
1581
1652
|
const buildResult = await (input.executeDockerBuild ?? runDockerBuild)({
|
|
1582
1653
|
context: paths.buildContext,
|
|
@@ -1635,9 +1706,85 @@ function renderBuildDockerfile(input) {
|
|
|
1635
1706
|
const skillAgentArguments = input.skillAgents.map((agent) => `-a ${agent}`).join(" ");
|
|
1636
1707
|
lines.push("# Install Agent Skills", "RUN set -eux \\", " && mkdir -p /home/devbox/.agents/skills \\", ` && export PATH="${input.buildNodeRuntime.runtimeRoot}/bin:$PATH" \\`, ` && npx -y skills add ycs77/skills -g ${skillAgentArguments} -s '*' -y`, "");
|
|
1637
1708
|
}
|
|
1638
|
-
lines.push("USER root");
|
|
1709
|
+
lines.push("USER root", "", "# Copy dotfiles", "COPY .bash_aliases /home/devbox/.bash_aliases", "COPY .gitconfig /home/devbox/.gitconfig", "RUN chown devbox:devbox /home/devbox/.bash_aliases \\", " && chown devbox:devbox /home/devbox/.gitconfig", "");
|
|
1710
|
+
const hasClaudeCode = input.agents.some((agent) => agent.name === "claude-code");
|
|
1711
|
+
const hasOmp = input.agents.some((agent) => agent.name === "omp");
|
|
1712
|
+
if (hasClaudeCode || hasOmp) {
|
|
1713
|
+
const ownershipCommands = [];
|
|
1714
|
+
lines.push("# Copy AI dotfiles");
|
|
1715
|
+
if (hasClaudeCode) {
|
|
1716
|
+
lines.push("COPY .claude/settings.json /home/devbox/.claude/settings.json");
|
|
1717
|
+
ownershipCommands.push("chown devbox:devbox /home/devbox/.claude/settings.json");
|
|
1718
|
+
}
|
|
1719
|
+
if (hasOmp) {
|
|
1720
|
+
lines.push("COPY .omp/agent/config.yml /home/devbox/.omp/agent/config.yml");
|
|
1721
|
+
ownershipCommands.push("chown devbox:devbox /home/devbox/.omp/agent", "chown devbox:devbox /home/devbox/.omp/agent/config.yml");
|
|
1722
|
+
}
|
|
1723
|
+
lines.push(`RUN ${ownershipCommands.join(" \\\n && ")}`, "");
|
|
1724
|
+
}
|
|
1725
|
+
lines.push("COPY entrypoint.sh /usr/local/bin/entrypoint.sh", "COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf", "RUN chmod +x /usr/local/bin/entrypoint.sh", "", "ENTRYPOINT [\"/usr/local/bin/entrypoint.sh\"]");
|
|
1726
|
+
return `${lines.join("\n")}\n`;
|
|
1727
|
+
}
|
|
1728
|
+
function renderDockerignore(assets) {
|
|
1729
|
+
const lines = [
|
|
1730
|
+
"# Machine-owned Workspace build context",
|
|
1731
|
+
"*",
|
|
1732
|
+
"!Dockerfile",
|
|
1733
|
+
"!entrypoint.sh",
|
|
1734
|
+
"!supervisord.conf",
|
|
1735
|
+
...assets.map((asset) => `!${asset.relativePath}`)
|
|
1736
|
+
];
|
|
1737
|
+
if (assets.some((asset) => asset.relativePath === ".claude/settings.json")) lines.push("!.claude", "!.claude/settings.json");
|
|
1738
|
+
if (assets.some((asset) => asset.relativePath === ".omp/agent/config.yml")) lines.push("!.omp", "!.omp/agent", "!.omp/agent/config.yml");
|
|
1639
1739
|
return `${lines.join("\n")}\n`;
|
|
1640
1740
|
}
|
|
1741
|
+
async function seedBuildContextAssets(buildContext, assets) {
|
|
1742
|
+
await Promise.all(assets.map(async (asset) => {
|
|
1743
|
+
const destination = join(buildContext, asset.relativePath);
|
|
1744
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
1745
|
+
try {
|
|
1746
|
+
await writeFile(destination, asset.content, { flag: "wx" });
|
|
1747
|
+
} catch (error) {
|
|
1748
|
+
if (typeof error !== "object" || error === null || !("code" in error) || error.code !== "EEXIST") throw error;
|
|
1749
|
+
}
|
|
1750
|
+
}));
|
|
1751
|
+
}
|
|
1752
|
+
function selectBuildContextAssets(agents) {
|
|
1753
|
+
const assets = [...BASE_WORKSPACE_IMAGE_DEFAULTS];
|
|
1754
|
+
if (agents.some((agent) => agent.name === "claude-code")) assets.push(CLAUDE_SETTINGS_DEFAULT);
|
|
1755
|
+
if (agents.some((agent) => agent.name === "omp")) assets.push(OMP_AGENT_CONFIG_DEFAULT);
|
|
1756
|
+
return assets;
|
|
1757
|
+
}
|
|
1758
|
+
function renderEntrypoint(input) {
|
|
1759
|
+
const lines = [
|
|
1760
|
+
"#!/bin/sh",
|
|
1761
|
+
"set -eu",
|
|
1762
|
+
""
|
|
1763
|
+
];
|
|
1764
|
+
if (input.nodeRuntimes.length > 0) lines.push("# Set the Node.js release line to use", "NODE_VERSION=\"${NODE_VERSION:-}\"", "if [ -n \"$NODE_VERSION\" ]; then", " case \"$NODE_VERSION\" in", " *[!0-9]*)", " echo \"NODE_VERSION must be a numeric release line: $NODE_VERSION\" >&2", " exit 1", " ;;", " esac", "", " NODE_RUNTIME_ROOT=\"/opt/devbox/runtimes/node/$NODE_VERSION\"", " if [ ! -x \"$NODE_RUNTIME_ROOT/bin/node\" ]; then", " echo \"Node.js release line $NODE_VERSION is not installed.\" >&2", " exit 1", " fi", "", " export NODE_RUNTIME_ROOT", " export PATH=\"$NODE_RUNTIME_ROOT/bin:$PATH\"", "fi", "");
|
|
1765
|
+
if (input.agents.some((agent) => agent.name === "agy")) lines.push("# Link shared Agent Skills", "if [ ! -L \"/home/devbox/.gemini/skills\" ]; then", " ln -sfn /home/devbox/.agents/skills /home/devbox/.gemini/skills", " chown -h devbox:devbox /home/devbox/.gemini/skills", "fi", "");
|
|
1766
|
+
lines.push("if [ $# -gt 0 ]; then", " exec gosu devbox \"$@\"", "else", " exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf", "fi");
|
|
1767
|
+
return `${lines.join("\n")}\n`;
|
|
1768
|
+
}
|
|
1769
|
+
function renderSupervisorConfiguration() {
|
|
1770
|
+
return `${[
|
|
1771
|
+
"[supervisord]",
|
|
1772
|
+
"nodaemon=true",
|
|
1773
|
+
"user=root",
|
|
1774
|
+
"logfile=/var/log/supervisor/supervisord.log",
|
|
1775
|
+
"pidfile=/var/run/supervisord.pid",
|
|
1776
|
+
"",
|
|
1777
|
+
"[program:idle]",
|
|
1778
|
+
"command=/bin/sleep infinity",
|
|
1779
|
+
"autorestart=false",
|
|
1780
|
+
"startsecs=0",
|
|
1781
|
+
"stopsignal=TERM",
|
|
1782
|
+
"stopasgroup=true",
|
|
1783
|
+
"killasgroup=true",
|
|
1784
|
+
"stdout_logfile=/dev/null",
|
|
1785
|
+
"stderr_logfile=/dev/null"
|
|
1786
|
+
].join("\n")}\n`;
|
|
1787
|
+
}
|
|
1641
1788
|
function renderNodeRuntimeStage(recipe) {
|
|
1642
1789
|
const keys = recipe.trustedReleaseKeys.map((key) => ` ${key}`).join(" \\\n");
|
|
1643
1790
|
return [
|
|
@@ -1653,8 +1800,6 @@ function renderNodeRuntimeStage(recipe) {
|
|
|
1653
1800
|
" && set -eux \\",
|
|
1654
1801
|
` && NODE_RUNTIME_VERSION=${recipe.version} \\`,
|
|
1655
1802
|
` && NODE_RUNTIME_ROOT=${recipe.runtimeRoot} \\`,
|
|
1656
|
-
` && NODE_ARCHIVE_URL="https://nodejs.org/dist/v\${NODE_RUNTIME_VERSION}/node-v\${NODE_RUNTIME_VERSION}-linux-\${ARCH}.tar.xz" \\`,
|
|
1657
|
-
` && NODE_CHECKSUMS_URL="https://nodejs.org/dist/v\${NODE_RUNTIME_VERSION}/SHASUMS256.txt.asc" \\`,
|
|
1658
1803
|
" && mkdir -p \"$NODE_RUNTIME_ROOT\" /tmp/node-source \\",
|
|
1659
1804
|
" && cd /tmp/node-source \\",
|
|
1660
1805
|
" && export GNUPGHOME=\"$(mktemp -d)\" \\",
|
|
@@ -1665,18 +1810,14 @@ function renderNodeRuntimeStage(recipe) {
|
|
|
1665
1810
|
" { gpg --batch --no-options --keyserver keyserver.ubuntu.com --recv-keys \"$key\" && gpg --batch --no-options --fingerprint \"$key\"; }; \\",
|
|
1666
1811
|
" done \\",
|
|
1667
1812
|
" && archive=\"node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\" \\",
|
|
1668
|
-
" && curl -
|
|
1669
|
-
" && curl -
|
|
1813
|
+
" && curl -fsSLO --compressed \"https://nodejs.org/dist/v${NODE_RUNTIME_VERSION}/node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\" \\",
|
|
1814
|
+
" && curl -fsSLO --compressed \"https://nodejs.org/dist/v${NODE_RUNTIME_VERSION}/SHASUMS256.txt.asc\" \\",
|
|
1670
1815
|
" && gpg --batch --no-options --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \\",
|
|
1671
1816
|
" && gpgconf --kill all \\",
|
|
1672
1817
|
" && rm -rf \"$GNUPGHOME\" \\",
|
|
1673
|
-
" &&
|
|
1674
|
-
"
|
|
1675
|
-
"
|
|
1676
|
-
" && sha256sum --check \"$archive.sha256\" \\",
|
|
1677
|
-
" && tar --extract --xz --file \"$archive\" --directory \"$NODE_RUNTIME_ROOT\" \\",
|
|
1678
|
-
" --strip-components=1 --no-same-owner \\",
|
|
1679
|
-
" && rm \"$archive\" SHASUMS256.txt.asc SHASUMS256.txt \"$archive.sha256\" \\",
|
|
1818
|
+
" && grep \" node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\\$\" SHASUMS256.txt | sha256sum -c - \\",
|
|
1819
|
+
" && tar -xJf \"node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\" -C \"$NODE_RUNTIME_ROOT\" --strip-components=1 --no-same-owner \\",
|
|
1820
|
+
" && rm \"node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\" SHASUMS256.txt.asc SHASUMS256.txt \\",
|
|
1680
1821
|
" && find \"$NODE_RUNTIME_ROOT/include/node/openssl/archs\" -mindepth 1 -maxdepth 1 ! -name \"$OPENSSL_ARCH\" -exec rm -rf {} \\; \\",
|
|
1681
1822
|
" && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \\",
|
|
1682
1823
|
" && export PATH=\"$NODE_RUNTIME_ROOT/bin:$PATH\" \\",
|
|
@@ -1724,29 +1865,45 @@ function createCli(signal, interactive) {
|
|
|
1724
1865
|
cli.command("").action(() => cli.outputHelp());
|
|
1725
1866
|
cli.command("init", "Register the current directory as a Devbox Project.").action(async () => {
|
|
1726
1867
|
if (!interactive) return interactiveFailure("devbox init requires an interactive terminal.", "Run devbox init from a TTY before it writes Project state.");
|
|
1868
|
+
console.log();
|
|
1869
|
+
showLogo();
|
|
1870
|
+
console.log();
|
|
1871
|
+
intro(c.bgCyan(" devbox "));
|
|
1727
1872
|
const result = await initializeProject({
|
|
1728
1873
|
signal,
|
|
1729
1874
|
prompt
|
|
1730
1875
|
});
|
|
1731
1876
|
if (!result.ok) return result;
|
|
1732
|
-
|
|
1877
|
+
if (result.value.confirmed === false) {
|
|
1878
|
+
outro("Project registration was not changed.");
|
|
1879
|
+
return success({});
|
|
1880
|
+
}
|
|
1881
|
+
if (!result.value.created) {
|
|
1882
|
+
outro("Project is already registered.");
|
|
1883
|
+
return success({});
|
|
1884
|
+
}
|
|
1885
|
+
outro(`Next: run ${c.cyan(c.bold("devbox build"))} to prepare your workspace, then ${c.cyan(c.bold("devbox up"))} to start it.`);
|
|
1886
|
+
return success({});
|
|
1733
1887
|
});
|
|
1734
1888
|
cli.command("config", "Edit Global or Local configuration for the current Project.").action(async () => {
|
|
1735
1889
|
if (!interactive) return interactiveFailure("devbox config requires an interactive terminal.", "Run devbox config from a TTY, or edit the supported Global or Local YAML directly.");
|
|
1890
|
+
intro(c.bgCyan(" devbox "));
|
|
1736
1891
|
if (await prompt.selectConfigurationScope() === "global") {
|
|
1737
1892
|
const result = await configureGlobal({
|
|
1738
1893
|
signal,
|
|
1739
1894
|
prompt
|
|
1740
1895
|
});
|
|
1741
1896
|
if (!result.ok) return result;
|
|
1742
|
-
|
|
1897
|
+
outro(result.value.changed ? "Global configuration updated." : "Global configuration was not changed.");
|
|
1898
|
+
return success({});
|
|
1743
1899
|
}
|
|
1744
1900
|
const result = await configureLocalProject({
|
|
1745
1901
|
signal,
|
|
1746
1902
|
prompt
|
|
1747
1903
|
});
|
|
1748
1904
|
if (!result.ok) return result;
|
|
1749
|
-
|
|
1905
|
+
outro(result.value.changed ? "Local configuration updated." : "Local configuration was not changed.");
|
|
1906
|
+
return success({});
|
|
1750
1907
|
});
|
|
1751
1908
|
cli.command("build", "Build the shared Workspace image from Global configuration.").option("--cache, --no-cache", "Use Docker layer cache.", { default: true }).action(async (options = {}) => {
|
|
1752
1909
|
const result = await buildWorkspace({
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ycs77/devbox",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "A containerized devbox for AI agents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
7
7
|
"agentic",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@clack/prompts": "^1.7.0",
|
|
37
37
|
"cac": "^7.0.0",
|
|
38
|
+
"picocolors": "^1.1.1",
|
|
38
39
|
"yaml": "^2.9.0"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|