@ycs77/devbox 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -4
- package/dist/cli.mjs +292 -299
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,11 +47,16 @@ From your project directory, initialize Devbox:
|
|
|
47
47
|
devbox init
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
**Global configuration** selects the Node.js versions and AI agents installed in the shared Workspace image. These choices apply to every Project; adding more makes the image larger and uses more disk space.
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
**Local configuration** selects the Node.js version for the current Project. A Project can use only a version included in the Global configuration.
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
Published ports accept one mapping per line. The following are the supported port mapping formats:
|
|
55
|
+
|
|
56
|
+
- `5173:5173` maps host port `5173` to container port `5173`.
|
|
57
|
+
- `APP_PORT:5173:5173` maps the host port from the `APP_PORT` environment variable (default port: `5173`) to container port `5173`.
|
|
58
|
+
|
|
59
|
+
Build the shared Workspace image once after setup or whenever you change global configuration:
|
|
55
60
|
|
|
56
61
|
```bash
|
|
57
62
|
devbox build
|
|
@@ -81,8 +86,8 @@ Run these commands from the initialized project directory:
|
|
|
81
86
|
| `devbox sh` | Open a Bash shell in the running sandbox. |
|
|
82
87
|
| `devbox stop` | Stop the sandbox and preserve it for a later restart. |
|
|
83
88
|
| `devbox down` | Stop and remove the sandbox container. |
|
|
89
|
+
| `devbox rm` | Stop and remove the current Project's Sandbox, then remove its Devbox state. |
|
|
84
90
|
| `devbox config` | Change global settings or this project's configuration. |
|
|
85
|
-
| `devbox cleanup --missing-projects` | Remove registrations for projects whose root directory no longer exists. |
|
|
86
91
|
|
|
87
92
|
## Troubleshooting
|
|
88
93
|
|
package/dist/cli.mjs
CHANGED
|
@@ -10,6 +10,9 @@ import { execFile, spawn } from "node:child_process";
|
|
|
10
10
|
import { promisify } from "node:util";
|
|
11
11
|
import { parse as parse$1, parseDocument, stringify } from "yaml";
|
|
12
12
|
import { once } from "node:events";
|
|
13
|
+
//#region package.json
|
|
14
|
+
var version = "0.7.0";
|
|
15
|
+
//#endregion
|
|
13
16
|
//#region src/catalog/index.ts
|
|
14
17
|
const NODE_RELEASE_KEYS = [
|
|
15
18
|
"5BE8A3F6C8A5C01D106C0AD820B1A390B168D356",
|
|
@@ -27,12 +30,14 @@ const PACKAGED_NODE_RECIPES = {
|
|
|
27
30
|
releaseLine: "24",
|
|
28
31
|
version: "24.19.0",
|
|
29
32
|
runtimeRoot: "/opt/devbox/runtimes/node/24",
|
|
33
|
+
distributionBaseUrl: "https://nodejs.org/dist",
|
|
30
34
|
trustedReleaseKeys: NODE_RELEASE_KEYS
|
|
31
35
|
},
|
|
32
36
|
"22": {
|
|
33
37
|
releaseLine: "22",
|
|
34
38
|
version: "22.23.2",
|
|
35
39
|
runtimeRoot: "/opt/devbox/runtimes/node/22",
|
|
40
|
+
distributionBaseUrl: "https://nodejs.org/dist",
|
|
36
41
|
trustedReleaseKeys: NODE_RELEASE_KEYS
|
|
37
42
|
}
|
|
38
43
|
};
|
|
@@ -50,7 +55,7 @@ const PACKAGED_AGENTS = {
|
|
|
50
55
|
},
|
|
51
56
|
supportsNotifications: true,
|
|
52
57
|
notificationInstallationCommands: ["claude plugin marketplace add ycs77/claude-code-notifications", "claude plugin install notification-basic-wsl@ycs77-notifications"],
|
|
53
|
-
|
|
58
|
+
skillInstallationTarget: "claude-code"
|
|
54
59
|
},
|
|
55
60
|
codex: {
|
|
56
61
|
home: {
|
|
@@ -62,7 +67,7 @@ const PACKAGED_AGENTS = {
|
|
|
62
67
|
shell: "sh"
|
|
63
68
|
},
|
|
64
69
|
supportsNotifications: true,
|
|
65
|
-
|
|
70
|
+
skillInstallationTarget: "codex",
|
|
66
71
|
notificationInstallationCommands: ["codex plugin marketplace add ycs77/codex-notifications", "codex plugin add notification-basic-wsl@ycs77-notifications"]
|
|
67
72
|
},
|
|
68
73
|
agy: {
|
|
@@ -74,8 +79,7 @@ const PACKAGED_AGENTS = {
|
|
|
74
79
|
url: "https://antigravity.google/cli/install.sh",
|
|
75
80
|
shell: "bash"
|
|
76
81
|
},
|
|
77
|
-
supportsNotifications: false
|
|
78
|
-
supportsSkillInstallation: false
|
|
82
|
+
supportsNotifications: false
|
|
79
83
|
},
|
|
80
84
|
omp: {
|
|
81
85
|
home: {
|
|
@@ -87,7 +91,7 @@ const PACKAGED_AGENTS = {
|
|
|
87
91
|
shell: "sh"
|
|
88
92
|
},
|
|
89
93
|
supportsNotifications: true,
|
|
90
|
-
|
|
94
|
+
skillInstallationTarget: "codex",
|
|
91
95
|
notificationInstallationCommands: ["omp plugin marketplace add ycs77/omp-notifications", "omp plugin install notification-basic@ycs77-notifications"]
|
|
92
96
|
}
|
|
93
97
|
};
|
|
@@ -467,10 +471,7 @@ function isMissingFileError$1(error) {
|
|
|
467
471
|
return typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
468
472
|
}
|
|
469
473
|
//#endregion
|
|
470
|
-
//#region src/
|
|
471
|
-
const WORKSPACE_IMAGE = "devbox-workspace:latest";
|
|
472
|
-
//#endregion
|
|
473
|
-
//#region src/project/compose-fragments.ts
|
|
474
|
+
//#region src/agent/compose.ts
|
|
474
475
|
const NOTIFICATION_FRAGMENT = {
|
|
475
476
|
environment: { PULSE_SERVER: "unix:/tmp/pulse-socket" },
|
|
476
477
|
volume: {
|
|
@@ -479,7 +480,7 @@ const NOTIFICATION_FRAGMENT = {
|
|
|
479
480
|
readOnly: true
|
|
480
481
|
}
|
|
481
482
|
};
|
|
482
|
-
function
|
|
483
|
+
function selectAgentComposeFragments(configuredAgents, notificationsEnabled) {
|
|
483
484
|
const agents = [];
|
|
484
485
|
let hasNotificationAgent = false;
|
|
485
486
|
for (const agent of configuredAgents) {
|
|
@@ -497,18 +498,27 @@ function selectComposeFragments({ selectedNode, configuredAgents, agentNotificat
|
|
|
497
498
|
}
|
|
498
499
|
}
|
|
499
500
|
return {
|
|
500
|
-
node: selectedNode === null ? void 0 : { environment: { NODE_VERSION: selectedNode } },
|
|
501
501
|
agents,
|
|
502
|
-
notification:
|
|
502
|
+
notification: notificationsEnabled && hasNotificationAgent ? NOTIFICATION_FRAGMENT : void 0
|
|
503
503
|
};
|
|
504
504
|
}
|
|
505
505
|
//#endregion
|
|
506
|
+
//#region src/runtimes/node/compose.ts
|
|
507
|
+
function selectNodeComposeFragment(selectedNode) {
|
|
508
|
+
return selectedNode === null ? void 0 : { environment: { NODE_VERSION: selectedNode } };
|
|
509
|
+
}
|
|
510
|
+
//#endregion
|
|
511
|
+
//#region src/workspace/image.ts
|
|
512
|
+
const WORKSPACE_IMAGE = "devbox-workspace:latest";
|
|
513
|
+
//#endregion
|
|
506
514
|
//#region src/project/compose.ts
|
|
507
515
|
function renderProjectCompose(input) {
|
|
508
|
-
const
|
|
516
|
+
const containerName = `devbox-${input.sandboxName}`;
|
|
517
|
+
const node = selectNodeComposeFragment(input.selectedNode);
|
|
518
|
+
const agent = selectAgentComposeFragments(input.configuredAgents, input.agentNotifications);
|
|
509
519
|
const environment = {
|
|
510
|
-
...
|
|
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,51 +536,51 @@ 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 = {
|
|
537
|
-
name:
|
|
547
|
+
name: containerName,
|
|
538
548
|
"x-devbox": {
|
|
539
549
|
version: 1,
|
|
540
550
|
project_root: input.projectRoot,
|
|
541
|
-
|
|
551
|
+
container_name: containerName,
|
|
542
552
|
sandbox_service: "devbox"
|
|
543
553
|
},
|
|
544
554
|
services: { devbox: {
|
|
545
555
|
image: WORKSPACE_IMAGE,
|
|
546
556
|
hostname: "devbox",
|
|
547
|
-
container_name:
|
|
557
|
+
container_name: containerName,
|
|
548
558
|
working_dir: `/workspace/${input.sandboxName}`,
|
|
549
559
|
...input.ports.length === 0 ? {} : { ports: input.ports.map((port) => renderComposePortMapping(port)) },
|
|
550
560
|
...Object.keys(environment).length === 0 ? {} : { environment },
|
|
551
561
|
volumes
|
|
552
562
|
} },
|
|
553
|
-
...
|
|
563
|
+
...agent.agents.length === 0 ? {} : { volumes: Object.fromEntries(agent.agents.map((fragment) => [fragment.volume.name, {
|
|
554
564
|
name: fragment.volume.name,
|
|
555
565
|
external: fragment.volume.external
|
|
556
566
|
}])) }
|
|
557
567
|
};
|
|
558
568
|
const source = stringify(definition);
|
|
559
|
-
return validatesProjectCompose(source, input) ? success(source) : failure({
|
|
569
|
+
return validatesProjectCompose(source, input, containerName) ? success(source) : failure({
|
|
560
570
|
kind: "operational",
|
|
561
571
|
code: "invalid-project-compose",
|
|
562
572
|
observed: `Devbox generated an invalid Compose definition for Project ${input.projectRoot}.`,
|
|
563
573
|
nextAction: "Run the command again. If the problem continues, report it as a Devbox bug."
|
|
564
574
|
});
|
|
565
575
|
}
|
|
566
|
-
function validatesProjectCompose(source, input) {
|
|
576
|
+
function validatesProjectCompose(source, input, containerName) {
|
|
567
577
|
try {
|
|
568
578
|
const definition = parse$1(source);
|
|
569
|
-
if (definition.name !==
|
|
579
|
+
if (definition.name !== containerName) return false;
|
|
570
580
|
const metadata = definition["x-devbox"];
|
|
571
|
-
if (metadata.version !== 1 || metadata.project_root !== input.projectRoot || metadata.
|
|
581
|
+
if (metadata.version !== 1 || metadata.project_root !== input.projectRoot || metadata.container_name !== containerName || metadata.sandbox_service !== "devbox") return false;
|
|
572
582
|
const service = definition.services.devbox;
|
|
573
|
-
if (service.image !== "devbox-workspace:latest" || service.container_name !==
|
|
583
|
+
if (service.image !== "devbox-workspace:latest" || service.container_name !== containerName || service.working_dir !== `/workspace/${input.sandboxName}` || "build" in service) return false;
|
|
574
584
|
const ports = input.ports.map((port) => renderComposePortMapping(port));
|
|
575
585
|
return JSON.stringify(service.ports ?? []) === JSON.stringify(ports) && (input.selectedNode === null && service.environment?.NODE_VERSION === void 0 || input.selectedNode !== null && service.environment?.NODE_VERSION === input.selectedNode);
|
|
576
586
|
} catch {
|
|
@@ -1050,13 +1060,20 @@ async function removeProjectUnlocked(input = {}) {
|
|
|
1050
1060
|
const registration = registryCheck.value.projects[projectRoot];
|
|
1051
1061
|
if (registration === void 0) return notRegistered(projectRoot);
|
|
1052
1062
|
if (!input.yes) {
|
|
1053
|
-
if (!await (input.confirm ?? (async () => false))(`Remove Project ${projectRoot} and
|
|
1063
|
+
if (!await (input.confirm ?? (async () => false))(`Remove Project ${projectRoot}, its Sandbox, and Devbox state?`)) return success({
|
|
1054
1064
|
root: projectRoot,
|
|
1055
1065
|
removed: false
|
|
1056
1066
|
});
|
|
1057
1067
|
}
|
|
1058
1068
|
if (input.signal?.aborted) throw new InterruptedError();
|
|
1059
1069
|
const stateDirectory = projectStateDirectory(registration.identity, paths.home);
|
|
1070
|
+
const environment = input.environment ?? currentHostEnvironment();
|
|
1071
|
+
await (environment.runDirect ?? environment.run)("docker", [
|
|
1072
|
+
"compose",
|
|
1073
|
+
"--file",
|
|
1074
|
+
join(stateDirectory, "compose.yaml"),
|
|
1075
|
+
"down"
|
|
1076
|
+
]);
|
|
1060
1077
|
try {
|
|
1061
1078
|
await rm(stateDirectory, {
|
|
1062
1079
|
recursive: true,
|
|
@@ -1082,107 +1099,6 @@ async function removeProjectUnlocked(input = {}) {
|
|
|
1082
1099
|
removed: true
|
|
1083
1100
|
});
|
|
1084
1101
|
}
|
|
1085
|
-
async function cleanupMissingProjects(input = {}) {
|
|
1086
|
-
return withStateLocks({
|
|
1087
|
-
devboxHome: input.devboxHome ?? join(homedir(), ".devbox"),
|
|
1088
|
-
global: true,
|
|
1089
|
-
projectRoots: [],
|
|
1090
|
-
signal: input.signal
|
|
1091
|
-
}, async (locks) => {
|
|
1092
|
-
const discovery = await discoverMissingProjects(input);
|
|
1093
|
-
if (!discovery.ok) return discovery;
|
|
1094
|
-
await locks.acquireProjectScopes(discovery.value.missingRoots);
|
|
1095
|
-
return cleanupMissingProjectsUnlocked(input, discovery.value);
|
|
1096
|
-
});
|
|
1097
|
-
}
|
|
1098
|
-
async function discoverMissingProjects(input) {
|
|
1099
|
-
if (input.signal?.aborted) throw new InterruptedError();
|
|
1100
|
-
const paths = devboxPaths(input.devboxHome);
|
|
1101
|
-
const registryState = await readOptionalFile(paths.projectRegistry);
|
|
1102
|
-
if (!registryState.ok) return registryState;
|
|
1103
|
-
if (!registryState.value.exists) return success({
|
|
1104
|
-
paths,
|
|
1105
|
-
registry: {
|
|
1106
|
-
version: 1,
|
|
1107
|
-
projects: {}
|
|
1108
|
-
},
|
|
1109
|
-
missingRoots: []
|
|
1110
|
-
});
|
|
1111
|
-
const registryCheck = parseProjectRegistry(registryState.value.content);
|
|
1112
|
-
if (!registryCheck.ok) return registryCheck;
|
|
1113
|
-
const missingRoots = [];
|
|
1114
|
-
for (const root of Object.keys(registryCheck.value.projects)) {
|
|
1115
|
-
if (input.signal?.aborted) throw new InterruptedError();
|
|
1116
|
-
const exists = await projectRootExists(root);
|
|
1117
|
-
if (exists === void 0) return failure({
|
|
1118
|
-
kind: "operational",
|
|
1119
|
-
code: "project-root-observation-failed",
|
|
1120
|
-
observed: `Devbox could not inspect Project root: ${root}.`,
|
|
1121
|
-
nextAction: "Check access to the registered Project root and try cleanup again."
|
|
1122
|
-
});
|
|
1123
|
-
if (!exists) missingRoots.push(root);
|
|
1124
|
-
}
|
|
1125
|
-
return success({
|
|
1126
|
-
paths,
|
|
1127
|
-
registry: registryCheck.value,
|
|
1128
|
-
missingRoots
|
|
1129
|
-
});
|
|
1130
|
-
}
|
|
1131
|
-
async function cleanupMissingProjectsUnlocked(input, discovery) {
|
|
1132
|
-
if (input.signal?.aborted) throw new InterruptedError();
|
|
1133
|
-
if (discovery.missingRoots.length === 0) return success({
|
|
1134
|
-
roots: [],
|
|
1135
|
-
removed: false
|
|
1136
|
-
});
|
|
1137
|
-
if (!input.yes) {
|
|
1138
|
-
if (!await (input.confirm ?? (async () => false))(`Remove Missing-root Project registrations?\n${discovery.missingRoots.map((root) => `- ${root}`).join("\n")}`)) return success({
|
|
1139
|
-
roots: [],
|
|
1140
|
-
removed: false
|
|
1141
|
-
});
|
|
1142
|
-
}
|
|
1143
|
-
const removedRoots = [];
|
|
1144
|
-
const projects = { ...discovery.registry.projects };
|
|
1145
|
-
for (const root of discovery.missingRoots) {
|
|
1146
|
-
const rootExists = await projectRootExists(root);
|
|
1147
|
-
if (rootExists === void 0) return failure({
|
|
1148
|
-
kind: "operational",
|
|
1149
|
-
code: "project-root-observation-failed",
|
|
1150
|
-
observed: `Devbox could not recheck Project root: ${root}.`,
|
|
1151
|
-
nextAction: "Check access to the registered Project root and try cleanup again."
|
|
1152
|
-
});
|
|
1153
|
-
if (rootExists) continue;
|
|
1154
|
-
const registration = projects[root];
|
|
1155
|
-
if (registration === void 0) continue;
|
|
1156
|
-
try {
|
|
1157
|
-
await rm(projectStateDirectory(registration.identity, discovery.paths.home), {
|
|
1158
|
-
recursive: true,
|
|
1159
|
-
force: true
|
|
1160
|
-
});
|
|
1161
|
-
} catch {
|
|
1162
|
-
return failure({
|
|
1163
|
-
kind: "operational",
|
|
1164
|
-
code: "missing-project-state-removal-failed",
|
|
1165
|
-
observed: `Devbox could not remove Missing-root Project state for ${root}.`,
|
|
1166
|
-
nextAction: "Check write access to ~/.devbox and try cleanup again."
|
|
1167
|
-
});
|
|
1168
|
-
}
|
|
1169
|
-
delete projects[root];
|
|
1170
|
-
removedRoots.push(root);
|
|
1171
|
-
}
|
|
1172
|
-
if (removedRoots.length === 0) return success({
|
|
1173
|
-
roots: [],
|
|
1174
|
-
removed: false
|
|
1175
|
-
});
|
|
1176
|
-
const written = await writeAtomically(discovery.paths.projectRegistry, serializeProjectRegistry({
|
|
1177
|
-
version: 1,
|
|
1178
|
-
projects
|
|
1179
|
-
}));
|
|
1180
|
-
if (!written.ok) return written;
|
|
1181
|
-
return success({
|
|
1182
|
-
roots: removedRoots,
|
|
1183
|
-
removed: true
|
|
1184
|
-
});
|
|
1185
|
-
}
|
|
1186
1102
|
async function runSandboxLifecycle(command, input = {}) {
|
|
1187
1103
|
const projectRoot = input.root ?? process.cwd();
|
|
1188
1104
|
return withStateLocks({
|
|
@@ -1301,14 +1217,6 @@ function allocateSandboxName(projectRoot, registry) {
|
|
|
1301
1217
|
nextAction: "Remove stale Devbox state only through its supported command and try again."
|
|
1302
1218
|
});
|
|
1303
1219
|
}
|
|
1304
|
-
async function projectRootExists(projectRoot) {
|
|
1305
|
-
try {
|
|
1306
|
-
return (await lstat(projectRoot)).isDirectory();
|
|
1307
|
-
} catch (error) {
|
|
1308
|
-
if (isMissingFileError(error)) return false;
|
|
1309
|
-
return;
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
1220
|
async function readOptionalFile(path) {
|
|
1313
1221
|
try {
|
|
1314
1222
|
return success({
|
|
@@ -1537,17 +1445,15 @@ function createConfigurationPrompter({ signal, input, output }) {
|
|
|
1537
1445
|
message: "Where should this configuration apply?",
|
|
1538
1446
|
options: [{
|
|
1539
1447
|
value: "local",
|
|
1540
|
-
label: "Current project"
|
|
1541
|
-
hint: "Enabled when this project’s container starts."
|
|
1448
|
+
label: "Current project"
|
|
1542
1449
|
}, {
|
|
1543
1450
|
value: "global",
|
|
1544
|
-
label: "Workspace Docker image"
|
|
1545
|
-
hint: "Built into the shared image. It affects image size and disk usage."
|
|
1451
|
+
label: "Workspace Docker image"
|
|
1546
1452
|
}],
|
|
1547
1453
|
...common
|
|
1548
1454
|
})),
|
|
1549
1455
|
editGlobal: async (configuration, catalog) => {
|
|
1550
|
-
p.log.step(
|
|
1456
|
+
p.log.step(c.bold(c.cyan("Workspace Docker image")), common);
|
|
1551
1457
|
return {
|
|
1552
1458
|
version: 1,
|
|
1553
1459
|
node: await promptValue(p.multiselect({
|
|
@@ -1572,7 +1478,7 @@ function createConfigurationPrompter({ signal, input, output }) {
|
|
|
1572
1478
|
};
|
|
1573
1479
|
},
|
|
1574
1480
|
editLocal: async (configuration, catalog, globalConfiguration, editPorts = true) => {
|
|
1575
|
-
p.log.step(
|
|
1481
|
+
p.log.step(c.bold(c.cyan("Project configuration")), common);
|
|
1576
1482
|
const node = await promptValue(p.select({
|
|
1577
1483
|
message: "Selected Node release line",
|
|
1578
1484
|
options: [{
|
|
@@ -1590,7 +1496,7 @@ function createConfigurationPrompter({ signal, input, output }) {
|
|
|
1590
1496
|
node
|
|
1591
1497
|
};
|
|
1592
1498
|
const ports = normalizePortMappings((await promptValue(p.multiline({
|
|
1593
|
-
message: "Published ports",
|
|
1499
|
+
message: "Published ports" + c.dim(" (multiline; one mapping per line; press Enter twice to submit)"),
|
|
1594
1500
|
initialValue: configuration.ports.join("\n"),
|
|
1595
1501
|
validate: (value) => {
|
|
1596
1502
|
const ports = normalizePortMappings((value ?? "").split("\n"));
|
|
@@ -1642,34 +1548,210 @@ function showLogo() {
|
|
|
1642
1548
|
for (let i = 0; i < ASCII_ART.length; i++) console.log(`${COLORS[i]}${ASCII_ART[i]}${REST}`);
|
|
1643
1549
|
}
|
|
1644
1550
|
//#endregion
|
|
1645
|
-
//#region \0devbox-
|
|
1646
|
-
var _devbox_workspace_image_default__bash_aliases_default = "alias ..='cd ..'\nalias ...='cd ../..'\nalias c='clear'\nalias g='git'\nalias ccc='claude --dangerously-skip-permissions'\nalias ccx='codex --sandbox workspace-write --ask-for-approval never'\nalias ggg='agy --dangerously-skip-permissions'\n\nfunction mkcd() {\n mkdir \"$1\"\n cd \"$1\"\n}\n\n# Laravel Artisan\nalias pa='php artisan'\nalias pav='php artisan serve'\nalias pam='php artisan migrate'\nalias pams='php artisan migrate --seed'\nalias pamr='php artisan migrate:rollback'\nalias pamf='php artisan migrate:fresh'\nalias pamfs='php artisan migrate:fresh --seed'\nalias pas='php artisan db:seed'\nalias par='php artisan route:list'\nalias paq='php artisan queue:work'\nalias pah='php artisan horizon'\nalias pat='php artisan tinker'\nalias phpunit='./vendor/bin/phpunit --colors=always'\nalias pu='phpunit'\nalias pest='./vendor/bin/pest --colors=always'\nalias p='pest'\nalias pint='./vendor/bin/pint'\nalias phpstan='./vendor/bin/phpstan analyse --ansi'\nalias rector='./vendor/bin/rector'\n\n# NPM (requires @antfu/ni)\nalias nv='nr dev' # nd exists in @antfu/ni\nalias nb='nr build'\nalias nll='nr lint' # nl is a Linux command\nalias nlf='nr lint --fix'\nalias nf='nr fmt'\nalias nfc='nr fmt --check'\nalias nc='nr check'\nalias nt='nr test'\nalias ntr='nr test --run'\n";
|
|
1647
|
-
//#endregion
|
|
1648
|
-
//#region \0devbox-workspace-image-default:.claude/settings.json.mjs
|
|
1551
|
+
//#region \0devbox-image-default:/home/runner/work/devbox/devbox/src/agent/image-defaults/.claude/settings.json.mjs
|
|
1649
1552
|
var settings_json_default = "{\n \"$schema\": \"https://json.schemastore.org/claude-code-settings.json\",\n \"env\": {\n \"CLAUDE_CODE_USE_POWERSHELL_TOOL\": \"0\"\n },\n \"attribution\": {\n \"commit\": \"\",\n \"pr\": \"\"\n },\n \"includeGitInstructions\": false,\n \"autoMemoryEnabled\": false\n}\n";
|
|
1650
1553
|
//#endregion
|
|
1651
|
-
//#region \0devbox-
|
|
1652
|
-
var _devbox_workspace_image_default__gitconfig_default = "[core]\n autocrlf = input\n fileMode = false\n pager = less -r\n quotepath = false\n[init]\n defaultBranch = main\n[color]\n diff = auto\n status = auto\n branch = auto\n[alias]\n cf = config\n cfl = config --list\n s = status\n a = add .\n l = log --oneline --graph\n la = log --oneline --graph --all\n last = log -1 HEAD\n c = commit\n cm = commit -m\n ca = commit --amend -m\n can = commit --amend --no-edit\n co = checkout\n cp = cherry-pick\n b = branch\n m = merge\n mnf = merge --no-ff\n rs = reset\n rs0h = reset HEAD --hard\n rs1 = reset HEAD~1\n rs1h = reset HEAD~1 --hard\n rb = rebase\n rbo = rebase --onto\n rbi = rebase -i\n rbc = rebase --continue\n rba = rebase --abort\n rm = remote\n r = remote -v\n ra = remote add\n rr = remote remove\n rp = remote prune\n f = fetch\n fp = fetch --prune\n p = push\n pl = pull\n";
|
|
1653
|
-
//#endregion
|
|
1654
|
-
//#region \0devbox-workspace-image-default:.omp/agent/config.yml.mjs
|
|
1554
|
+
//#region \0devbox-image-default:/home/runner/work/devbox/devbox/src/agent/image-defaults/.omp/agent/config.yml.mjs
|
|
1655
1555
|
var config_yml_default = "setupVersion: 2\n\n# Basic\nmodelRoles:\n default: openai-codex/gpt-5.6-terra:high\n smol: openai-codex/gpt-5.6-luna:high\n slow: openai-codex/gpt-5.6-sol:max\n plan: openai-codex/gpt-5.6-sol:max\nsymbolPreset: unicode\n\n# Theme\ntheme:\n dark: titanium\n light: light\ncomposer:\n shape: claude\n\n# Auto QA\ndev:\n autoqa: false\n autoqaConsent: denied\n";
|
|
1656
1556
|
//#endregion
|
|
1557
|
+
//#region \0devbox-image-default:/home/runner/work/devbox/devbox/src/agent/image-defaults/AGENTS.md.mjs
|
|
1558
|
+
var AGENTS_md_default = "# 協作規範\n\n## 語言\n\n- 一律使用繁體中文台灣用語產生對話及結果\n- 嚴禁使用簡體字,或是繁體字但中國大陸用語\n- 用詞要口語、常見,避免生硬或翻譯腔的詞彙;例如「根因」改用「原因」、「根本原因」、「主要原因」\n\n## 開發規範\n\n### 共用原則\n\n- 優先遵循專案既有的 `AGENTS.md`、文件、設定檔、lockfile、指令與程式碼慣例;專案沒有既有規範時,才使用本文件的預設選擇\n- 優先使用目前環境提供的專用工具;只有在專用工具無法完成工作或需要執行專案指令時,才使用適合的 CLI 工具\n- 執行會修改檔案、安裝套件或變更環境的指令前,確認目前位於預期的工作目錄\n- 可以在目前 workspace 內存取所需的上層目錄;存取 workspace 以外的檔案或目錄前,必須先取得使用者同意\n- 嚴禁執行任何會遞迴刪除根目錄的指令(例如 `rm -rf /`),也不得改寫或規避為效果相同的形式\n- 執行其他可能造成不可逆資料遺失的操作前,必須說明影響並取得使用者明確同意\n\n### JavaScript / TypeScript\n\n- 優先使用專案既有的 Node.js 版本設定與套件管理器\n- 專案未指定套件管理器且不存在 lockfile 時,使用 npm\n- 需要執行專案套件提供的 CLI 時,使用專案既有套件管理器對應的執行方式;使用 npm 時使用 npx\n- 不得擅自更換套件管理器、刪除 lockfile 或產生不同格式的 lockfile\n- TypeScript 格式規則\n - 陣列型別優先使用 `T[]`,避免使用 `Array<T>`\n - 字典型物件優先使用 `Record<string, T>`,避免使用 `{ [key: string]: T }`\n- Vue 3 程式碼變更前,先讀取 `/ycs77-vue` skill\n\n### PHP\n\n- 優先使用專案既有的 Composer scripts\n\n### 常見 CLI 工具\n\n- 版本控制操作使用 `git`\n- 所有 Git 操作都必須直接執行 `git` CLI\n- 禁止透過 Eval、JavaScript、Bun、Node.js、Python 或臨時程式碼間接包裝或呼叫 Git 指令\n- GitHub 相關操作優先使用 `gh` CLI\n\n## 安全\n\n- 在任何對話、指令輸出或程式碼中,不得揭露密碼、金鑰、Access Token、API Key 等機密資料\n- 不得讀取或輸出 `.env`、`.env.*` 等可能包含機密資料的檔案;不含真實機密的範本檔案(例如 `.env.example`)除外\n- 開發時需要使用機密資料,應以環境變數名稱或假值代稱,不得嘗試取得實際值\n";
|
|
1559
|
+
//#endregion
|
|
1560
|
+
//#region src/agent/build.ts
|
|
1561
|
+
function createAgentWorkspaceContribution(input) {
|
|
1562
|
+
const agents = input.configuredAgents.map((name) => ({
|
|
1563
|
+
name,
|
|
1564
|
+
recipe: PACKAGED_AGENTS[name]
|
|
1565
|
+
}));
|
|
1566
|
+
const buildContextAssets = selectBuildContextAssets(agents);
|
|
1567
|
+
const skillAgents = [...new Set(agents.flatMap((agent) => agent.recipe.skillInstallationTarget === void 0 ? [] : [agent.recipe.skillInstallationTarget]))];
|
|
1568
|
+
return {
|
|
1569
|
+
buildContextAssets,
|
|
1570
|
+
dockerignoreEntries: selectDockerignoreEntries(buildContextAssets),
|
|
1571
|
+
installationDockerfileLines: renderAgentInstallations(agents, input.buildNodeRuntimeRoot, skillAgents),
|
|
1572
|
+
assetDockerfileLines: renderAgentAssets(agents),
|
|
1573
|
+
notificationDockerfileLines: renderAgentNotifications(agents, input.notificationsEnabled),
|
|
1574
|
+
entrypointSetup: renderAgentEntrypointSetup(agents)
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
function renderAgentInstallations(agents, buildNodeRuntimeRoot, skillAgents) {
|
|
1578
|
+
const lines = [];
|
|
1579
|
+
for (const agent of agents) {
|
|
1580
|
+
const preInstallCommands = agent.recipe.installation.preInstallCommands ?? [];
|
|
1581
|
+
lines.push(`# Install ${agent.name}`, `RUN mkdir -p ${agent.recipe.home.target} \\`, ...preInstallCommands.map((command) => ` && ${command} \\`), ` && curl -fsSL ${agent.recipe.installation.url} | ${agent.recipe.installation.shell}`, "");
|
|
1582
|
+
}
|
|
1583
|
+
if (agents.length > 0) lines.push("# Set the PATH to include AI tools", "ENV PATH=\"/home/devbox/.local/bin:${PATH}\"", "");
|
|
1584
|
+
if (buildNodeRuntimeRoot !== void 0 && skillAgents.length > 0) {
|
|
1585
|
+
const skillAgentArguments = skillAgents.map((agent) => `-a ${agent}`).join(" ");
|
|
1586
|
+
lines.push("# Install Agent Skills", "RUN set -eux \\", " && mkdir -p /home/devbox/.agents/skills \\", ` && export PATH="${buildNodeRuntimeRoot}/bin:$PATH" \\`, ` && npx -y skills add ycs77/skills -g ${skillAgentArguments} -s '*' -y`, "");
|
|
1587
|
+
}
|
|
1588
|
+
return lines;
|
|
1589
|
+
}
|
|
1590
|
+
function selectDockerignoreEntries(assets) {
|
|
1591
|
+
const entries = [];
|
|
1592
|
+
if (assets.some((asset) => asset.relativePath === "AGENTS.md")) entries.push("!AGENTS.md");
|
|
1593
|
+
if (assets.some((asset) => asset.relativePath.startsWith(".claude/"))) entries.push("!.claude", "!.claude/settings.json");
|
|
1594
|
+
if (assets.some((asset) => asset.relativePath.startsWith(".omp/agent/"))) entries.push("!.omp", "!.omp/agent", "!.omp/agent/config.yml");
|
|
1595
|
+
return entries;
|
|
1596
|
+
}
|
|
1597
|
+
function selectBuildContextAssets(agents) {
|
|
1598
|
+
const assets = [];
|
|
1599
|
+
if (agents.length > 0) assets.push({
|
|
1600
|
+
relativePath: "AGENTS.md",
|
|
1601
|
+
content: AGENTS_md_default
|
|
1602
|
+
});
|
|
1603
|
+
if (agents.some((agent) => agent.name === "claude-code")) assets.push({
|
|
1604
|
+
relativePath: ".claude/settings.json",
|
|
1605
|
+
content: settings_json_default
|
|
1606
|
+
});
|
|
1607
|
+
if (agents.some((agent) => agent.name === "omp")) assets.push({
|
|
1608
|
+
relativePath: ".omp/agent/config.yml",
|
|
1609
|
+
content: config_yml_default
|
|
1610
|
+
});
|
|
1611
|
+
return assets;
|
|
1612
|
+
}
|
|
1613
|
+
function renderAgentAssets(agents) {
|
|
1614
|
+
const hasClaudeCode = agents.some((agent) => agent.name === "claude-code");
|
|
1615
|
+
const hasCodex = agents.some((agent) => agent.name === "codex");
|
|
1616
|
+
const hasAgy = agents.some((agent) => agent.name === "agy");
|
|
1617
|
+
const hasOmp = agents.some((agent) => agent.name === "omp");
|
|
1618
|
+
if (!hasClaudeCode && !hasCodex && !hasAgy && !hasOmp) return [];
|
|
1619
|
+
const lines = ["# Copy AI dotfiles"];
|
|
1620
|
+
if (hasClaudeCode) lines.push("COPY --chown=devbox:devbox .claude/settings.json /home/devbox/.claude/settings.json", "COPY --chown=devbox:devbox AGENTS.md /home/devbox/.claude/CLAUDE.md");
|
|
1621
|
+
if (hasCodex) lines.push("COPY --chown=devbox:devbox AGENTS.md /home/devbox/.codex/AGENTS.md");
|
|
1622
|
+
if (hasAgy) lines.push("COPY --chown=devbox:devbox AGENTS.md /home/devbox/.gemini/GEMINI.md");
|
|
1623
|
+
if (hasOmp) lines.push("COPY --chown=devbox:devbox .omp/agent/config.yml /home/devbox/.omp/agent/config.yml", "COPY --chown=devbox:devbox AGENTS.md /home/devbox/.omp/agent/AGENTS.md");
|
|
1624
|
+
lines.push("");
|
|
1625
|
+
return lines;
|
|
1626
|
+
}
|
|
1627
|
+
function renderAgentNotifications(agents, notificationsEnabled) {
|
|
1628
|
+
const commands = notificationsEnabled ? agents.flatMap((agent) => agent.recipe.supportsNotifications ? agent.recipe.notificationInstallationCommands : []) : [];
|
|
1629
|
+
if (commands.length === 0) return [];
|
|
1630
|
+
return [
|
|
1631
|
+
"# Install Agent Notification Plugins",
|
|
1632
|
+
"USER devbox",
|
|
1633
|
+
"RUN set -eux \\",
|
|
1634
|
+
...commands.map((command, index) => ` && ${command}${index < commands.length - 1 ? " \\" : ""}`),
|
|
1635
|
+
"USER root",
|
|
1636
|
+
""
|
|
1637
|
+
];
|
|
1638
|
+
}
|
|
1639
|
+
function renderAgentEntrypointSetup(agents) {
|
|
1640
|
+
if (!agents.some((agent) => agent.name === "agy")) return [];
|
|
1641
|
+
return [
|
|
1642
|
+
"# Link shared Agent Skills",
|
|
1643
|
+
"if [ ! -L \"/home/devbox/.gemini/skills\" ]; then",
|
|
1644
|
+
" ln -sfn /home/devbox/.agents/skills /home/devbox/.gemini/skills",
|
|
1645
|
+
" chown -h devbox:devbox /home/devbox/.gemini/skills",
|
|
1646
|
+
"fi",
|
|
1647
|
+
""
|
|
1648
|
+
];
|
|
1649
|
+
}
|
|
1650
|
+
//#endregion
|
|
1651
|
+
//#region src/runtimes/node/build.ts
|
|
1652
|
+
function createNodeWorkspaceContribution(configuredReleaseLines) {
|
|
1653
|
+
const runtimes = configuredReleaseLines.map((releaseLine) => PACKAGED_NODE_RECIPES[releaseLine]);
|
|
1654
|
+
const buildRuntime = selectBuildNodeRuntime(runtimes);
|
|
1655
|
+
return {
|
|
1656
|
+
buildNodeRuntimeRoot: buildRuntime?.runtimeRoot,
|
|
1657
|
+
runtimeInstallations: runtimes.flatMap(renderNodeRuntimeStage),
|
|
1658
|
+
sandboxUserSetup: buildRuntime === void 0 ? [] : [
|
|
1659
|
+
"# Configure pnpm for the non-root Sandbox user",
|
|
1660
|
+
"RUN mkdir -p /home/devbox/.config/pnpm /home/devbox/.pnpm-store \\",
|
|
1661
|
+
" && printf 'storeDir: /home/devbox/.pnpm-store\\n' > /home/devbox/.config/pnpm/config.yaml \\",
|
|
1662
|
+
" && chown devbox:devbox /home/devbox/.config/pnpm/config.yaml",
|
|
1663
|
+
""
|
|
1664
|
+
],
|
|
1665
|
+
entrypointSetup: renderNodeEntrypointSetup(runtimes)
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
function selectBuildNodeRuntime(runtimes) {
|
|
1669
|
+
let buildRuntime;
|
|
1670
|
+
for (const runtime of runtimes) if (buildRuntime === void 0 || Number.parseFloat(runtime.releaseLine) > Number.parseFloat(buildRuntime.releaseLine)) buildRuntime = runtime;
|
|
1671
|
+
return buildRuntime;
|
|
1672
|
+
}
|
|
1673
|
+
function renderNodeEntrypointSetup(runtimes) {
|
|
1674
|
+
if (runtimes.length === 0) return [];
|
|
1675
|
+
return [
|
|
1676
|
+
"# Set the Node.js release line to use",
|
|
1677
|
+
"NODE_VERSION=\"${NODE_VERSION:-}\"",
|
|
1678
|
+
"if [ -n \"$NODE_VERSION\" ]; then",
|
|
1679
|
+
" case \"$NODE_VERSION\" in",
|
|
1680
|
+
" *[!0-9]*)",
|
|
1681
|
+
" echo \"NODE_VERSION must be a numeric release line: $NODE_VERSION\" >&2",
|
|
1682
|
+
" exit 1",
|
|
1683
|
+
" ;;",
|
|
1684
|
+
" esac",
|
|
1685
|
+
"",
|
|
1686
|
+
" NODE_RUNTIME_ROOT=\"/opt/devbox/runtimes/node/$NODE_VERSION\"",
|
|
1687
|
+
" if [ ! -x \"$NODE_RUNTIME_ROOT/bin/node\" ]; then",
|
|
1688
|
+
" echo \"Node.js release line $NODE_VERSION is not installed.\" >&2",
|
|
1689
|
+
" exit 1",
|
|
1690
|
+
" fi",
|
|
1691
|
+
"",
|
|
1692
|
+
" if ! grep -q \"# Devbox\" /etc/bash.bashrc; then",
|
|
1693
|
+
" printf \"\\n# Devbox\\nexport PATH=\\\"$NODE_RUNTIME_ROOT/bin:\\$PATH\\\"\\n\" >> /etc/bash.bashrc",
|
|
1694
|
+
" fi",
|
|
1695
|
+
"",
|
|
1696
|
+
" export PATH=\"$NODE_RUNTIME_ROOT/bin:$PATH\"",
|
|
1697
|
+
"fi",
|
|
1698
|
+
""
|
|
1699
|
+
];
|
|
1700
|
+
}
|
|
1701
|
+
function renderNodeRuntimeStage(recipe) {
|
|
1702
|
+
const keys = recipe.trustedReleaseKeys.map((key) => ` ${key}`).join(" \\\n");
|
|
1703
|
+
return [
|
|
1704
|
+
`# Install Node.js ${recipe.releaseLine}`,
|
|
1705
|
+
"RUN ARCH= OPENSSL_ARCH= && dpkgArch=\"$(dpkg --print-architecture)\" \\",
|
|
1706
|
+
" && case \"${dpkgArch##*-}\" in \\",
|
|
1707
|
+
" amd64) ARCH='x64' OPENSSL_ARCH='linux-x86_64';; \\",
|
|
1708
|
+
" ppc64el) ARCH='ppc64le' OPENSSL_ARCH='linux-ppc64le';; \\",
|
|
1709
|
+
" s390x) ARCH='s390x' OPENSSL_ARCH='linux*-s390x';; \\",
|
|
1710
|
+
" arm64) ARCH='arm64' OPENSSL_ARCH='linux-aarch64';; \\",
|
|
1711
|
+
" *) echo \"unsupported architecture\"; exit 1 ;; \\",
|
|
1712
|
+
" esac \\",
|
|
1713
|
+
" && set -eux \\",
|
|
1714
|
+
` && NODE_RUNTIME_VERSION=${recipe.version} \\`,
|
|
1715
|
+
` && NODE_RUNTIME_ROOT=${recipe.runtimeRoot} \\`,
|
|
1716
|
+
" && mkdir -p \"$NODE_RUNTIME_ROOT\" /tmp/node-source \\",
|
|
1717
|
+
" && cd /tmp/node-source \\",
|
|
1718
|
+
" && export GNUPGHOME=\"$(mktemp -d)\" \\",
|
|
1719
|
+
" && for key in \\",
|
|
1720
|
+
`${keys} \\`,
|
|
1721
|
+
" ; do \\",
|
|
1722
|
+
" { gpg --batch --no-options --keyserver hkps://keys.openpgp.org --recv-keys \"$key\" && gpg --batch --no-options --fingerprint \"$key\"; } || \\",
|
|
1723
|
+
" { gpg --batch --no-options --keyserver keyserver.ubuntu.com --recv-keys \"$key\" && gpg --batch --no-options --fingerprint \"$key\"; }; \\",
|
|
1724
|
+
" done \\",
|
|
1725
|
+
" && archive=\"node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\" \\",
|
|
1726
|
+
` && curl -fsSLO --compressed "${recipe.distributionBaseUrl}/v\${NODE_RUNTIME_VERSION}/node-v\${NODE_RUNTIME_VERSION}-linux-\${ARCH}.tar.xz" \\`,
|
|
1727
|
+
` && curl -fsSLO --compressed "${recipe.distributionBaseUrl}/v\${NODE_RUNTIME_VERSION}/SHASUMS256.txt.asc" \\`,
|
|
1728
|
+
" && gpg --batch --no-options --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \\",
|
|
1729
|
+
" && gpgconf --kill all \\",
|
|
1730
|
+
" && rm -rf \"$GNUPGHOME\" \\",
|
|
1731
|
+
" && grep \" node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\\$\" SHASUMS256.txt | sha256sum -c - \\",
|
|
1732
|
+
" && tar -xJf \"node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\" -C \"$NODE_RUNTIME_ROOT\" --strip-components=1 --no-same-owner \\",
|
|
1733
|
+
" && rm \"node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\" SHASUMS256.txt.asc SHASUMS256.txt \\",
|
|
1734
|
+
" && find \"$NODE_RUNTIME_ROOT/include/node/openssl/archs\" -mindepth 1 -maxdepth 1 ! -name \"$OPENSSL_ARCH\" -exec rm -rf {} \\; \\",
|
|
1735
|
+
" && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \\",
|
|
1736
|
+
" && export PATH=\"$NODE_RUNTIME_ROOT/bin:$PATH\" \\",
|
|
1737
|
+
" && npm install -g npm \\",
|
|
1738
|
+
" && npm uninstall -g corepack \\",
|
|
1739
|
+
" && npm install -g yarn pnpm @antfu/ni --allow-scripts=pnpm,yarn \\",
|
|
1740
|
+
" && \"$NODE_RUNTIME_ROOT/bin/node\" --version \\",
|
|
1741
|
+
" && \"$NODE_RUNTIME_ROOT/bin/npm\" --version \\",
|
|
1742
|
+
" && rm -rf /tmp/node-source",
|
|
1743
|
+
""
|
|
1744
|
+
];
|
|
1745
|
+
}
|
|
1746
|
+
//#endregion
|
|
1657
1747
|
//#region src/workspace/build.ts
|
|
1658
1748
|
const BASE_WORKSPACE_IMAGE_DEFAULTS = [{
|
|
1659
1749
|
relativePath: ".bash_aliases",
|
|
1660
|
-
content:
|
|
1750
|
+
content: "alias ..='cd ..'\nalias ...='cd ../..'\nalias c='clear'\nalias g='git'\nalias ccc='claude --dangerously-skip-permissions'\nalias ccx='codex --sandbox workspace-write --ask-for-approval never'\nalias ggg='agy --dangerously-skip-permissions'\n\nfunction mkcd() {\n mkdir \"$1\"\n cd \"$1\"\n}\n\n# Laravel Artisan\nalias pa='php artisan'\nalias pav='php artisan serve'\nalias pam='php artisan migrate'\nalias pams='php artisan migrate --seed'\nalias pamr='php artisan migrate:rollback'\nalias pamf='php artisan migrate:fresh'\nalias pamfs='php artisan migrate:fresh --seed'\nalias pas='php artisan db:seed'\nalias par='php artisan route:list'\nalias paq='php artisan queue:work'\nalias pah='php artisan horizon'\nalias pat='php artisan tinker'\nalias phpunit='./vendor/bin/phpunit --colors=always'\nalias pu='phpunit'\nalias pest='./vendor/bin/pest --colors=always'\nalias p='pest'\nalias pint='./vendor/bin/pint'\nalias phpstan='./vendor/bin/phpstan analyse --ansi'\nalias rector='./vendor/bin/rector'\n\n# NPM (requires @antfu/ni)\nalias nv='nr dev' # nd exists in @antfu/ni\nalias nb='nr build'\nalias nll='nr lint' # nl is a Linux command\nalias nlf='nr lint --fix'\nalias nf='nr fmt'\nalias nfc='nr fmt --check'\nalias nc='nr check'\nalias nt='nr test'\nalias ntr='nr test --run'\n"
|
|
1661
1751
|
}, {
|
|
1662
1752
|
relativePath: ".gitconfig",
|
|
1663
|
-
content:
|
|
1753
|
+
content: "[core]\n autocrlf = input\n fileMode = false\n pager = less -r\n quotepath = false\n[init]\n defaultBranch = main\n[color]\n diff = auto\n status = auto\n branch = auto\n[alias]\n cf = config\n cfl = config --list\n s = status\n a = add .\n l = log --oneline --graph\n la = log --oneline --graph --all\n last = log -1 HEAD\n c = commit\n cm = commit -m\n ca = commit --amend -m\n can = commit --amend --no-edit\n co = checkout\n cp = cherry-pick\n b = branch\n m = merge\n mnf = merge --no-ff\n rs = reset\n rs0h = reset HEAD --hard\n rs1 = reset HEAD~1\n rs1h = reset HEAD~1 --hard\n rb = rebase\n rbo = rebase --onto\n rbi = rebase -i\n rbc = rebase --continue\n rba = rebase --abort\n rm = remote\n r = remote -v\n ra = remote add\n rr = remote remove\n rp = remote prune\n f = fetch\n fp = fetch --prune\n p = push\n pl = pull\n"
|
|
1664
1754
|
}];
|
|
1665
|
-
const CLAUDE_SETTINGS_DEFAULT = {
|
|
1666
|
-
relativePath: ".claude/settings.json",
|
|
1667
|
-
content: settings_json_default
|
|
1668
|
-
};
|
|
1669
|
-
const OMP_AGENT_CONFIG_DEFAULT = {
|
|
1670
|
-
relativePath: ".omp/agent/config.yml",
|
|
1671
|
-
content: config_yml_default
|
|
1672
|
-
};
|
|
1673
1755
|
async function buildWorkspace(input = {}) {
|
|
1674
1756
|
return withStateLocks({
|
|
1675
1757
|
devboxHome: input.devboxHome ?? join(homedir(), ".devbox"),
|
|
@@ -1691,7 +1773,7 @@ async function buildWorkspaceUnlocked(input) {
|
|
|
1691
1773
|
kind: "operational",
|
|
1692
1774
|
code: "global-configuration-missing",
|
|
1693
1775
|
observed: "Global configuration is required before building the Workspace image.",
|
|
1694
|
-
nextAction: "Run devbox
|
|
1776
|
+
nextAction: "Run devbox init to commit Global configuration before devbox build."
|
|
1695
1777
|
});
|
|
1696
1778
|
}
|
|
1697
1779
|
const parsedGlobal = parseGlobalConfiguration(globalSource, catalogCheck.value);
|
|
@@ -1699,27 +1781,25 @@ async function buildWorkspaceUnlocked(input) {
|
|
|
1699
1781
|
const globalConfiguration = parsedGlobal.value;
|
|
1700
1782
|
const hostIdentity = currentHostIdentity();
|
|
1701
1783
|
if (!hostIdentity.ok) return hostIdentity;
|
|
1702
|
-
const
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1784
|
+
const node = createNodeWorkspaceContribution(globalConfiguration.node);
|
|
1785
|
+
const agent = createAgentWorkspaceContribution({
|
|
1786
|
+
configuredAgents: globalConfiguration.agent,
|
|
1787
|
+
notificationsEnabled: globalConfiguration.agent_notifications,
|
|
1788
|
+
buildNodeRuntimeRoot: node.buildNodeRuntimeRoot
|
|
1789
|
+
});
|
|
1790
|
+
const buildContextAssets = [...BASE_WORKSPACE_IMAGE_DEFAULTS, ...agent.buildContextAssets];
|
|
1708
1791
|
const dockerfile = renderBuildDockerfile({
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
agents,
|
|
1712
|
-
agentNotifications: globalConfiguration.agent_notifications,
|
|
1713
|
-
skillAgents: globalConfiguration.agent.filter((agent) => PACKAGED_AGENTS[agent]?.supportsSkillInstallation === true)
|
|
1792
|
+
node,
|
|
1793
|
+
agent
|
|
1714
1794
|
});
|
|
1715
1795
|
await mkdir(paths.buildContext, { recursive: true });
|
|
1716
1796
|
await seedBuildContextAssets(paths.buildContext, buildContextAssets);
|
|
1717
1797
|
await Promise.all([
|
|
1718
1798
|
writeFile(join(paths.buildContext, "Dockerfile"), dockerfile),
|
|
1719
|
-
writeFile(join(paths.buildContext, ".dockerignore"), renderDockerignore(buildContextAssets)),
|
|
1799
|
+
writeFile(join(paths.buildContext, ".dockerignore"), renderDockerignore(buildContextAssets, agent.dockerignoreEntries)),
|
|
1720
1800
|
writeFile(join(paths.buildContext, "entrypoint.sh"), renderEntrypoint({
|
|
1721
|
-
|
|
1722
|
-
|
|
1801
|
+
node,
|
|
1802
|
+
agent
|
|
1723
1803
|
})),
|
|
1724
1804
|
writeFile(join(paths.buildContext, "supervisord.conf"), renderSupervisorConfiguration())
|
|
1725
1805
|
]);
|
|
@@ -1733,14 +1813,6 @@ async function buildWorkspaceUnlocked(input) {
|
|
|
1733
1813
|
if (!buildResult.ok) return buildResult;
|
|
1734
1814
|
return success({ image: WORKSPACE_IMAGE });
|
|
1735
1815
|
}
|
|
1736
|
-
function buildNodeRuntimeRecipe(globalConfiguration) {
|
|
1737
|
-
let buildNodeRuntime;
|
|
1738
|
-
for (const releaseLine of globalConfiguration.node) {
|
|
1739
|
-
const recipe = PACKAGED_NODE_RECIPES[releaseLine];
|
|
1740
|
-
if (buildNodeRuntime === void 0 || Number.parseFloat(releaseLine) > Number.parseFloat(buildNodeRuntime.releaseLine)) buildNodeRuntime = recipe;
|
|
1741
|
-
}
|
|
1742
|
-
return buildNodeRuntime;
|
|
1743
|
-
}
|
|
1744
1816
|
function currentHostIdentity() {
|
|
1745
1817
|
const userId = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
1746
1818
|
const groupId = typeof process.getgid === "function" ? process.getgid() : void 0;
|
|
@@ -1797,51 +1869,26 @@ function renderBuildDockerfile(input) {
|
|
|
1797
1869
|
" && chmod 0440 /etc/sudoers.d/devbox",
|
|
1798
1870
|
""
|
|
1799
1871
|
];
|
|
1800
|
-
|
|
1872
|
+
lines.push(...input.node.runtimeInstallations);
|
|
1801
1873
|
lines.push("WORKDIR /workspace", "USER devbox", "");
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
if (input.agents.length > 0) lines.push("# Set the PATH to include AI tools", "ENV PATH=\"/home/devbox/.local/bin:${PATH}\"", "");
|
|
1808
|
-
if (input.buildNodeRuntime !== void 0 && input.skillAgents.length > 0) {
|
|
1809
|
-
const skillAgentArguments = input.skillAgents.map((agent) => `-a ${agent}`).join(" ");
|
|
1810
|
-
lines.push("# Install Agent Skills", "RUN set -eux \\", " && mkdir -p /home/devbox/.agents/skills \\", ` && export PATH="${input.buildNodeRuntime.runtimeRoot}/bin:$PATH" \\`, ` && npx -y skills add ycs77/skills -g ${skillAgentArguments} -s '*' -y`, "");
|
|
1811
|
-
}
|
|
1812
|
-
lines.push("USER root", "", "# Copy dotfiles", "COPY .bash_aliases /home/devbox/.bash_aliases", "COPY .gitconfig /home/devbox/.gitconfig", "RUN chown devbox:devbox /home/devbox/.bash_aliases \\", " && chown devbox:devbox /home/devbox/.gitconfig", "");
|
|
1813
|
-
const hasClaudeCode = input.agents.some((agent) => agent.name === "claude-code");
|
|
1814
|
-
const hasOmp = input.agents.some((agent) => agent.name === "omp");
|
|
1815
|
-
if (hasClaudeCode || hasOmp) {
|
|
1816
|
-
const ownershipCommands = [];
|
|
1817
|
-
lines.push("# Copy AI dotfiles");
|
|
1818
|
-
if (hasClaudeCode) {
|
|
1819
|
-
lines.push("COPY .claude/settings.json /home/devbox/.claude/settings.json");
|
|
1820
|
-
ownershipCommands.push("chown devbox:devbox /home/devbox/.claude/settings.json");
|
|
1821
|
-
}
|
|
1822
|
-
if (hasOmp) {
|
|
1823
|
-
lines.push("COPY .omp/agent/config.yml /home/devbox/.omp/agent/config.yml");
|
|
1824
|
-
ownershipCommands.push("chown devbox:devbox /home/devbox/.omp/agent", "chown devbox:devbox /home/devbox/.omp/agent/config.yml");
|
|
1825
|
-
}
|
|
1826
|
-
lines.push(`RUN ${ownershipCommands.join(" \\\n && ")}`, "");
|
|
1827
|
-
}
|
|
1828
|
-
const notificationInstallationCommands = input.agentNotifications ? input.agents.flatMap((agent) => agent.recipe.supportsNotifications ? agent.recipe.notificationInstallationCommands : []) : [];
|
|
1829
|
-
if (notificationInstallationCommands.length > 0) lines.push("# Install Agent Notification Plugins", "USER devbox", "RUN set -eux \\", ...notificationInstallationCommands.map((command, index) => ` && ${command}${index < notificationInstallationCommands.length - 1 ? " \\" : ""}`), "USER root", "");
|
|
1874
|
+
lines.push(...input.node.sandboxUserSetup);
|
|
1875
|
+
lines.push(...input.agent.installationDockerfileLines);
|
|
1876
|
+
lines.push("USER root", "", "# Copy dotfiles", "COPY --chown=devbox:devbox .bash_aliases /home/devbox/.bash_aliases", "COPY --chown=devbox:devbox .gitconfig /home/devbox/.gitconfig", "");
|
|
1877
|
+
lines.push(...input.agent.assetDockerfileLines);
|
|
1878
|
+
lines.push(...input.agent.notificationDockerfileLines);
|
|
1830
1879
|
lines.push("COPY entrypoint.sh /usr/local/bin/entrypoint.sh", "COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf", "RUN chmod +x /usr/local/bin/entrypoint.sh", "", "ENTRYPOINT [\"/usr/local/bin/entrypoint.sh\"]");
|
|
1831
1880
|
return `${lines.join("\n")}\n`;
|
|
1832
1881
|
}
|
|
1833
|
-
function renderDockerignore(assets) {
|
|
1834
|
-
|
|
1882
|
+
function renderDockerignore(assets, additionalEntries) {
|
|
1883
|
+
return `${[
|
|
1835
1884
|
"# Machine-owned Workspace build context",
|
|
1836
1885
|
"*",
|
|
1837
1886
|
"!Dockerfile",
|
|
1838
1887
|
"!entrypoint.sh",
|
|
1839
1888
|
"!supervisord.conf",
|
|
1840
|
-
...assets.map((asset) => `!${asset.relativePath}`)
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
if (assets.some((asset) => asset.relativePath === ".omp/agent/config.yml")) lines.push("!.omp", "!.omp/agent", "!.omp/agent/config.yml");
|
|
1844
|
-
return `${lines.join("\n")}\n`;
|
|
1889
|
+
...assets.map((asset) => `!${asset.relativePath}`),
|
|
1890
|
+
...additionalEntries
|
|
1891
|
+
].join("\n")}\n`;
|
|
1845
1892
|
}
|
|
1846
1893
|
async function seedBuildContextAssets(buildContext, assets) {
|
|
1847
1894
|
await Promise.all(assets.map(async (asset) => {
|
|
@@ -1854,18 +1901,18 @@ async function seedBuildContextAssets(buildContext, assets) {
|
|
|
1854
1901
|
}
|
|
1855
1902
|
}));
|
|
1856
1903
|
}
|
|
1857
|
-
function selectBuildContextAssets(agents) {
|
|
1858
|
-
const assets = [...BASE_WORKSPACE_IMAGE_DEFAULTS];
|
|
1859
|
-
if (agents.some((agent) => agent.name === "claude-code")) assets.push(CLAUDE_SETTINGS_DEFAULT);
|
|
1860
|
-
if (agents.some((agent) => agent.name === "omp")) assets.push(OMP_AGENT_CONFIG_DEFAULT);
|
|
1861
|
-
return assets;
|
|
1862
|
-
}
|
|
1863
1904
|
function renderEntrypoint(input) {
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1905
|
+
return `${[
|
|
1906
|
+
"#!/usr/bin/env bash",
|
|
1907
|
+
"",
|
|
1908
|
+
...input.node.entrypointSetup,
|
|
1909
|
+
...input.agent.entrypointSetup,
|
|
1910
|
+
"if [ $# -gt 0 ]; then",
|
|
1911
|
+
" exec gosu devbox \"$@\"",
|
|
1912
|
+
"else",
|
|
1913
|
+
" exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf",
|
|
1914
|
+
"fi"
|
|
1915
|
+
].join("\n")}\n`;
|
|
1869
1916
|
}
|
|
1870
1917
|
function renderSupervisorConfiguration() {
|
|
1871
1918
|
return `${[
|
|
@@ -1886,51 +1933,6 @@ function renderSupervisorConfiguration() {
|
|
|
1886
1933
|
"stderr_logfile=/dev/null"
|
|
1887
1934
|
].join("\n")}\n`;
|
|
1888
1935
|
}
|
|
1889
|
-
function renderNodeRuntimeStage(recipe) {
|
|
1890
|
-
const keys = recipe.trustedReleaseKeys.map((key) => ` ${key}`).join(" \\\n");
|
|
1891
|
-
return [
|
|
1892
|
-
`# Install Node.js ${recipe.releaseLine}`,
|
|
1893
|
-
"RUN ARCH= OPENSSL_ARCH= && dpkgArch=\"$(dpkg --print-architecture)\" \\",
|
|
1894
|
-
" && case \"${dpkgArch##*-}\" in \\",
|
|
1895
|
-
" amd64) ARCH='x64' OPENSSL_ARCH='linux-x86_64';; \\",
|
|
1896
|
-
" ppc64el) ARCH='ppc64le' OPENSSL_ARCH='linux-ppc64le';; \\",
|
|
1897
|
-
" s390x) ARCH='s390x' OPENSSL_ARCH='linux*-s390x';; \\",
|
|
1898
|
-
" arm64) ARCH='arm64' OPENSSL_ARCH='linux-aarch64';; \\",
|
|
1899
|
-
" *) echo \"unsupported architecture\"; exit 1 ;; \\",
|
|
1900
|
-
" esac \\",
|
|
1901
|
-
" && set -eux \\",
|
|
1902
|
-
` && NODE_RUNTIME_VERSION=${recipe.version} \\`,
|
|
1903
|
-
` && NODE_RUNTIME_ROOT=${recipe.runtimeRoot} \\`,
|
|
1904
|
-
" && mkdir -p \"$NODE_RUNTIME_ROOT\" /tmp/node-source \\",
|
|
1905
|
-
" && cd /tmp/node-source \\",
|
|
1906
|
-
" && export GNUPGHOME=\"$(mktemp -d)\" \\",
|
|
1907
|
-
" && for key in \\",
|
|
1908
|
-
`${keys} \\`,
|
|
1909
|
-
" ; do \\",
|
|
1910
|
-
" { gpg --batch --no-options --keyserver hkps://keys.openpgp.org --recv-keys \"$key\" && gpg --batch --no-options --fingerprint \"$key\"; } || \\",
|
|
1911
|
-
" { gpg --batch --no-options --keyserver keyserver.ubuntu.com --recv-keys \"$key\" && gpg --batch --no-options --fingerprint \"$key\"; }; \\",
|
|
1912
|
-
" done \\",
|
|
1913
|
-
" && archive=\"node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\" \\",
|
|
1914
|
-
" && curl -fsSLO --compressed \"https://nodejs.org/dist/v${NODE_RUNTIME_VERSION}/node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\" \\",
|
|
1915
|
-
" && curl -fsSLO --compressed \"https://nodejs.org/dist/v${NODE_RUNTIME_VERSION}/SHASUMS256.txt.asc\" \\",
|
|
1916
|
-
" && gpg --batch --no-options --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \\",
|
|
1917
|
-
" && gpgconf --kill all \\",
|
|
1918
|
-
" && rm -rf \"$GNUPGHOME\" \\",
|
|
1919
|
-
" && grep \" node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\\$\" SHASUMS256.txt | sha256sum -c - \\",
|
|
1920
|
-
" && tar -xJf \"node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\" -C \"$NODE_RUNTIME_ROOT\" --strip-components=1 --no-same-owner \\",
|
|
1921
|
-
" && rm \"node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\" SHASUMS256.txt.asc SHASUMS256.txt \\",
|
|
1922
|
-
" && find \"$NODE_RUNTIME_ROOT/include/node/openssl/archs\" -mindepth 1 -maxdepth 1 ! -name \"$OPENSSL_ARCH\" -exec rm -rf {} \\; \\",
|
|
1923
|
-
" && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \\",
|
|
1924
|
-
" && export PATH=\"$NODE_RUNTIME_ROOT/bin:$PATH\" \\",
|
|
1925
|
-
" && npm install -g npm \\",
|
|
1926
|
-
" && npm uninstall -g corepack \\",
|
|
1927
|
-
" && npm install -g yarn pnpm @antfu/ni --allow-scripts=pnpm,yarn \\",
|
|
1928
|
-
" && \"$NODE_RUNTIME_ROOT/bin/node\" --version \\",
|
|
1929
|
-
" && \"$NODE_RUNTIME_ROOT/bin/npm\" --version \\",
|
|
1930
|
-
" && rm -rf /tmp/node-source",
|
|
1931
|
-
""
|
|
1932
|
-
];
|
|
1933
|
-
}
|
|
1934
1936
|
async function runDockerBuild(invocation, signal) {
|
|
1935
1937
|
const build = spawn("docker", [
|
|
1936
1938
|
"build",
|
|
@@ -1987,7 +1989,7 @@ function createCli(signal, interactive) {
|
|
|
1987
1989
|
outro("Project is already registered.");
|
|
1988
1990
|
return success({});
|
|
1989
1991
|
}
|
|
1990
|
-
outro(`Next: run ${
|
|
1992
|
+
outro(`Next: run ${command("devbox build")} to prepare your workspace, then ${command("devbox up")} to start it.`);
|
|
1991
1993
|
return success({});
|
|
1992
1994
|
});
|
|
1993
1995
|
cli.command("config", "Edit Global or Local configuration for the current Project.").action(async () => {
|
|
@@ -1999,7 +2001,7 @@ function createCli(signal, interactive) {
|
|
|
1999
2001
|
prompt
|
|
2000
2002
|
});
|
|
2001
2003
|
if (!result.ok) return result;
|
|
2002
|
-
outro(result.value.changed ?
|
|
2004
|
+
outro(result.value.changed ? `Global configuration updated. Run ${command("devbox build")} to rebuild the Workspace image.` : "Global configuration was not changed.");
|
|
2003
2005
|
return success({});
|
|
2004
2006
|
}
|
|
2005
2007
|
const result = await configureLocalProject({
|
|
@@ -2018,7 +2020,7 @@ function createCli(signal, interactive) {
|
|
|
2018
2020
|
if (!result.ok) return result;
|
|
2019
2021
|
return success({ message: `Built Workspace image: ${result.value.image}` });
|
|
2020
2022
|
});
|
|
2021
|
-
cli.command("rm", "Remove the current
|
|
2023
|
+
cli.command("rm", "Remove the current Project Sandbox and Devbox state.").option("--yes", "Skip the confirmation prompt.").action(async (options = {}) => {
|
|
2022
2024
|
if (!interactive && !options.yes) return interactiveFailure("devbox rm requires a TTY or --yes.", "Run devbox rm from a TTY, or pass --yes to confirm removal.");
|
|
2023
2025
|
const result = await removeProject({
|
|
2024
2026
|
signal,
|
|
@@ -2028,22 +2030,6 @@ function createCli(signal, interactive) {
|
|
|
2028
2030
|
if (!result.ok) return result;
|
|
2029
2031
|
return success({ message: result.value.removed ? `Removed Project: ${result.value.root}` : "Project removal was not changed." });
|
|
2030
2032
|
});
|
|
2031
|
-
cli.command("cleanup", "Remove explicitly selected disposable Devbox state.").option("--missing-projects", "Remove Missing-root Project registrations.").option("--yes", "Skip the confirmation prompt.").action(async (options = {}) => {
|
|
2032
|
-
if (!options.missingProjects) return failure({
|
|
2033
|
-
kind: "usage",
|
|
2034
|
-
code: "cleanup-mode-required",
|
|
2035
|
-
observed: "cleanup requires --missing-projects.",
|
|
2036
|
-
nextAction: "Run devbox cleanup --missing-projects."
|
|
2037
|
-
});
|
|
2038
|
-
if (!interactive && !options.yes) return interactiveFailure("devbox cleanup --missing-projects requires a TTY or --yes.", "Run devbox cleanup --missing-projects from a TTY, or pass --yes to confirm removal.");
|
|
2039
|
-
const result = await cleanupMissingProjects({
|
|
2040
|
-
signal,
|
|
2041
|
-
confirm: prompt.confirm,
|
|
2042
|
-
yes: options.yes
|
|
2043
|
-
});
|
|
2044
|
-
if (!result.ok) return result;
|
|
2045
|
-
return success({ message: result.value.removed ? `Removed Missing-root Projects: ${result.value.roots.join(", ")}` : "No Missing-root Projects were removed." });
|
|
2046
|
-
});
|
|
2047
2033
|
for (const [command, description] of [
|
|
2048
2034
|
["up", "Start the current Project Sandbox."],
|
|
2049
2035
|
["down", "Stop and remove the current Project Sandbox."],
|
|
@@ -2056,12 +2042,13 @@ function createCli(signal, interactive) {
|
|
|
2056
2042
|
const result = await runSandboxShell({ signal });
|
|
2057
2043
|
return result.ok ? success({}) : result;
|
|
2058
2044
|
});
|
|
2045
|
+
cli.version(version);
|
|
2059
2046
|
cli.help();
|
|
2060
2047
|
return cli;
|
|
2061
2048
|
}
|
|
2062
2049
|
function present(result) {
|
|
2063
2050
|
if (!result.ok) {
|
|
2064
|
-
process.stderr.write(`${result.error.observed}\n${result.error.nextAction}\n`);
|
|
2051
|
+
process.stderr.write(`${highlightCommands(result.error.observed)}\n${highlightCommands(result.error.nextAction)}\n`);
|
|
2065
2052
|
return result.error.kind === "usage" ? 2 : 1;
|
|
2066
2053
|
}
|
|
2067
2054
|
if (result.value.message !== void 0) process.stdout.write(`${result.value.message}\n`);
|
|
@@ -2094,6 +2081,12 @@ async function main() {
|
|
|
2094
2081
|
function isCacError(error) {
|
|
2095
2082
|
return error instanceof Error && error.name === "CACError";
|
|
2096
2083
|
}
|
|
2084
|
+
function command(value) {
|
|
2085
|
+
return c.cyan(c.bold(value));
|
|
2086
|
+
}
|
|
2087
|
+
function highlightCommands(message) {
|
|
2088
|
+
return message.replace(/\bdevbox (?:init|build|config|up|down|stop|sh|rm)\b/g, command);
|
|
2089
|
+
}
|
|
2097
2090
|
function interactiveFailure(observed, nextAction) {
|
|
2098
2091
|
return failure({
|
|
2099
2092
|
kind: "usage",
|