@ycs77/devbox 0.4.0 → 0.5.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 +18 -15
- package/dist/cli.mjs +143 -39
- 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
|
@@ -209,7 +209,8 @@ function defaultGlobalConfiguration(catalog = PACKAGED_RUNTIME_CATALOG) {
|
|
|
209
209
|
function defaultLocalConfiguration(globalConfiguration) {
|
|
210
210
|
return {
|
|
211
211
|
version: 1,
|
|
212
|
-
node: globalConfiguration.node[0] ?? null
|
|
212
|
+
node: globalConfiguration.node[0] ?? null,
|
|
213
|
+
ports: ["APP_PORT:5173:5173"]
|
|
213
214
|
};
|
|
214
215
|
}
|
|
215
216
|
function parseGlobalConfiguration(source, catalog = PACKAGED_RUNTIME_CATALOG) {
|
|
@@ -249,15 +250,22 @@ function parseLocalConfiguration(source, globalConfiguration, catalog = PACKAGED
|
|
|
249
250
|
const parsed = parseYaml(source);
|
|
250
251
|
if (!parsed.ok) return invalid$1("invalid-local-configuration", parsed.observed);
|
|
251
252
|
const document = parsed.value;
|
|
252
|
-
if (!isRecord(document) || !hasExactKeys$1(document, ["version", "node"])
|
|
253
|
+
if (!isRecord(document) || !hasExactKeys$1(document, ["version", "node"]) && !hasExactKeys$1(document, [
|
|
254
|
+
"version",
|
|
255
|
+
"node",
|
|
256
|
+
"ports"
|
|
257
|
+
])) return invalid$1("invalid-local-configuration", "Local configuration contains unknown or missing fields.");
|
|
253
258
|
if (document.version !== 1) return invalid$1("invalid-local-configuration", "Local configuration must use version: 1.");
|
|
254
259
|
const node = runtimeLineOrNull(document.node);
|
|
255
260
|
if (node === void 0) return invalid$1("invalid-local-configuration", `Node Runtime is invalid: ${String(document.node)}.`);
|
|
256
261
|
if (node !== null && !catalog.runtimes.node?.includes(node)) return invalid$1("invalid-local-configuration", `Node Runtime is not in the packaged catalog: ${String(document.node)}.`);
|
|
257
262
|
if (node !== null && !globalConfiguration.node.includes(node)) return invalid$1("unconfigured-runtime-selection", `Local configuration selects a Node Runtime that is not configured globally: ${node}.`);
|
|
263
|
+
const ports = document.ports === void 0 ? success([]) : normalizePortMappings(document.ports);
|
|
264
|
+
if (!ports.ok) return ports;
|
|
258
265
|
return success({
|
|
259
266
|
version: 1,
|
|
260
|
-
node
|
|
267
|
+
node,
|
|
268
|
+
ports: ports.value
|
|
261
269
|
});
|
|
262
270
|
}
|
|
263
271
|
function serializeGlobalConfiguration(configuration) {
|
|
@@ -271,12 +279,31 @@ function serializeGlobalConfiguration(configuration) {
|
|
|
271
279
|
function serializeLocalConfiguration(configuration) {
|
|
272
280
|
return stringify({
|
|
273
281
|
version: 1,
|
|
274
|
-
node: configuration.node
|
|
282
|
+
node: configuration.node,
|
|
283
|
+
ports: [...configuration.ports]
|
|
275
284
|
});
|
|
276
285
|
}
|
|
277
286
|
function configurationsEqual(left, right) {
|
|
278
287
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
279
288
|
}
|
|
289
|
+
function renderComposePortMapping(mapping) {
|
|
290
|
+
return parsePortMapping(mapping)?.compose ?? mapping;
|
|
291
|
+
}
|
|
292
|
+
function normalizePortMappings(values) {
|
|
293
|
+
if (!Array.isArray(values) || !values.every((value) => typeof value === "string")) return invalid$1("invalid-local-configuration", "Ports must be a list of port mappings.");
|
|
294
|
+
const mappings = [];
|
|
295
|
+
const hostPorts = /* @__PURE__ */ new Set();
|
|
296
|
+
for (const input of values) {
|
|
297
|
+
const mapping = input.trim();
|
|
298
|
+
if (mapping.length === 0) continue;
|
|
299
|
+
const parsed = parsePortMapping(mapping);
|
|
300
|
+
if (parsed === void 0) return invalid$1("invalid-local-configuration", `Port mapping is invalid: ${mapping}.`);
|
|
301
|
+
if (hostPorts.has(parsed.hostPort)) return invalid$1("invalid-local-configuration", `Host port is configured more than once: ${parsed.hostPort}.`);
|
|
302
|
+
hostPorts.add(parsed.hostPort);
|
|
303
|
+
mappings.push(mapping);
|
|
304
|
+
}
|
|
305
|
+
return success(mappings);
|
|
306
|
+
}
|
|
280
307
|
function parseYaml(source) {
|
|
281
308
|
try {
|
|
282
309
|
const document = parseDocument(source, {
|
|
@@ -307,6 +334,22 @@ function runtimeLineOrNull(value) {
|
|
|
307
334
|
if (value === null) return null;
|
|
308
335
|
return runtimeLine(value);
|
|
309
336
|
}
|
|
337
|
+
function parsePortMapping(mapping) {
|
|
338
|
+
const literal = /^(\d+):(\d+)$/.exec(mapping);
|
|
339
|
+
if (literal !== null) return validPort(literal[1]) && validPort(literal[2]) ? {
|
|
340
|
+
hostPort: String(Number(literal[1])),
|
|
341
|
+
compose: mapping
|
|
342
|
+
} : void 0;
|
|
343
|
+
const environment = /^([A-Za-z_][A-Za-z0-9_]*):(\d+):(\d+)$/.exec(mapping);
|
|
344
|
+
if (environment !== null) return validPort(environment[2]) && validPort(environment[3]) ? {
|
|
345
|
+
hostPort: String(Number(environment[2])),
|
|
346
|
+
compose: `\${${environment[1]}:-${environment[2]}}:${environment[3]}`
|
|
347
|
+
} : void 0;
|
|
348
|
+
}
|
|
349
|
+
function validPort(value) {
|
|
350
|
+
const port = Number(value);
|
|
351
|
+
return Number.isInteger(port) && port >= 1 && port <= 65535;
|
|
352
|
+
}
|
|
310
353
|
function isRecord(value) {
|
|
311
354
|
if (typeof value !== "object" || value === null) return false;
|
|
312
355
|
const prototype = Object.getPrototypeOf(value);
|
|
@@ -464,7 +507,6 @@ function selectComposeFragments({ selectedNode, configuredAgents, agentNotificat
|
|
|
464
507
|
function renderProjectCompose(input) {
|
|
465
508
|
const fragments = selectComposeFragments(input);
|
|
466
509
|
const environment = {
|
|
467
|
-
TERM: "xterm-256color",
|
|
468
510
|
...fragments.node?.environment,
|
|
469
511
|
...fragments.notification?.environment
|
|
470
512
|
};
|
|
@@ -501,8 +543,10 @@ function renderProjectCompose(input) {
|
|
|
501
543
|
},
|
|
502
544
|
services: { devbox: {
|
|
503
545
|
image: WORKSPACE_IMAGE,
|
|
546
|
+
hostname: "devbox",
|
|
504
547
|
container_name: `devbox-${input.sandboxName}`,
|
|
505
548
|
working_dir: `/workspace/${input.sandboxName}`,
|
|
549
|
+
...input.ports.length === 0 ? {} : { ports: input.ports.map((port) => renderComposePortMapping(port)) },
|
|
506
550
|
...Object.keys(environment).length === 0 ? {} : { environment },
|
|
507
551
|
volumes
|
|
508
552
|
} },
|
|
@@ -527,7 +571,8 @@ function validatesProjectCompose(source, input) {
|
|
|
527
571
|
if (metadata.version !== 1 || metadata.project_root !== input.projectRoot || metadata.compose_name !== input.sandboxName || metadata.sandbox_service !== "devbox") return false;
|
|
528
572
|
const service = definition.services.devbox;
|
|
529
573
|
if (service.image !== "devbox-workspace:latest" || service.container_name !== `devbox-${input.sandboxName}` || service.working_dir !== `/workspace/${input.sandboxName}` || "build" in service) return false;
|
|
530
|
-
|
|
574
|
+
const ports = input.ports.map((port) => renderComposePortMapping(port));
|
|
575
|
+
return JSON.stringify(service.ports ?? []) === JSON.stringify(ports) && (input.selectedNode === null && service.environment?.NODE_VERSION === void 0 || input.selectedNode !== null && service.environment?.NODE_VERSION === input.selectedNode);
|
|
531
576
|
} catch {
|
|
532
577
|
return false;
|
|
533
578
|
}
|
|
@@ -920,7 +965,7 @@ async function configureGlobalUnlocked(input = {}, locks) {
|
|
|
920
965
|
for (const [root, localConfiguration] of localConfigurations) {
|
|
921
966
|
if (localConfiguration.node === null || !removedNode.has(localConfiguration.node)) continue;
|
|
922
967
|
let replacement = input.nextLocalConfigurations?.[root];
|
|
923
|
-
if (replacement === void 0 && input.prompt?.editLocal) replacement = await input.prompt.editLocal(localConfiguration, catalog, nextCheck.value);
|
|
968
|
+
if (replacement === void 0 && input.prompt?.editLocal) replacement = await input.prompt.editLocal(localConfiguration, catalog, nextCheck.value, false);
|
|
924
969
|
if (replacement === void 0) return failure({
|
|
925
970
|
kind: "validation",
|
|
926
971
|
code: "node-replacement-required",
|
|
@@ -1145,9 +1190,12 @@ async function runSandboxLifecycle(command, input = {}) {
|
|
|
1145
1190
|
global: false,
|
|
1146
1191
|
projectRoots: [projectRoot],
|
|
1147
1192
|
signal: input.signal
|
|
1148
|
-
}, () =>
|
|
1193
|
+
}, () => runSandboxCommandUnlocked(command, input, projectRoot));
|
|
1194
|
+
}
|
|
1195
|
+
async function runSandboxShell(input = {}) {
|
|
1196
|
+
return runSandboxCommandUnlocked("sh", input, input.root ?? process.cwd());
|
|
1149
1197
|
}
|
|
1150
|
-
async function
|
|
1198
|
+
async function runSandboxCommandUnlocked(command, input, projectRoot) {
|
|
1151
1199
|
if (input.signal?.aborted) throw new InterruptedError();
|
|
1152
1200
|
const rootCheck = await validateProjectRoot(projectRoot);
|
|
1153
1201
|
if (!rootCheck.ok) return rootCheck;
|
|
@@ -1329,7 +1377,8 @@ async function composeStateWrite(input) {
|
|
|
1329
1377
|
selectedNode: input.localConfiguration.node,
|
|
1330
1378
|
configuredAgents: input.globalConfiguration.agent,
|
|
1331
1379
|
agentNotifications: input.globalConfiguration.agent_notifications,
|
|
1332
|
-
claudeHostConfiguration: claudeHostConfiguration.value
|
|
1380
|
+
claudeHostConfiguration: claudeHostConfiguration.value,
|
|
1381
|
+
ports: input.localConfiguration.ports
|
|
1333
1382
|
});
|
|
1334
1383
|
if (!rendered.ok) return rendered;
|
|
1335
1384
|
const path = join(input.stateDirectory, "compose.yaml");
|
|
@@ -1485,18 +1534,20 @@ function createConfigurationPrompter({ signal, input, output }) {
|
|
|
1485
1534
|
}));
|
|
1486
1535
|
},
|
|
1487
1536
|
selectConfigurationScope: async () => promptValue(p.select({
|
|
1488
|
-
message: "
|
|
1537
|
+
message: "Where should this configuration apply?",
|
|
1489
1538
|
options: [{
|
|
1490
1539
|
value: "local",
|
|
1491
|
-
label: "Current
|
|
1540
|
+
label: "Current project",
|
|
1541
|
+
hint: "Enabled when this project’s container starts."
|
|
1492
1542
|
}, {
|
|
1493
1543
|
value: "global",
|
|
1494
|
-
label: "
|
|
1544
|
+
label: "Workspace Docker image",
|
|
1545
|
+
hint: "Built into the shared image. It affects image size and disk usage."
|
|
1495
1546
|
}],
|
|
1496
1547
|
...common
|
|
1497
1548
|
})),
|
|
1498
1549
|
editGlobal: async (configuration, catalog) => {
|
|
1499
|
-
p.log.step(`${c.bold(c.cyan("
|
|
1550
|
+
p.log.step(`${c.bold(c.cyan("Workspace Docker image"))}\n${c.dim("Add runtimes and agents to the shared image. They are available to every project and increase the image size.")}`, common);
|
|
1500
1551
|
return {
|
|
1501
1552
|
version: 1,
|
|
1502
1553
|
node: await promptValue(p.multiselect({
|
|
@@ -1520,22 +1571,38 @@ function createConfigurationPrompter({ signal, input, output }) {
|
|
|
1520
1571
|
}))
|
|
1521
1572
|
};
|
|
1522
1573
|
},
|
|
1523
|
-
editLocal: async (configuration, catalog, globalConfiguration) => {
|
|
1524
|
-
p.log.step(`${c.bold(c.cyan("Project configuration"))}\n${c.dim("Choose the
|
|
1574
|
+
editLocal: async (configuration, catalog, globalConfiguration, editPorts = true) => {
|
|
1575
|
+
p.log.step(`${c.bold(c.cyan("Project configuration"))}\n${c.dim(editPorts ? "Choose the Node version and ports to use when this project’s container starts." : "Choose the Node version to use when this project’s container starts.")}`, common);
|
|
1576
|
+
const node = await promptValue(p.select({
|
|
1577
|
+
message: "Selected Node release line",
|
|
1578
|
+
options: [{
|
|
1579
|
+
value: null,
|
|
1580
|
+
label: "None"
|
|
1581
|
+
}, ...(globalConfiguration.node ?? []).map((value) => ({
|
|
1582
|
+
value,
|
|
1583
|
+
label: value
|
|
1584
|
+
}))],
|
|
1585
|
+
initialValue: configuration.node,
|
|
1586
|
+
...common
|
|
1587
|
+
}));
|
|
1588
|
+
if (!editPorts) return {
|
|
1589
|
+
...configuration,
|
|
1590
|
+
node
|
|
1591
|
+
};
|
|
1592
|
+
const ports = normalizePortMappings((await promptValue(p.multiline({
|
|
1593
|
+
message: "Published ports",
|
|
1594
|
+
initialValue: configuration.ports.join("\n"),
|
|
1595
|
+
validate: (value) => {
|
|
1596
|
+
const ports = normalizePortMappings((value ?? "").split("\n"));
|
|
1597
|
+
return ports.ok ? void 0 : ports.error.observed;
|
|
1598
|
+
},
|
|
1599
|
+
...common
|
|
1600
|
+
}))).split("\n"));
|
|
1601
|
+
if (!ports.ok) throw new Error(ports.error.observed);
|
|
1525
1602
|
return {
|
|
1526
1603
|
version: 1,
|
|
1527
|
-
node
|
|
1528
|
-
|
|
1529
|
-
options: [{
|
|
1530
|
-
value: null,
|
|
1531
|
-
label: "None"
|
|
1532
|
-
}, ...(globalConfiguration.node ?? []).map((value) => ({
|
|
1533
|
-
value,
|
|
1534
|
-
label: value
|
|
1535
|
-
}))],
|
|
1536
|
-
initialValue: configuration.node,
|
|
1537
|
-
...common
|
|
1538
|
-
}))
|
|
1604
|
+
node,
|
|
1605
|
+
ports: ports.value
|
|
1539
1606
|
};
|
|
1540
1607
|
}
|
|
1541
1608
|
};
|
|
@@ -1630,6 +1697,8 @@ async function buildWorkspaceUnlocked(input) {
|
|
|
1630
1697
|
const parsedGlobal = parseGlobalConfiguration(globalSource, catalogCheck.value);
|
|
1631
1698
|
if (!parsedGlobal.ok) return parsedGlobal;
|
|
1632
1699
|
const globalConfiguration = parsedGlobal.value;
|
|
1700
|
+
const hostIdentity = currentHostIdentity();
|
|
1701
|
+
if (!hostIdentity.ok) return hostIdentity;
|
|
1633
1702
|
const agents = globalConfiguration.agent.map((name) => ({
|
|
1634
1703
|
name,
|
|
1635
1704
|
recipe: PACKAGED_AGENTS[name]
|
|
@@ -1658,7 +1727,8 @@ async function buildWorkspaceUnlocked(input) {
|
|
|
1658
1727
|
const buildResult = await (input.executeDockerBuild ?? runDockerBuild)({
|
|
1659
1728
|
context: paths.buildContext,
|
|
1660
1729
|
image: WORKSPACE_IMAGE,
|
|
1661
|
-
noCache: input.noCache === true
|
|
1730
|
+
noCache: input.noCache === true,
|
|
1731
|
+
buildArgs: hostIdentity.value
|
|
1662
1732
|
});
|
|
1663
1733
|
if (!buildResult.ok) return buildResult;
|
|
1664
1734
|
return success({ image: WORKSPACE_IMAGE });
|
|
@@ -1671,17 +1741,41 @@ function buildNodeRuntimeRecipe(globalConfiguration) {
|
|
|
1671
1741
|
}
|
|
1672
1742
|
return buildNodeRuntime;
|
|
1673
1743
|
}
|
|
1744
|
+
function currentHostIdentity() {
|
|
1745
|
+
const userId = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
1746
|
+
const groupId = typeof process.getgid === "function" ? process.getgid() : void 0;
|
|
1747
|
+
if (typeof userId !== "number" || typeof groupId !== "number" || !Number.isSafeInteger(userId) || !Number.isSafeInteger(groupId) || userId <= 0 || groupId <= 0) return failure({
|
|
1748
|
+
kind: "validation",
|
|
1749
|
+
code: "invalid-host-identity",
|
|
1750
|
+
observed: "The invoking host process must have a non-root POSIX numeric UID and GID.",
|
|
1751
|
+
nextAction: "Run devbox build as the intended non-root host user."
|
|
1752
|
+
});
|
|
1753
|
+
return success({
|
|
1754
|
+
USER_ID: String(userId),
|
|
1755
|
+
GROUP_ID: String(groupId)
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1674
1758
|
function renderBuildDockerfile(input) {
|
|
1675
1759
|
const lines = [
|
|
1676
1760
|
"# Generated by devbox build. Machine-owned Workspace build context.",
|
|
1677
1761
|
"FROM ubuntu:24.04",
|
|
1678
1762
|
"",
|
|
1763
|
+
"ARG USER_ID",
|
|
1764
|
+
"ARG GROUP_ID",
|
|
1765
|
+
"",
|
|
1679
1766
|
"ENV DEBIAN_FRONTEND=noninteractive",
|
|
1680
1767
|
"ENV TZ=UTC",
|
|
1681
1768
|
"ENV LANG=C.UTF-8",
|
|
1769
|
+
"ENV TERM=xterm-256color",
|
|
1770
|
+
"ENV EDITOR=vim",
|
|
1771
|
+
"ENV VISUAL=vim",
|
|
1682
1772
|
"",
|
|
1683
1773
|
"RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone",
|
|
1684
1774
|
"",
|
|
1775
|
+
"RUN echo \"Acquire::http::Pipeline-Depth 0;\" > /etc/apt/apt.conf.d/99custom && \\",
|
|
1776
|
+
" echo \"Acquire::http::No-Cache true;\" >> /etc/apt/apt.conf.d/99custom && \\",
|
|
1777
|
+
" echo \"Acquire::BrokenProxy true;\" >> /etc/apt/apt.conf.d/99custom",
|
|
1778
|
+
"",
|
|
1685
1779
|
"# Install the Base profile packages and build tools",
|
|
1686
1780
|
"RUN set -eux \\",
|
|
1687
1781
|
" && apt-get update && apt-get upgrade -y \\",
|
|
@@ -1697,8 +1791,8 @@ function renderBuildDockerfile(input) {
|
|
|
1697
1791
|
"",
|
|
1698
1792
|
"# Create the devbox user and group",
|
|
1699
1793
|
"RUN userdel -r ubuntu \\",
|
|
1700
|
-
" && groupadd -
|
|
1701
|
-
" && useradd -m -s /bin/bash -N -g
|
|
1794
|
+
" && groupadd -g \"$GROUP_ID\" devbox \\",
|
|
1795
|
+
" && useradd -m -s /bin/bash -N -g \"$GROUP_ID\" -u \"$USER_ID\" devbox \\",
|
|
1702
1796
|
" && echo \"devbox ALL=(ALL) NOPASSWD:ALL\" > /etc/sudoers.d/devbox \\",
|
|
1703
1797
|
" && chmod 0440 /etc/sudoers.d/devbox",
|
|
1704
1798
|
""
|
|
@@ -1767,11 +1861,7 @@ function selectBuildContextAssets(agents) {
|
|
|
1767
1861
|
return assets;
|
|
1768
1862
|
}
|
|
1769
1863
|
function renderEntrypoint(input) {
|
|
1770
|
-
const lines = [
|
|
1771
|
-
"#!/bin/sh",
|
|
1772
|
-
"set -eu",
|
|
1773
|
-
""
|
|
1774
|
-
];
|
|
1864
|
+
const lines = ["#!/usr/bin/env bash", ""];
|
|
1775
1865
|
if (input.nodeRuntimes.length > 0) lines.push("# Set the Node.js release line to use", "NODE_VERSION=\"${NODE_VERSION:-}\"", "if [ -n \"$NODE_VERSION\" ]; then", " case \"$NODE_VERSION\" in", " *[!0-9]*)", " echo \"NODE_VERSION must be a numeric release line: $NODE_VERSION\" >&2", " exit 1", " ;;", " esac", "", " NODE_RUNTIME_ROOT=\"/opt/devbox/runtimes/node/$NODE_VERSION\"", " if [ ! -x \"$NODE_RUNTIME_ROOT/bin/node\" ]; then", " echo \"Node.js release line $NODE_VERSION is not installed.\" >&2", " exit 1", " fi", "", " if ! grep -q \"# Devbox\" /etc/bash.bashrc; then", " printf \"\\n# Devbox\\nexport PATH=\\\"$NODE_RUNTIME_ROOT/bin:\\$PATH\\\"\\n\" >> /etc/bash.bashrc", " fi", "", " export PATH=\"$NODE_RUNTIME_ROOT/bin:$PATH\"", "fi", "");
|
|
1776
1866
|
if (input.agents.some((agent) => agent.name === "agy")) lines.push("# Link shared Agent Skills", "if [ ! -L \"/home/devbox/.gemini/skills\" ]; then", " ln -sfn /home/devbox/.agents/skills /home/devbox/.gemini/skills", " chown -h devbox:devbox /home/devbox/.gemini/skills", "fi", "");
|
|
1777
1867
|
lines.push("if [ $# -gt 0 ]; then", " exec gosu devbox \"$@\"", "else", " exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf", "fi");
|
|
@@ -1834,7 +1924,7 @@ function renderNodeRuntimeStage(recipe) {
|
|
|
1834
1924
|
" && export PATH=\"$NODE_RUNTIME_ROOT/bin:$PATH\" \\",
|
|
1835
1925
|
" && npm install -g npm \\",
|
|
1836
1926
|
" && npm uninstall -g corepack \\",
|
|
1837
|
-
" && npm install -g yarn pnpm @antfu/ni \\",
|
|
1927
|
+
" && npm install -g yarn pnpm @antfu/ni --allow-scripts=pnpm,yarn \\",
|
|
1838
1928
|
" && \"$NODE_RUNTIME_ROOT/bin/node\" --version \\",
|
|
1839
1929
|
" && \"$NODE_RUNTIME_ROOT/bin/npm\" --version \\",
|
|
1840
1930
|
" && rm -rf /tmp/node-source",
|
|
@@ -1845,6 +1935,10 @@ async function runDockerBuild(invocation, signal) {
|
|
|
1845
1935
|
const build = spawn("docker", [
|
|
1846
1936
|
"build",
|
|
1847
1937
|
...invocation.noCache ? ["--no-cache"] : [],
|
|
1938
|
+
"--build-arg",
|
|
1939
|
+
`USER_ID=${invocation.buildArgs.USER_ID}`,
|
|
1940
|
+
"--build-arg",
|
|
1941
|
+
`GROUP_ID=${invocation.buildArgs.GROUP_ID}`,
|
|
1848
1942
|
"--tag",
|
|
1849
1943
|
invocation.image,
|
|
1850
1944
|
"--file",
|
|
@@ -1953,12 +2047,15 @@ function createCli(signal, interactive) {
|
|
|
1953
2047
|
for (const [command, description] of [
|
|
1954
2048
|
["up", "Start the current Project Sandbox."],
|
|
1955
2049
|
["down", "Stop and remove the current Project Sandbox."],
|
|
1956
|
-
["stop", "Stop the current Project Sandbox."]
|
|
1957
|
-
["sh", "Open a shell in the current Project Sandbox."]
|
|
2050
|
+
["stop", "Stop the current Project Sandbox."]
|
|
1958
2051
|
]) cli.command(command, description).action(async () => {
|
|
1959
2052
|
const result = await runSandboxLifecycle(command, { signal });
|
|
1960
2053
|
return result.ok ? success({}) : result;
|
|
1961
2054
|
});
|
|
2055
|
+
cli.command("sh", "Open a shell in the current Project Sandbox.").action(async () => {
|
|
2056
|
+
const result = await runSandboxShell({ signal });
|
|
2057
|
+
return result.ok ? success({}) : result;
|
|
2058
|
+
});
|
|
1962
2059
|
cli.help();
|
|
1963
2060
|
return cli;
|
|
1964
2061
|
}
|
|
@@ -1985,11 +2082,18 @@ async function main() {
|
|
|
1985
2082
|
return 130;
|
|
1986
2083
|
}
|
|
1987
2084
|
if (error instanceof HostCommandError) return error.exitCode;
|
|
2085
|
+
if (isCacError(error)) {
|
|
2086
|
+
process.stderr.write(`${error.message}\n`);
|
|
2087
|
+
return 2;
|
|
2088
|
+
}
|
|
1988
2089
|
throw error;
|
|
1989
2090
|
} finally {
|
|
1990
2091
|
process.off("SIGINT", interrupt);
|
|
1991
2092
|
}
|
|
1992
2093
|
}
|
|
2094
|
+
function isCacError(error) {
|
|
2095
|
+
return error instanceof Error && error.name === "CACError";
|
|
2096
|
+
}
|
|
1993
2097
|
function interactiveFailure(observed, nextAction) {
|
|
1994
2098
|
return failure({
|
|
1995
2099
|
kind: "usage",
|