@ycs77/devbox 0.6.0 → 0.7.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 +9 -4
- package/dist/cli.mjs +451 -554
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -2,16 +2,120 @@ import * as p from "@clack/prompts";
|
|
|
2
2
|
import { intro, outro } from "@clack/prompts";
|
|
3
3
|
import { cac } from "cac";
|
|
4
4
|
import c from "picocolors";
|
|
5
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
6
5
|
import { lstat, mkdir, readFile, rename, rm, rmdir, writeFile } from "node:fs/promises";
|
|
7
6
|
import { homedir } from "node:os";
|
|
8
7
|
import { basename, dirname, isAbsolute, join, parse, relative, sep } from "node:path";
|
|
8
|
+
import { parse as parse$1, parseDocument, stringify } from "yaml";
|
|
9
9
|
import { execFile, spawn } from "node:child_process";
|
|
10
10
|
import { promisify } from "node:util";
|
|
11
|
-
import {
|
|
11
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
12
12
|
import { once } from "node:events";
|
|
13
13
|
//#region package.json
|
|
14
|
-
var version = "0.
|
|
14
|
+
var version = "0.7.1";
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/result.ts
|
|
17
|
+
function success(value) {
|
|
18
|
+
return {
|
|
19
|
+
ok: true,
|
|
20
|
+
value
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function failure(error) {
|
|
24
|
+
return {
|
|
25
|
+
ok: false,
|
|
26
|
+
error
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/host.ts
|
|
31
|
+
const execFile$1 = promisify(execFile);
|
|
32
|
+
var HostCommandError = class extends Error {
|
|
33
|
+
exitCode;
|
|
34
|
+
constructor(file, args, exitCode) {
|
|
35
|
+
super(`${file} ${args.join(" ")} exited with status ${exitCode}.`);
|
|
36
|
+
this.exitCode = exitCode;
|
|
37
|
+
this.name = "HostCommandError";
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const supportedHostAction = "Start Docker Desktop and ensure Docker Compose is available.";
|
|
41
|
+
function currentHostEnvironment() {
|
|
42
|
+
return {
|
|
43
|
+
run: async (file, args) => {
|
|
44
|
+
await execFile$1(file, args);
|
|
45
|
+
},
|
|
46
|
+
runDirect: async (file, args) => new Promise((resolve, reject) => {
|
|
47
|
+
const command = spawn(file, args, { stdio: "inherit" });
|
|
48
|
+
command.once("error", reject);
|
|
49
|
+
command.once("close", (code) => {
|
|
50
|
+
if (code === 0) {
|
|
51
|
+
resolve();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
reject(new HostCommandError(file, args, code ?? 1));
|
|
55
|
+
});
|
|
56
|
+
})
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async function validateSupportedHost(environment = currentHostEnvironment()) {
|
|
60
|
+
for (const [file, args, description] of [[
|
|
61
|
+
"docker",
|
|
62
|
+
["--version"],
|
|
63
|
+
"Docker"
|
|
64
|
+
], [
|
|
65
|
+
"docker",
|
|
66
|
+
["compose", "version"],
|
|
67
|
+
"Docker Compose"
|
|
68
|
+
]]) try {
|
|
69
|
+
await environment.run(file, args);
|
|
70
|
+
} catch {
|
|
71
|
+
return failure({
|
|
72
|
+
kind: "validation",
|
|
73
|
+
code: "missing-host-prerequisite",
|
|
74
|
+
observed: `${description} is unavailable.`,
|
|
75
|
+
nextAction: supportedHostAction
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return success(void 0);
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/agent/volumes.ts
|
|
82
|
+
async function ensureSharedAgentVolumes(composeSource, environment = currentHostEnvironment()) {
|
|
83
|
+
const volumeNames = /* @__PURE__ */ new Set();
|
|
84
|
+
try {
|
|
85
|
+
const definition = parse$1(composeSource);
|
|
86
|
+
if (!isMapping(definition)) throw new Error("Expected a Compose mapping.");
|
|
87
|
+
const volumes = definition.volumes;
|
|
88
|
+
if (volumes !== void 0) {
|
|
89
|
+
if (!isMapping(volumes)) throw new Error("Expected a volume mapping.");
|
|
90
|
+
for (const [key, volume] of Object.entries(volumes)) {
|
|
91
|
+
if (volume === null) continue;
|
|
92
|
+
if (!isMapping(volume)) throw new Error("Expected a volume definition.");
|
|
93
|
+
if (volume.external !== true) continue;
|
|
94
|
+
const name = volume.name ?? key;
|
|
95
|
+
if (typeof name !== "string" || name.length === 0) throw new Error("Expected an external volume name.");
|
|
96
|
+
volumeNames.add(name);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
return failure({
|
|
101
|
+
kind: "operational",
|
|
102
|
+
code: "invalid-project-compose",
|
|
103
|
+
observed: "Devbox could not read external volumes from the retained Sandbox definition.",
|
|
104
|
+
nextAction: "Run devbox config to publish the Sandbox definition, then retry devbox up."
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
for (const volumeName of volumeNames) await environment.run("docker", [
|
|
108
|
+
"volume",
|
|
109
|
+
"create",
|
|
110
|
+
"--driver",
|
|
111
|
+
"local",
|
|
112
|
+
volumeName
|
|
113
|
+
]);
|
|
114
|
+
return success(void 0);
|
|
115
|
+
}
|
|
116
|
+
function isMapping(value) {
|
|
117
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
118
|
+
}
|
|
15
119
|
//#endregion
|
|
16
120
|
//#region src/catalog/index.ts
|
|
17
121
|
const NODE_RELEASE_KEYS = [
|
|
@@ -55,7 +159,7 @@ const PACKAGED_AGENTS = {
|
|
|
55
159
|
},
|
|
56
160
|
supportsNotifications: true,
|
|
57
161
|
notificationInstallationCommands: ["claude plugin marketplace add ycs77/claude-code-notifications", "claude plugin install notification-basic-wsl@ycs77-notifications"],
|
|
58
|
-
|
|
162
|
+
skillInstallationTarget: "claude-code"
|
|
59
163
|
},
|
|
60
164
|
codex: {
|
|
61
165
|
home: {
|
|
@@ -67,7 +171,7 @@ const PACKAGED_AGENTS = {
|
|
|
67
171
|
shell: "sh"
|
|
68
172
|
},
|
|
69
173
|
supportsNotifications: true,
|
|
70
|
-
|
|
174
|
+
skillInstallationTarget: "codex",
|
|
71
175
|
notificationInstallationCommands: ["codex plugin marketplace add ycs77/codex-notifications", "codex plugin add notification-basic-wsl@ycs77-notifications"]
|
|
72
176
|
},
|
|
73
177
|
agy: {
|
|
@@ -79,8 +183,7 @@ const PACKAGED_AGENTS = {
|
|
|
79
183
|
url: "https://antigravity.google/cli/install.sh",
|
|
80
184
|
shell: "bash"
|
|
81
185
|
},
|
|
82
|
-
supportsNotifications: false
|
|
83
|
-
supportsSkillInstallation: false
|
|
186
|
+
supportsNotifications: false
|
|
84
187
|
},
|
|
85
188
|
omp: {
|
|
86
189
|
home: {
|
|
@@ -92,7 +195,7 @@ const PACKAGED_AGENTS = {
|
|
|
92
195
|
shell: "sh"
|
|
93
196
|
},
|
|
94
197
|
supportsNotifications: true,
|
|
95
|
-
|
|
198
|
+
skillInstallationTarget: "codex",
|
|
96
199
|
notificationInstallationCommands: ["omp plugin marketplace add ycs77/omp-notifications", "omp plugin install notification-basic@ycs77-notifications"]
|
|
97
200
|
}
|
|
98
201
|
};
|
|
@@ -101,85 +204,6 @@ const PACKAGED_RUNTIME_CATALOG = {
|
|
|
101
204
|
agents: Object.keys(PACKAGED_AGENTS)
|
|
102
205
|
};
|
|
103
206
|
//#endregion
|
|
104
|
-
//#region src/result.ts
|
|
105
|
-
function success(value) {
|
|
106
|
-
return {
|
|
107
|
-
ok: true,
|
|
108
|
-
value
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
function failure(error) {
|
|
112
|
-
return {
|
|
113
|
-
ok: false,
|
|
114
|
-
error
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
//#endregion
|
|
118
|
-
//#region src/host.ts
|
|
119
|
-
const execFile$1 = promisify(execFile);
|
|
120
|
-
var HostCommandError = class extends Error {
|
|
121
|
-
exitCode;
|
|
122
|
-
constructor(file, args, exitCode) {
|
|
123
|
-
super(`${file} ${args.join(" ")} exited with status ${exitCode}.`);
|
|
124
|
-
this.exitCode = exitCode;
|
|
125
|
-
this.name = "HostCommandError";
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
const supportedHostAction = "Start Docker Desktop and ensure Docker Compose is available.";
|
|
129
|
-
function currentHostEnvironment() {
|
|
130
|
-
return {
|
|
131
|
-
run: async (file, args) => {
|
|
132
|
-
await execFile$1(file, args);
|
|
133
|
-
},
|
|
134
|
-
runDirect: async (file, args) => new Promise((resolve, reject) => {
|
|
135
|
-
const command = spawn(file, args, { stdio: "inherit" });
|
|
136
|
-
command.once("error", reject);
|
|
137
|
-
command.once("close", (code) => {
|
|
138
|
-
if (code === 0) {
|
|
139
|
-
resolve();
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
reject(new HostCommandError(file, args, code ?? 1));
|
|
143
|
-
});
|
|
144
|
-
})
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
async function validateSupportedHost(environment = currentHostEnvironment()) {
|
|
148
|
-
for (const [file, args, description] of [[
|
|
149
|
-
"docker",
|
|
150
|
-
["--version"],
|
|
151
|
-
"Docker"
|
|
152
|
-
], [
|
|
153
|
-
"docker",
|
|
154
|
-
["compose", "version"],
|
|
155
|
-
"Docker Compose"
|
|
156
|
-
]]) try {
|
|
157
|
-
await environment.run(file, args);
|
|
158
|
-
} catch {
|
|
159
|
-
return failure({
|
|
160
|
-
kind: "validation",
|
|
161
|
-
code: "missing-host-prerequisite",
|
|
162
|
-
observed: `${description} is unavailable.`,
|
|
163
|
-
nextAction: supportedHostAction
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
return success(void 0);
|
|
167
|
-
}
|
|
168
|
-
//#endregion
|
|
169
|
-
//#region src/agent/volumes.ts
|
|
170
|
-
async function ensureSharedAgentVolumes(configuredAgents, environment = currentHostEnvironment()) {
|
|
171
|
-
for (const configuredAgent of configuredAgents) {
|
|
172
|
-
const volumeName = PACKAGED_AGENTS[configuredAgent]?.home.volumeName;
|
|
173
|
-
if (volumeName !== void 0) await environment.run("docker", [
|
|
174
|
-
"volume",
|
|
175
|
-
"create",
|
|
176
|
-
"--driver",
|
|
177
|
-
"local",
|
|
178
|
-
volumeName
|
|
179
|
-
]);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
//#endregion
|
|
183
207
|
//#region src/configuration/index.ts
|
|
184
208
|
function normalizeCatalog(catalog = PACKAGED_RUNTIME_CATALOG) {
|
|
185
209
|
if (!isRecord(catalog.runtimes) || !Array.isArray(catalog.agents)) return invalid$1("invalid-runtime-catalog", "The packaged Runtime catalog is invalid.");
|
|
@@ -405,7 +429,7 @@ async function withStateLocks(input, operation) {
|
|
|
405
429
|
try {
|
|
406
430
|
await lstat(input.devboxHome);
|
|
407
431
|
} catch (error) {
|
|
408
|
-
if (isMissingFileError$
|
|
432
|
+
if (isMissingFileError$2(error)) devboxHomeExisted = false;
|
|
409
433
|
else throw error;
|
|
410
434
|
}
|
|
411
435
|
const acquired = [];
|
|
@@ -468,7 +492,7 @@ async function acquireLock(target) {
|
|
|
468
492
|
function isFileExistsError(error) {
|
|
469
493
|
return typeof error === "object" && error !== null && error.code === "EEXIST";
|
|
470
494
|
}
|
|
471
|
-
function isMissingFileError$
|
|
495
|
+
function isMissingFileError$2(error) {
|
|
472
496
|
return typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
473
497
|
}
|
|
474
498
|
//#endregion
|
|
@@ -514,6 +538,7 @@ const WORKSPACE_IMAGE = "devbox-workspace:latest";
|
|
|
514
538
|
//#endregion
|
|
515
539
|
//#region src/project/compose.ts
|
|
516
540
|
function renderProjectCompose(input) {
|
|
541
|
+
const containerName = `devbox-${input.sandboxName}`;
|
|
517
542
|
const node = selectNodeComposeFragment(input.selectedNode);
|
|
518
543
|
const agent = selectAgentComposeFragments(input.configuredAgents, input.agentNotifications);
|
|
519
544
|
const environment = {
|
|
@@ -544,17 +569,17 @@ function renderProjectCompose(input) {
|
|
|
544
569
|
}]
|
|
545
570
|
];
|
|
546
571
|
const definition = {
|
|
547
|
-
name:
|
|
572
|
+
name: containerName,
|
|
548
573
|
"x-devbox": {
|
|
549
574
|
version: 1,
|
|
550
575
|
project_root: input.projectRoot,
|
|
551
|
-
|
|
576
|
+
container_name: containerName,
|
|
552
577
|
sandbox_service: "devbox"
|
|
553
578
|
},
|
|
554
579
|
services: { devbox: {
|
|
555
580
|
image: WORKSPACE_IMAGE,
|
|
556
581
|
hostname: "devbox",
|
|
557
|
-
container_name:
|
|
582
|
+
container_name: containerName,
|
|
558
583
|
working_dir: `/workspace/${input.sandboxName}`,
|
|
559
584
|
...input.ports.length === 0 ? {} : { ports: input.ports.map((port) => renderComposePortMapping(port)) },
|
|
560
585
|
...Object.keys(environment).length === 0 ? {} : { environment },
|
|
@@ -566,21 +591,21 @@ function renderProjectCompose(input) {
|
|
|
566
591
|
}])) }
|
|
567
592
|
};
|
|
568
593
|
const source = stringify(definition);
|
|
569
|
-
return validatesProjectCompose(source, input) ? success(source) : failure({
|
|
594
|
+
return validatesProjectCompose(source, input, containerName) ? success(source) : failure({
|
|
570
595
|
kind: "operational",
|
|
571
596
|
code: "invalid-project-compose",
|
|
572
597
|
observed: `Devbox generated an invalid Compose definition for Project ${input.projectRoot}.`,
|
|
573
598
|
nextAction: "Run the command again. If the problem continues, report it as a Devbox bug."
|
|
574
599
|
});
|
|
575
600
|
}
|
|
576
|
-
function validatesProjectCompose(source, input) {
|
|
601
|
+
function validatesProjectCompose(source, input, containerName) {
|
|
577
602
|
try {
|
|
578
603
|
const definition = parse$1(source);
|
|
579
|
-
if (definition.name !==
|
|
604
|
+
if (definition.name !== containerName) return false;
|
|
580
605
|
const metadata = definition["x-devbox"];
|
|
581
|
-
if (metadata.version !== 1 || metadata.project_root !== input.projectRoot || metadata.
|
|
606
|
+
if (metadata.version !== 1 || metadata.project_root !== input.projectRoot || metadata.container_name !== containerName || metadata.sandbox_service !== "devbox") return false;
|
|
582
607
|
const service = definition.services.devbox;
|
|
583
|
-
if (service.image !== "devbox-workspace:latest" || service.container_name !==
|
|
608
|
+
if (service.image !== "devbox-workspace:latest" || service.container_name !== containerName || service.working_dir !== `/workspace/${input.sandboxName}` || "build" in service) return false;
|
|
584
609
|
const ports = input.ports.map((port) => renderComposePortMapping(port));
|
|
585
610
|
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);
|
|
586
611
|
} catch {
|
|
@@ -658,6 +683,200 @@ function invalid(observed) {
|
|
|
658
683
|
});
|
|
659
684
|
}
|
|
660
685
|
//#endregion
|
|
686
|
+
//#region src/project/publication.ts
|
|
687
|
+
async function publishProjectState(input) {
|
|
688
|
+
const definitions = [];
|
|
689
|
+
for (const project of input.projects) {
|
|
690
|
+
const prepared = project.preparedDefinition === void 0 ? await prepareSandboxDefinition(input.devboxHome, input.globalConfiguration, project) : success(project.preparedDefinition);
|
|
691
|
+
if (!prepared.ok) return prepared;
|
|
692
|
+
definitions.push(prepared.value);
|
|
693
|
+
}
|
|
694
|
+
const claudeHostConfiguration = await ensureClaudeHostConfiguration(input.devboxHome, input.globalConfiguration);
|
|
695
|
+
if (!claudeHostConfiguration.ok) return claudeHostConfiguration;
|
|
696
|
+
const writes = [
|
|
697
|
+
...input.globalPrevious === void 0 ? [] : [{
|
|
698
|
+
path: join(input.devboxHome, "config.yaml"),
|
|
699
|
+
content: serializeGlobalConfiguration(input.globalConfiguration),
|
|
700
|
+
previous: input.globalPrevious
|
|
701
|
+
}],
|
|
702
|
+
...input.projects.flatMap((project) => project.localPrevious === void 0 ? [] : [{
|
|
703
|
+
path: join(project.stateDirectory, "config.yaml"),
|
|
704
|
+
content: serializeLocalConfiguration(project.localConfiguration),
|
|
705
|
+
previous: project.localPrevious
|
|
706
|
+
}]),
|
|
707
|
+
...input.registry === void 0 ? [] : [{
|
|
708
|
+
path: join(input.devboxHome, "projects.yaml"),
|
|
709
|
+
content: serializeProjectRegistry(input.registry.value),
|
|
710
|
+
previous: input.registry.previous
|
|
711
|
+
}],
|
|
712
|
+
...definitions
|
|
713
|
+
].filter((write) => !write.previous.exists || write.content !== write.previous.content);
|
|
714
|
+
const written = await writeStateTransaction(writes);
|
|
715
|
+
return written.ok ? success(writes.length > 0) : written;
|
|
716
|
+
}
|
|
717
|
+
async function prepareSandboxDefinition(devboxHome, globalConfiguration, project) {
|
|
718
|
+
const claudeHostConfiguration = globalConfiguration.agent.includes("claude-code") ? join(devboxHome, "agents", "claude", ".claude.json") : void 0;
|
|
719
|
+
const rendered = renderProjectCompose({
|
|
720
|
+
projectRoot: project.root,
|
|
721
|
+
sandboxName: project.name,
|
|
722
|
+
selectedNode: project.localConfiguration.node,
|
|
723
|
+
configuredAgents: globalConfiguration.agent,
|
|
724
|
+
agentNotifications: globalConfiguration.agent_notifications,
|
|
725
|
+
claudeHostConfiguration,
|
|
726
|
+
ports: project.localConfiguration.ports
|
|
727
|
+
});
|
|
728
|
+
if (!rendered.ok) return rendered;
|
|
729
|
+
const path = join(project.stateDirectory, "compose.yaml");
|
|
730
|
+
const previous = await readOptionalFile(path);
|
|
731
|
+
if (!previous.ok) return previous;
|
|
732
|
+
return success({
|
|
733
|
+
path,
|
|
734
|
+
content: rendered.value,
|
|
735
|
+
previous: previous.value
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
async function ensureClaudeHostConfiguration(devboxHome, globalConfiguration) {
|
|
739
|
+
if (!globalConfiguration.agent.includes("claude-code")) return success(void 0);
|
|
740
|
+
const path = join(devboxHome, "agents", "claude", ".claude.json");
|
|
741
|
+
try {
|
|
742
|
+
if ((await lstat(path)).isFile()) return success(path);
|
|
743
|
+
return failure({
|
|
744
|
+
kind: "operational",
|
|
745
|
+
code: "claude-host-configuration-invalid",
|
|
746
|
+
observed: `Claude host configuration is not a regular file: ${path}.`,
|
|
747
|
+
nextAction: "Replace it with a regular file, then run the command again."
|
|
748
|
+
});
|
|
749
|
+
} catch (error) {
|
|
750
|
+
if (!isMissingFileError$1(error)) return failure({
|
|
751
|
+
kind: "operational",
|
|
752
|
+
code: "claude-host-configuration-unreadable",
|
|
753
|
+
observed: `Devbox could not inspect Claude host configuration: ${path}.`,
|
|
754
|
+
nextAction: "Check access to ~/.devbox and run the command again."
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
try {
|
|
758
|
+
await mkdir(dirname(path), {
|
|
759
|
+
recursive: true,
|
|
760
|
+
mode: 448
|
|
761
|
+
});
|
|
762
|
+
await writeFile(path, "{}\n", {
|
|
763
|
+
encoding: "utf8",
|
|
764
|
+
flag: "wx",
|
|
765
|
+
mode: 384
|
|
766
|
+
});
|
|
767
|
+
return success(path);
|
|
768
|
+
} catch (error) {
|
|
769
|
+
if (error.code === "EEXIST") return ensureClaudeHostConfiguration(devboxHome, globalConfiguration);
|
|
770
|
+
return failure({
|
|
771
|
+
kind: "operational",
|
|
772
|
+
code: "claude-host-configuration-write-failed",
|
|
773
|
+
observed: `Devbox could not create Claude host configuration: ${path}.`,
|
|
774
|
+
nextAction: "Check write access to ~/.devbox and run the command again."
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
async function readOptionalFile(path) {
|
|
779
|
+
try {
|
|
780
|
+
return success({
|
|
781
|
+
exists: true,
|
|
782
|
+
content: await readFile(path, "utf8")
|
|
783
|
+
});
|
|
784
|
+
} catch (error) {
|
|
785
|
+
if (isMissingFileError$1(error)) return success({
|
|
786
|
+
exists: false,
|
|
787
|
+
content: ""
|
|
788
|
+
});
|
|
789
|
+
return failure({
|
|
790
|
+
kind: "operational",
|
|
791
|
+
code: "state-read-failed",
|
|
792
|
+
observed: `Devbox could not read state file: ${path}.`,
|
|
793
|
+
nextAction: "Check write access to ~/.devbox and try again."
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
async function writeAtomically(path, content) {
|
|
798
|
+
const staged = await stageStateWrite(path, content, "tmp");
|
|
799
|
+
if (!staged.ok) return staged;
|
|
800
|
+
try {
|
|
801
|
+
await rename(staged.value, path);
|
|
802
|
+
return success(void 0);
|
|
803
|
+
} catch {
|
|
804
|
+
await rm(staged.value, { force: true }).catch(() => void 0);
|
|
805
|
+
return failure({
|
|
806
|
+
kind: "operational",
|
|
807
|
+
code: "state-write-failed",
|
|
808
|
+
observed: `Devbox could not atomically write state file: ${path}.`,
|
|
809
|
+
nextAction: "Check write access to ~/.devbox and run the command again."
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
async function stageStateWrite(path, content, suffix) {
|
|
814
|
+
const directory = dirname(path);
|
|
815
|
+
const temporaryPath = join(directory, `.${basename(path)}-${process.pid}-${randomUUID()}.${suffix}`);
|
|
816
|
+
try {
|
|
817
|
+
await mkdir(directory, {
|
|
818
|
+
recursive: true,
|
|
819
|
+
mode: 448
|
|
820
|
+
});
|
|
821
|
+
await writeFile(temporaryPath, content, {
|
|
822
|
+
encoding: "utf8",
|
|
823
|
+
flag: "wx",
|
|
824
|
+
mode: 384
|
|
825
|
+
});
|
|
826
|
+
return success(temporaryPath);
|
|
827
|
+
} catch {
|
|
828
|
+
await rm(temporaryPath, { force: true }).catch(() => void 0);
|
|
829
|
+
return failure({
|
|
830
|
+
kind: "operational",
|
|
831
|
+
code: "state-write-failed",
|
|
832
|
+
observed: `Devbox could not atomically write state file: ${path}.`,
|
|
833
|
+
nextAction: "Check write access to ~/.devbox and run the command again."
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
async function writeStateTransaction(writes) {
|
|
838
|
+
const staged = [];
|
|
839
|
+
for (const write of writes) {
|
|
840
|
+
const stagedWrite = await stageStateWrite(write.path, write.content, "transaction.tmp");
|
|
841
|
+
if (!stagedWrite.ok) {
|
|
842
|
+
await Promise.all(staged.map((stage) => rm(stage.path, { force: true }).catch(() => void 0)));
|
|
843
|
+
return stagedWrite;
|
|
844
|
+
}
|
|
845
|
+
staged.push({
|
|
846
|
+
write,
|
|
847
|
+
path: stagedWrite.value
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
const published = [];
|
|
851
|
+
try {
|
|
852
|
+
for (const stage of staged) {
|
|
853
|
+
await rename(stage.path, stage.write.path);
|
|
854
|
+
published.push(stage.write);
|
|
855
|
+
}
|
|
856
|
+
return success(void 0);
|
|
857
|
+
} catch {
|
|
858
|
+
let restored = true;
|
|
859
|
+
for (const write of published.reverse()) if (write.previous.exists) {
|
|
860
|
+
const restoredWrite = await writeAtomically(write.path, write.previous.content);
|
|
861
|
+
restored &&= restoredWrite.ok;
|
|
862
|
+
} else try {
|
|
863
|
+
await rm(write.path, { force: true });
|
|
864
|
+
} catch {
|
|
865
|
+
restored = false;
|
|
866
|
+
}
|
|
867
|
+
await Promise.all(staged.map((stage) => rm(stage.path, { force: true }).catch(() => void 0)));
|
|
868
|
+
return failure({
|
|
869
|
+
kind: "operational",
|
|
870
|
+
code: "state-write-failed",
|
|
871
|
+
observed: restored ? "Devbox could not publish Project state; the previous state was restored." : "Devbox could not publish Project state or restore the previous state.",
|
|
872
|
+
nextAction: "Check write access to ~/.devbox and run the command again."
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
function isMissingFileError$1(error) {
|
|
877
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
878
|
+
}
|
|
879
|
+
//#endregion
|
|
661
880
|
//#region src/project/index.ts
|
|
662
881
|
function devboxPaths(devboxHome = join(homedir(), ".devbox")) {
|
|
663
882
|
return {
|
|
@@ -719,24 +938,22 @@ async function initializeProjectUnlocked(input = {}) {
|
|
|
719
938
|
if (!registryCheck.ok) return registryCheck;
|
|
720
939
|
const registry = registryCheck.value;
|
|
721
940
|
const registration = registry.projects[projectRoot];
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
941
|
+
if (registration !== void 0) return success({
|
|
942
|
+
root: projectRoot,
|
|
943
|
+
stateDirectory: projectStateDirectory(registration.identity, paths.home),
|
|
944
|
+
sandboxName: registration.name,
|
|
945
|
+
created: false,
|
|
946
|
+
globalConfigurationCreated: false
|
|
947
|
+
});
|
|
948
|
+
const allocatedIdentity = await allocateSandboxIdentity(projectRoot, paths, registry);
|
|
949
|
+
if (!allocatedIdentity.ok) return allocatedIdentity;
|
|
950
|
+
const allocatedName = allocateSandboxName(projectRoot, registry);
|
|
951
|
+
if (!allocatedName.ok) return allocatedName;
|
|
952
|
+
const identity = allocatedIdentity.value;
|
|
953
|
+
const name = allocatedName.value;
|
|
735
954
|
const stateDirectory = projectStateDirectory(identity, paths.home);
|
|
736
|
-
const registered = registration !== void 0;
|
|
737
955
|
const globalState = await readOptionalFile(paths.globalConfiguration);
|
|
738
956
|
if (!globalState.ok) return globalState;
|
|
739
|
-
if (registered && !globalState.value.exists) return missingConfiguration("global", paths.globalConfiguration);
|
|
740
957
|
let globalConfiguration;
|
|
741
958
|
let globalChanged = false;
|
|
742
959
|
if (globalState.value.exists) {
|
|
@@ -757,24 +974,11 @@ async function initializeProjectUnlocked(input = {}) {
|
|
|
757
974
|
}
|
|
758
975
|
globalChanged = true;
|
|
759
976
|
}
|
|
760
|
-
const localPath = join(stateDirectory, "config.yaml");
|
|
761
|
-
const localState = registered ? await readOptionalFile(localPath) : success({
|
|
762
|
-
exists: false,
|
|
763
|
-
content: ""
|
|
764
|
-
});
|
|
765
|
-
if (!localState.ok) return localState;
|
|
766
|
-
if (registered && !localState.value.exists) return missingConfiguration("local", localPath);
|
|
767
977
|
let localConfiguration;
|
|
768
|
-
|
|
769
|
-
if (localState.value.exists) {
|
|
770
|
-
const parsedLocal = parseLocalConfiguration(localState.value.content, globalConfiguration, catalog);
|
|
771
|
-
if (!parsedLocal.ok) return parsedLocal;
|
|
772
|
-
localConfiguration = parsedLocal.value;
|
|
773
|
-
} else if (input.initialLocalConfiguration !== void 0) {
|
|
978
|
+
if (input.initialLocalConfiguration !== void 0) {
|
|
774
979
|
const initialLocal = validateLocalObject(input.initialLocalConfiguration, globalConfiguration, catalog);
|
|
775
980
|
if (!initialLocal.ok) return initialLocal;
|
|
776
981
|
localConfiguration = initialLocal.value;
|
|
777
|
-
localChanged = true;
|
|
778
982
|
} else {
|
|
779
983
|
localConfiguration = defaultLocalConfiguration(globalConfiguration);
|
|
780
984
|
if (input.prompt?.editLocal) {
|
|
@@ -782,35 +986,20 @@ async function initializeProjectUnlocked(input = {}) {
|
|
|
782
986
|
if (!checkedLocal.ok) return checkedLocal;
|
|
783
987
|
localConfiguration = checkedLocal.value;
|
|
784
988
|
}
|
|
785
|
-
localChanged = true;
|
|
786
989
|
}
|
|
787
|
-
const
|
|
788
|
-
projectRoot,
|
|
789
|
-
|
|
990
|
+
const definition = await prepareSandboxDefinition(paths.home, globalConfiguration, {
|
|
991
|
+
root: projectRoot,
|
|
992
|
+
name,
|
|
790
993
|
stateDirectory,
|
|
791
|
-
devboxHome: paths.home,
|
|
792
|
-
globalConfiguration,
|
|
793
994
|
localConfiguration
|
|
794
995
|
});
|
|
795
|
-
if (!
|
|
796
|
-
if (registered) {
|
|
797
|
-
if (!composeWrite.value.previous.exists || composeWrite.value.content !== composeWrite.value.previous.content) {
|
|
798
|
-
const written = await writeStateTransaction([composeWrite.value]);
|
|
799
|
-
if (!written.ok) return written;
|
|
800
|
-
}
|
|
801
|
-
return success({
|
|
802
|
-
root: projectRoot,
|
|
803
|
-
stateDirectory,
|
|
804
|
-
sandboxName: name,
|
|
805
|
-
created: false,
|
|
806
|
-
confirmed: true
|
|
807
|
-
});
|
|
808
|
-
}
|
|
996
|
+
if (!definition.ok) return definition;
|
|
809
997
|
if (!await (input.confirm ?? input.prompt?.confirm ?? (async () => true))("Save configuration?")) return success({
|
|
810
998
|
root: projectRoot,
|
|
811
999
|
stateDirectory,
|
|
812
1000
|
sandboxName: name,
|
|
813
1001
|
created: false,
|
|
1002
|
+
globalConfigurationCreated: false,
|
|
814
1003
|
confirmed: false
|
|
815
1004
|
});
|
|
816
1005
|
const nextRegistry = {
|
|
@@ -823,30 +1012,33 @@ async function initializeProjectUnlocked(input = {}) {
|
|
|
823
1012
|
}
|
|
824
1013
|
}
|
|
825
1014
|
};
|
|
826
|
-
const written = await
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
...localChanged ? [{
|
|
833
|
-
path: localPath,
|
|
834
|
-
content: serializeLocalConfiguration(localConfiguration),
|
|
835
|
-
previous: localState.value
|
|
836
|
-
}] : [],
|
|
837
|
-
{
|
|
838
|
-
path: paths.projectRegistry,
|
|
839
|
-
content: serializeProjectRegistry(nextRegistry),
|
|
1015
|
+
const written = await publishProjectState({
|
|
1016
|
+
devboxHome: paths.home,
|
|
1017
|
+
globalConfiguration,
|
|
1018
|
+
globalPrevious: globalChanged ? globalState.value : void 0,
|
|
1019
|
+
registry: {
|
|
1020
|
+
value: nextRegistry,
|
|
840
1021
|
previous: registryState.value
|
|
841
1022
|
},
|
|
842
|
-
|
|
843
|
-
|
|
1023
|
+
projects: [{
|
|
1024
|
+
root: projectRoot,
|
|
1025
|
+
name,
|
|
1026
|
+
stateDirectory,
|
|
1027
|
+
localConfiguration,
|
|
1028
|
+
localPrevious: {
|
|
1029
|
+
exists: false,
|
|
1030
|
+
content: ""
|
|
1031
|
+
},
|
|
1032
|
+
preparedDefinition: definition.value
|
|
1033
|
+
}]
|
|
1034
|
+
});
|
|
844
1035
|
if (!written.ok) return written;
|
|
845
1036
|
return success({
|
|
846
1037
|
root: projectRoot,
|
|
847
1038
|
stateDirectory,
|
|
848
1039
|
sandboxName: name,
|
|
849
1040
|
created: true,
|
|
1041
|
+
globalConfigurationCreated: globalChanged,
|
|
850
1042
|
confirmed: true
|
|
851
1043
|
});
|
|
852
1044
|
}
|
|
@@ -882,14 +1074,10 @@ async function configureLocalProjectUnlocked(input = {}) {
|
|
|
882
1074
|
const localCheck = parseLocalConfiguration(localState.value.content, globalCheck.value, catalog);
|
|
883
1075
|
if (!localCheck.ok) return localCheck;
|
|
884
1076
|
let nextConfiguration = input.nextConfiguration ?? localCheck.value;
|
|
885
|
-
if (input.nextConfiguration === void 0 && input.prompt?.editLocal) nextConfiguration = await input.prompt.editLocal(localCheck.value, catalog, globalCheck.value);
|
|
886
|
-
const nextCheck = validateLocalObject(nextConfiguration, globalCheck.value, catalog);
|
|
887
|
-
if (!nextCheck.ok) return nextCheck;
|
|
888
|
-
|
|
889
|
-
scope: "local",
|
|
890
|
-
root: projectRoot,
|
|
891
|
-
changed: false
|
|
892
|
-
});
|
|
1077
|
+
if (input.nextConfiguration === void 0 && input.prompt?.editLocal) nextConfiguration = await input.prompt.editLocal(localCheck.value, catalog, globalCheck.value);
|
|
1078
|
+
const nextCheck = validateLocalObject(nextConfiguration, globalCheck.value, catalog);
|
|
1079
|
+
if (!nextCheck.ok) return nextCheck;
|
|
1080
|
+
const localChanged = !configurationsEqual(localCheck.value, nextCheck.value);
|
|
893
1081
|
if (!await (input.confirm ?? input.prompt?.confirm ?? (async () => true))("Save Project configuration?", {
|
|
894
1082
|
title: "Review configuration",
|
|
895
1083
|
sections: [{
|
|
@@ -903,25 +1091,22 @@ async function configureLocalProjectUnlocked(input = {}) {
|
|
|
903
1091
|
changed: false
|
|
904
1092
|
});
|
|
905
1093
|
if (input.signal?.aborted) throw new InterruptedError();
|
|
906
|
-
const
|
|
907
|
-
projectRoot,
|
|
908
|
-
sandboxName: registration.name,
|
|
909
|
-
stateDirectory,
|
|
1094
|
+
const written = await publishProjectState({
|
|
910
1095
|
devboxHome: paths.home,
|
|
911
1096
|
globalConfiguration: globalCheck.value,
|
|
912
|
-
|
|
1097
|
+
projects: [{
|
|
1098
|
+
root: projectRoot,
|
|
1099
|
+
name: registration.name,
|
|
1100
|
+
stateDirectory,
|
|
1101
|
+
localConfiguration: nextCheck.value,
|
|
1102
|
+
localPrevious: localChanged ? localState.value : void 0
|
|
1103
|
+
}]
|
|
913
1104
|
});
|
|
914
|
-
if (!composeWrite.ok) return composeWrite;
|
|
915
|
-
const written = await writeStateTransaction([{
|
|
916
|
-
path: localPath,
|
|
917
|
-
content: serializeLocalConfiguration(nextCheck.value),
|
|
918
|
-
previous: localState.value
|
|
919
|
-
}, composeWrite.value]);
|
|
920
1105
|
if (!written.ok) return written;
|
|
921
1106
|
return success({
|
|
922
1107
|
scope: "local",
|
|
923
1108
|
root: projectRoot,
|
|
924
|
-
changed:
|
|
1109
|
+
changed: written.value
|
|
925
1110
|
});
|
|
926
1111
|
}
|
|
927
1112
|
async function configureGlobal(input = {}) {
|
|
@@ -986,10 +1171,7 @@ async function configureGlobalUnlocked(input = {}, locks) {
|
|
|
986
1171
|
if (!replacementCheck.ok) return replacementCheck;
|
|
987
1172
|
replacementConfigurations.set(root, replacementCheck.value);
|
|
988
1173
|
}
|
|
989
|
-
|
|
990
|
-
scope: "global",
|
|
991
|
-
changed: false
|
|
992
|
-
});
|
|
1174
|
+
const globalChanged = !globalState.value.exists || !configurationsEqual(currentConfiguration, nextCheck.value);
|
|
993
1175
|
if (!await (input.confirm ?? input.prompt?.confirm ?? (async () => true))("Save Global configuration?", {
|
|
994
1176
|
title: "Review configuration",
|
|
995
1177
|
sections: [{
|
|
@@ -1006,39 +1188,25 @@ async function configureGlobalUnlocked(input = {}, locks) {
|
|
|
1006
1188
|
changed: false
|
|
1007
1189
|
});
|
|
1008
1190
|
if (input.signal?.aborted) throw new InterruptedError();
|
|
1009
|
-
const
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1191
|
+
const written = await publishProjectState({
|
|
1192
|
+
devboxHome: paths.home,
|
|
1193
|
+
globalConfiguration: nextCheck.value,
|
|
1194
|
+
globalPrevious: globalChanged ? globalState.value : void 0,
|
|
1195
|
+
projects: Object.entries(registryCheck.value.projects).map(([root, registration]) => ({
|
|
1196
|
+
root,
|
|
1197
|
+
name: registration.name,
|
|
1014
1198
|
stateDirectory: projectStateDirectory(registration.identity, paths.home),
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
localConfiguration: replacementConfigurations.get(root) ?? localConfigurations.get(root)
|
|
1018
|
-
});
|
|
1019
|
-
if (!composeWrite.ok) return composeWrite;
|
|
1020
|
-
if (!composeWrite.value.previous.exists || composeWrite.value.content !== composeWrite.value.previous.content) composeWrites.push(composeWrite.value);
|
|
1021
|
-
}
|
|
1022
|
-
const written = await writeStateTransaction([
|
|
1023
|
-
{
|
|
1024
|
-
path: paths.globalConfiguration,
|
|
1025
|
-
content: serializeGlobalConfiguration(nextCheck.value),
|
|
1026
|
-
previous: globalState.value
|
|
1027
|
-
},
|
|
1028
|
-
...[...replacementConfigurations].map(([root, configuration]) => ({
|
|
1029
|
-
path: join(paths.projects, registryCheck.value.projects[root].identity, "config.yaml"),
|
|
1030
|
-
content: serializeLocalConfiguration(configuration),
|
|
1031
|
-
previous: {
|
|
1199
|
+
localConfiguration: replacementConfigurations.get(root) ?? localConfigurations.get(root),
|
|
1200
|
+
localPrevious: replacementConfigurations.has(root) ? {
|
|
1032
1201
|
exists: true,
|
|
1033
1202
|
content: localConfigurationSources.get(root)
|
|
1034
|
-
}
|
|
1035
|
-
}))
|
|
1036
|
-
|
|
1037
|
-
]);
|
|
1203
|
+
} : void 0
|
|
1204
|
+
}))
|
|
1205
|
+
});
|
|
1038
1206
|
if (!written.ok) return written;
|
|
1039
1207
|
return success({
|
|
1040
1208
|
scope: "global",
|
|
1041
|
-
changed:
|
|
1209
|
+
changed: written.value
|
|
1042
1210
|
});
|
|
1043
1211
|
}
|
|
1044
1212
|
async function removeProject(input = {}) {
|
|
@@ -1060,13 +1228,20 @@ async function removeProjectUnlocked(input = {}) {
|
|
|
1060
1228
|
const registration = registryCheck.value.projects[projectRoot];
|
|
1061
1229
|
if (registration === void 0) return notRegistered(projectRoot);
|
|
1062
1230
|
if (!input.yes) {
|
|
1063
|
-
if (!await (input.confirm ?? (async () => false))(`Remove Project ${projectRoot} and
|
|
1231
|
+
if (!await (input.confirm ?? (async () => false))(`Remove Project ${projectRoot}, its Sandbox, and Devbox state?`)) return success({
|
|
1064
1232
|
root: projectRoot,
|
|
1065
1233
|
removed: false
|
|
1066
1234
|
});
|
|
1067
1235
|
}
|
|
1068
1236
|
if (input.signal?.aborted) throw new InterruptedError();
|
|
1069
1237
|
const stateDirectory = projectStateDirectory(registration.identity, paths.home);
|
|
1238
|
+
const environment = input.environment ?? currentHostEnvironment();
|
|
1239
|
+
await (environment.runDirect ?? environment.run)("docker", [
|
|
1240
|
+
"compose",
|
|
1241
|
+
"--file",
|
|
1242
|
+
join(stateDirectory, "compose.yaml"),
|
|
1243
|
+
"down"
|
|
1244
|
+
]);
|
|
1070
1245
|
try {
|
|
1071
1246
|
await rm(stateDirectory, {
|
|
1072
1247
|
recursive: true,
|
|
@@ -1092,107 +1267,6 @@ async function removeProjectUnlocked(input = {}) {
|
|
|
1092
1267
|
removed: true
|
|
1093
1268
|
});
|
|
1094
1269
|
}
|
|
1095
|
-
async function cleanupMissingProjects(input = {}) {
|
|
1096
|
-
return withStateLocks({
|
|
1097
|
-
devboxHome: input.devboxHome ?? join(homedir(), ".devbox"),
|
|
1098
|
-
global: true,
|
|
1099
|
-
projectRoots: [],
|
|
1100
|
-
signal: input.signal
|
|
1101
|
-
}, async (locks) => {
|
|
1102
|
-
const discovery = await discoverMissingProjects(input);
|
|
1103
|
-
if (!discovery.ok) return discovery;
|
|
1104
|
-
await locks.acquireProjectScopes(discovery.value.missingRoots);
|
|
1105
|
-
return cleanupMissingProjectsUnlocked(input, discovery.value);
|
|
1106
|
-
});
|
|
1107
|
-
}
|
|
1108
|
-
async function discoverMissingProjects(input) {
|
|
1109
|
-
if (input.signal?.aborted) throw new InterruptedError();
|
|
1110
|
-
const paths = devboxPaths(input.devboxHome);
|
|
1111
|
-
const registryState = await readOptionalFile(paths.projectRegistry);
|
|
1112
|
-
if (!registryState.ok) return registryState;
|
|
1113
|
-
if (!registryState.value.exists) return success({
|
|
1114
|
-
paths,
|
|
1115
|
-
registry: {
|
|
1116
|
-
version: 1,
|
|
1117
|
-
projects: {}
|
|
1118
|
-
},
|
|
1119
|
-
missingRoots: []
|
|
1120
|
-
});
|
|
1121
|
-
const registryCheck = parseProjectRegistry(registryState.value.content);
|
|
1122
|
-
if (!registryCheck.ok) return registryCheck;
|
|
1123
|
-
const missingRoots = [];
|
|
1124
|
-
for (const root of Object.keys(registryCheck.value.projects)) {
|
|
1125
|
-
if (input.signal?.aborted) throw new InterruptedError();
|
|
1126
|
-
const exists = await projectRootExists(root);
|
|
1127
|
-
if (exists === void 0) return failure({
|
|
1128
|
-
kind: "operational",
|
|
1129
|
-
code: "project-root-observation-failed",
|
|
1130
|
-
observed: `Devbox could not inspect Project root: ${root}.`,
|
|
1131
|
-
nextAction: "Check access to the registered Project root and try cleanup again."
|
|
1132
|
-
});
|
|
1133
|
-
if (!exists) missingRoots.push(root);
|
|
1134
|
-
}
|
|
1135
|
-
return success({
|
|
1136
|
-
paths,
|
|
1137
|
-
registry: registryCheck.value,
|
|
1138
|
-
missingRoots
|
|
1139
|
-
});
|
|
1140
|
-
}
|
|
1141
|
-
async function cleanupMissingProjectsUnlocked(input, discovery) {
|
|
1142
|
-
if (input.signal?.aborted) throw new InterruptedError();
|
|
1143
|
-
if (discovery.missingRoots.length === 0) return success({
|
|
1144
|
-
roots: [],
|
|
1145
|
-
removed: false
|
|
1146
|
-
});
|
|
1147
|
-
if (!input.yes) {
|
|
1148
|
-
if (!await (input.confirm ?? (async () => false))(`Remove Missing-root Project registrations?\n${discovery.missingRoots.map((root) => `- ${root}`).join("\n")}`)) return success({
|
|
1149
|
-
roots: [],
|
|
1150
|
-
removed: false
|
|
1151
|
-
});
|
|
1152
|
-
}
|
|
1153
|
-
const removedRoots = [];
|
|
1154
|
-
const projects = { ...discovery.registry.projects };
|
|
1155
|
-
for (const root of discovery.missingRoots) {
|
|
1156
|
-
const rootExists = await projectRootExists(root);
|
|
1157
|
-
if (rootExists === void 0) return failure({
|
|
1158
|
-
kind: "operational",
|
|
1159
|
-
code: "project-root-observation-failed",
|
|
1160
|
-
observed: `Devbox could not recheck Project root: ${root}.`,
|
|
1161
|
-
nextAction: "Check access to the registered Project root and try cleanup again."
|
|
1162
|
-
});
|
|
1163
|
-
if (rootExists) continue;
|
|
1164
|
-
const registration = projects[root];
|
|
1165
|
-
if (registration === void 0) continue;
|
|
1166
|
-
try {
|
|
1167
|
-
await rm(projectStateDirectory(registration.identity, discovery.paths.home), {
|
|
1168
|
-
recursive: true,
|
|
1169
|
-
force: true
|
|
1170
|
-
});
|
|
1171
|
-
} catch {
|
|
1172
|
-
return failure({
|
|
1173
|
-
kind: "operational",
|
|
1174
|
-
code: "missing-project-state-removal-failed",
|
|
1175
|
-
observed: `Devbox could not remove Missing-root Project state for ${root}.`,
|
|
1176
|
-
nextAction: "Check write access to ~/.devbox and try cleanup again."
|
|
1177
|
-
});
|
|
1178
|
-
}
|
|
1179
|
-
delete projects[root];
|
|
1180
|
-
removedRoots.push(root);
|
|
1181
|
-
}
|
|
1182
|
-
if (removedRoots.length === 0) return success({
|
|
1183
|
-
roots: [],
|
|
1184
|
-
removed: false
|
|
1185
|
-
});
|
|
1186
|
-
const written = await writeAtomically(discovery.paths.projectRegistry, serializeProjectRegistry({
|
|
1187
|
-
version: 1,
|
|
1188
|
-
projects
|
|
1189
|
-
}));
|
|
1190
|
-
if (!written.ok) return written;
|
|
1191
|
-
return success({
|
|
1192
|
-
roots: removedRoots,
|
|
1193
|
-
removed: true
|
|
1194
|
-
});
|
|
1195
|
-
}
|
|
1196
1270
|
async function runSandboxLifecycle(command, input = {}) {
|
|
1197
1271
|
const projectRoot = input.root ?? process.cwd();
|
|
1198
1272
|
return withStateLocks({
|
|
@@ -1215,15 +1289,19 @@ async function runSandboxCommandUnlocked(command, input, projectRoot) {
|
|
|
1215
1289
|
const registration = registryCheck.value.projects[projectRoot];
|
|
1216
1290
|
if (registration === void 0) return notRegistered(projectRoot);
|
|
1217
1291
|
const environment = input.environment ?? currentHostEnvironment();
|
|
1292
|
+
const composePath = join(projectStateDirectory(registration.identity, paths.home), "compose.yaml");
|
|
1218
1293
|
if (command === "up") {
|
|
1219
|
-
const
|
|
1220
|
-
if (!
|
|
1221
|
-
if (!
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1294
|
+
const composeState = await readOptionalFile(composePath);
|
|
1295
|
+
if (!composeState.ok) return composeState;
|
|
1296
|
+
if (!composeState.value.exists) return failure({
|
|
1297
|
+
kind: "operational",
|
|
1298
|
+
code: "missing-project-compose",
|
|
1299
|
+
observed: `Sandbox definition is missing: ${composePath}.`,
|
|
1300
|
+
nextAction: "Run devbox config to publish the Sandbox definition, then retry devbox up."
|
|
1301
|
+
});
|
|
1302
|
+
const volumes = await ensureSharedAgentVolumes(composeState.value.content, environment);
|
|
1303
|
+
if (!volumes.ok) return volumes;
|
|
1225
1304
|
}
|
|
1226
|
-
const composePath = join(projectStateDirectory(registration.identity, paths.home), "compose.yaml");
|
|
1227
1305
|
const operation = command === "up" ? ["up", "-d"] : command === "sh" ? [
|
|
1228
1306
|
"exec",
|
|
1229
1307
|
"--user",
|
|
@@ -1311,174 +1389,6 @@ function allocateSandboxName(projectRoot, registry) {
|
|
|
1311
1389
|
nextAction: "Remove stale Devbox state only through its supported command and try again."
|
|
1312
1390
|
});
|
|
1313
1391
|
}
|
|
1314
|
-
async function projectRootExists(projectRoot) {
|
|
1315
|
-
try {
|
|
1316
|
-
return (await lstat(projectRoot)).isDirectory();
|
|
1317
|
-
} catch (error) {
|
|
1318
|
-
if (isMissingFileError(error)) return false;
|
|
1319
|
-
return;
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
async function readOptionalFile(path) {
|
|
1323
|
-
try {
|
|
1324
|
-
return success({
|
|
1325
|
-
exists: true,
|
|
1326
|
-
content: await readFile(path, "utf8")
|
|
1327
|
-
});
|
|
1328
|
-
} catch (error) {
|
|
1329
|
-
if (isMissingFileError(error)) return success({
|
|
1330
|
-
exists: false,
|
|
1331
|
-
content: ""
|
|
1332
|
-
});
|
|
1333
|
-
return failure({
|
|
1334
|
-
kind: "operational",
|
|
1335
|
-
code: "state-read-failed",
|
|
1336
|
-
observed: `Devbox could not read state file: ${path}.`,
|
|
1337
|
-
nextAction: "Check write access to ~/.devbox and try again."
|
|
1338
|
-
});
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
async function writeAtomically(path, content) {
|
|
1342
|
-
const staged = await stageStateWrite(path, content, "tmp");
|
|
1343
|
-
if (!staged.ok) return staged;
|
|
1344
|
-
try {
|
|
1345
|
-
await rename(staged.value, path);
|
|
1346
|
-
return success(void 0);
|
|
1347
|
-
} catch {
|
|
1348
|
-
await rm(staged.value, { force: true }).catch(() => void 0);
|
|
1349
|
-
return failure({
|
|
1350
|
-
kind: "operational",
|
|
1351
|
-
code: "state-write-failed",
|
|
1352
|
-
observed: `Devbox could not atomically write state file: ${path}.`,
|
|
1353
|
-
nextAction: "Check write access to ~/.devbox and run the command again."
|
|
1354
|
-
});
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
async function stageStateWrite(path, content, suffix) {
|
|
1358
|
-
const directory = dirname(path);
|
|
1359
|
-
const temporaryPath = join(directory, `.${basename(path)}-${process.pid}-${randomUUID()}.${suffix}`);
|
|
1360
|
-
try {
|
|
1361
|
-
await mkdir(directory, {
|
|
1362
|
-
recursive: true,
|
|
1363
|
-
mode: 448
|
|
1364
|
-
});
|
|
1365
|
-
await writeFile(temporaryPath, content, {
|
|
1366
|
-
encoding: "utf8",
|
|
1367
|
-
flag: "wx",
|
|
1368
|
-
mode: 384
|
|
1369
|
-
});
|
|
1370
|
-
return success(temporaryPath);
|
|
1371
|
-
} catch {
|
|
1372
|
-
await rm(temporaryPath, { force: true }).catch(() => void 0);
|
|
1373
|
-
return failure({
|
|
1374
|
-
kind: "operational",
|
|
1375
|
-
code: "state-write-failed",
|
|
1376
|
-
observed: `Devbox could not atomically write state file: ${path}.`,
|
|
1377
|
-
nextAction: "Check write access to ~/.devbox and run the command again."
|
|
1378
|
-
});
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
async function composeStateWrite(input) {
|
|
1382
|
-
const claudeHostConfiguration = await ensureClaudeHostConfiguration(input.devboxHome, input.globalConfiguration);
|
|
1383
|
-
if (!claudeHostConfiguration.ok) return claudeHostConfiguration;
|
|
1384
|
-
const rendered = renderProjectCompose({
|
|
1385
|
-
projectRoot: input.projectRoot,
|
|
1386
|
-
sandboxName: input.sandboxName,
|
|
1387
|
-
selectedNode: input.localConfiguration.node,
|
|
1388
|
-
configuredAgents: input.globalConfiguration.agent,
|
|
1389
|
-
agentNotifications: input.globalConfiguration.agent_notifications,
|
|
1390
|
-
claudeHostConfiguration: claudeHostConfiguration.value,
|
|
1391
|
-
ports: input.localConfiguration.ports
|
|
1392
|
-
});
|
|
1393
|
-
if (!rendered.ok) return rendered;
|
|
1394
|
-
const path = join(input.stateDirectory, "compose.yaml");
|
|
1395
|
-
const previous = await readOptionalFile(path);
|
|
1396
|
-
if (!previous.ok) return previous;
|
|
1397
|
-
return success({
|
|
1398
|
-
path,
|
|
1399
|
-
content: rendered.value,
|
|
1400
|
-
previous: previous.value
|
|
1401
|
-
});
|
|
1402
|
-
}
|
|
1403
|
-
async function ensureClaudeHostConfiguration(devboxHome, globalConfiguration) {
|
|
1404
|
-
if (!globalConfiguration.agent.includes("claude-code")) return success(void 0);
|
|
1405
|
-
const path = devboxPaths(devboxHome).claudeHostConfiguration;
|
|
1406
|
-
try {
|
|
1407
|
-
if ((await lstat(path)).isFile()) return success(path);
|
|
1408
|
-
return failure({
|
|
1409
|
-
kind: "operational",
|
|
1410
|
-
code: "claude-host-configuration-invalid",
|
|
1411
|
-
observed: `Claude host configuration is not a regular file: ${path}.`,
|
|
1412
|
-
nextAction: "Replace it with a regular file, then run the command again."
|
|
1413
|
-
});
|
|
1414
|
-
} catch (error) {
|
|
1415
|
-
if (!isMissingFileError(error)) return failure({
|
|
1416
|
-
kind: "operational",
|
|
1417
|
-
code: "claude-host-configuration-unreadable",
|
|
1418
|
-
observed: `Devbox could not inspect Claude host configuration: ${path}.`,
|
|
1419
|
-
nextAction: "Check access to ~/.devbox and run the command again."
|
|
1420
|
-
});
|
|
1421
|
-
}
|
|
1422
|
-
try {
|
|
1423
|
-
await mkdir(dirname(path), {
|
|
1424
|
-
recursive: true,
|
|
1425
|
-
mode: 448
|
|
1426
|
-
});
|
|
1427
|
-
await writeFile(path, "{}\n", {
|
|
1428
|
-
encoding: "utf8",
|
|
1429
|
-
flag: "wx",
|
|
1430
|
-
mode: 384
|
|
1431
|
-
});
|
|
1432
|
-
return success(path);
|
|
1433
|
-
} catch (error) {
|
|
1434
|
-
if (error.code === "EEXIST") return ensureClaudeHostConfiguration(devboxHome, globalConfiguration);
|
|
1435
|
-
return failure({
|
|
1436
|
-
kind: "operational",
|
|
1437
|
-
code: "claude-host-configuration-write-failed",
|
|
1438
|
-
observed: `Devbox could not create Claude host configuration: ${path}.`,
|
|
1439
|
-
nextAction: "Check write access to ~/.devbox and run the command again."
|
|
1440
|
-
});
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
async function writeStateTransaction(writes) {
|
|
1444
|
-
const staged = [];
|
|
1445
|
-
for (const write of writes) {
|
|
1446
|
-
const stagedWrite = await stageStateWrite(write.path, write.content, "transaction.tmp");
|
|
1447
|
-
if (!stagedWrite.ok) {
|
|
1448
|
-
await Promise.all(staged.map((stage) => rm(stage.path, { force: true }).catch(() => void 0)));
|
|
1449
|
-
return stagedWrite;
|
|
1450
|
-
}
|
|
1451
|
-
staged.push({
|
|
1452
|
-
write,
|
|
1453
|
-
path: stagedWrite.value
|
|
1454
|
-
});
|
|
1455
|
-
}
|
|
1456
|
-
const published = [];
|
|
1457
|
-
try {
|
|
1458
|
-
for (const stage of staged) {
|
|
1459
|
-
await rename(stage.path, stage.write.path);
|
|
1460
|
-
published.push(stage.write);
|
|
1461
|
-
}
|
|
1462
|
-
return success(void 0);
|
|
1463
|
-
} catch {
|
|
1464
|
-
let restored = true;
|
|
1465
|
-
for (const write of published.reverse()) if (write.previous.exists) {
|
|
1466
|
-
const restoredWrite = await writeAtomically(write.path, write.previous.content);
|
|
1467
|
-
restored &&= restoredWrite.ok;
|
|
1468
|
-
} else try {
|
|
1469
|
-
await rm(write.path, { force: true });
|
|
1470
|
-
} catch {
|
|
1471
|
-
restored = false;
|
|
1472
|
-
}
|
|
1473
|
-
await Promise.all(staged.map((stage) => rm(stage.path, { force: true }).catch(() => void 0)));
|
|
1474
|
-
return failure({
|
|
1475
|
-
kind: "operational",
|
|
1476
|
-
code: "state-write-failed",
|
|
1477
|
-
observed: restored ? "Devbox could not publish Project state; the previous state was restored." : "Devbox could not publish Project state or restore the previous state.",
|
|
1478
|
-
nextAction: "Check write access to ~/.devbox and run the command again."
|
|
1479
|
-
});
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
1392
|
function validateGlobalObject(configuration, catalog) {
|
|
1483
1393
|
try {
|
|
1484
1394
|
return parseGlobalConfiguration(serializeGlobalConfiguration(configuration), catalog);
|
|
@@ -1547,17 +1457,15 @@ function createConfigurationPrompter({ signal, input, output }) {
|
|
|
1547
1457
|
message: "Where should this configuration apply?",
|
|
1548
1458
|
options: [{
|
|
1549
1459
|
value: "local",
|
|
1550
|
-
label: "Current project"
|
|
1551
|
-
hint: "Enabled when this project’s container starts."
|
|
1460
|
+
label: "Current project"
|
|
1552
1461
|
}, {
|
|
1553
1462
|
value: "global",
|
|
1554
|
-
label: "Workspace Docker image"
|
|
1555
|
-
hint: "Built into the shared image. It affects image size and disk usage."
|
|
1463
|
+
label: "Workspace Docker image"
|
|
1556
1464
|
}],
|
|
1557
1465
|
...common
|
|
1558
1466
|
})),
|
|
1559
1467
|
editGlobal: async (configuration, catalog) => {
|
|
1560
|
-
p.log.step(
|
|
1468
|
+
p.log.step(c.bold(c.cyan("Workspace Docker image")), common);
|
|
1561
1469
|
return {
|
|
1562
1470
|
version: 1,
|
|
1563
1471
|
node: await promptValue(p.multiselect({
|
|
@@ -1582,7 +1490,7 @@ function createConfigurationPrompter({ signal, input, output }) {
|
|
|
1582
1490
|
};
|
|
1583
1491
|
},
|
|
1584
1492
|
editLocal: async (configuration, catalog, globalConfiguration, editPorts = true) => {
|
|
1585
|
-
p.log.step(
|
|
1493
|
+
p.log.step(c.bold(c.cyan("Project configuration")), common);
|
|
1586
1494
|
const node = await promptValue(p.select({
|
|
1587
1495
|
message: "Selected Node release line",
|
|
1588
1496
|
options: [{
|
|
@@ -1600,7 +1508,7 @@ function createConfigurationPrompter({ signal, input, output }) {
|
|
|
1600
1508
|
node
|
|
1601
1509
|
};
|
|
1602
1510
|
const ports = normalizePortMappings((await promptValue(p.multiline({
|
|
1603
|
-
message: "Published ports",
|
|
1511
|
+
message: "Published ports" + c.dim(" (multiline; one mapping per line; press Enter twice to submit)"),
|
|
1604
1512
|
initialValue: configuration.ports.join("\n"),
|
|
1605
1513
|
validate: (value) => {
|
|
1606
1514
|
const ports = normalizePortMappings((value ?? "").split("\n"));
|
|
@@ -1652,20 +1560,28 @@ function showLogo() {
|
|
|
1652
1560
|
for (let i = 0; i < ASCII_ART.length; i++) console.log(`${COLORS[i]}${ASCII_ART[i]}${REST}`);
|
|
1653
1561
|
}
|
|
1654
1562
|
//#endregion
|
|
1655
|
-
//#region \0devbox-image-default:/home/runner/work/devbox/devbox/src/agent/image-defaults/.claude/settings.json.mjs
|
|
1656
|
-
var settings_json_default = "{\n \"$schema\": \"https://json.schemastore.org/claude-code-settings.json\",\n \"env\": {\n \"CLAUDE_CODE_USE_POWERSHELL_TOOL\": \"0\"\n },\n \"attribution\": {\n \"commit\": \"\",\n \"pr\": \"\"\n },\n \"includeGitInstructions\": false,\n \"autoMemoryEnabled\": false\n}\n";
|
|
1657
|
-
//#endregion
|
|
1658
|
-
//#region \0devbox-image-default:/home/runner/work/devbox/devbox/src/agent/image-defaults/.omp/agent/config.yml.mjs
|
|
1659
|
-
var config_yml_default = "setupVersion: 2\n\n# Basic\nmodelRoles:\n default: openai-codex/gpt-5.6-terra:high\n smol: openai-codex/gpt-5.6-luna:high\n slow: openai-codex/gpt-5.6-sol:max\n plan: openai-codex/gpt-5.6-sol:max\nsymbolPreset: unicode\n\n# Theme\ntheme:\n dark: titanium\n light: light\ncomposer:\n shape: claude\n\n# Auto QA\ndev:\n autoqa: false\n autoqaConsent: denied\n";
|
|
1660
|
-
//#endregion
|
|
1661
1563
|
//#region src/agent/build.ts
|
|
1564
|
+
const AGENT_BUILD_CONTEXT_ASSETS = {
|
|
1565
|
+
instructions: {
|
|
1566
|
+
relativePath: "AGENTS.md",
|
|
1567
|
+
content: "# 協作規範\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"
|
|
1568
|
+
},
|
|
1569
|
+
claude: {
|
|
1570
|
+
relativePath: ".claude/settings.json",
|
|
1571
|
+
content: "{\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"
|
|
1572
|
+
},
|
|
1573
|
+
omp: {
|
|
1574
|
+
relativePath: ".omp/agent/config.yml",
|
|
1575
|
+
content: "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"
|
|
1576
|
+
}
|
|
1577
|
+
};
|
|
1662
1578
|
function createAgentWorkspaceContribution(input) {
|
|
1663
1579
|
const agents = input.configuredAgents.map((name) => ({
|
|
1664
1580
|
name,
|
|
1665
1581
|
recipe: PACKAGED_AGENTS[name]
|
|
1666
1582
|
}));
|
|
1667
1583
|
const buildContextAssets = selectBuildContextAssets(agents);
|
|
1668
|
-
const skillAgents = agents.
|
|
1584
|
+
const skillAgents = [...new Set(agents.flatMap((agent) => agent.recipe.skillInstallationTarget === void 0 ? [] : [agent.recipe.skillInstallationTarget]))];
|
|
1669
1585
|
return {
|
|
1670
1586
|
buildContextAssets,
|
|
1671
1587
|
dockerignoreEntries: selectDockerignoreEntries(buildContextAssets),
|
|
@@ -1690,37 +1606,30 @@ function renderAgentInstallations(agents, buildNodeRuntimeRoot, skillAgents) {
|
|
|
1690
1606
|
}
|
|
1691
1607
|
function selectDockerignoreEntries(assets) {
|
|
1692
1608
|
const entries = [];
|
|
1693
|
-
if (assets.some((asset) => asset.relativePath === ".
|
|
1694
|
-
if (assets.some((asset) => asset.relativePath
|
|
1609
|
+
if (assets.some((asset) => asset.relativePath === "AGENTS.md")) entries.push("!AGENTS.md");
|
|
1610
|
+
if (assets.some((asset) => asset.relativePath.startsWith(".claude/"))) entries.push("!.claude", "!.claude/settings.json");
|
|
1611
|
+
if (assets.some((asset) => asset.relativePath.startsWith(".omp/agent/"))) entries.push("!.omp", "!.omp/agent", "!.omp/agent/config.yml");
|
|
1695
1612
|
return entries;
|
|
1696
1613
|
}
|
|
1697
1614
|
function selectBuildContextAssets(agents) {
|
|
1698
1615
|
const assets = [];
|
|
1699
|
-
if (agents.
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
});
|
|
1703
|
-
if (agents.some((agent) => agent.name === "omp")) assets.push({
|
|
1704
|
-
relativePath: ".omp/agent/config.yml",
|
|
1705
|
-
content: config_yml_default
|
|
1706
|
-
});
|
|
1616
|
+
if (agents.length > 0) assets.push(AGENT_BUILD_CONTEXT_ASSETS.instructions);
|
|
1617
|
+
if (agents.some((agent) => agent.name === "claude-code")) assets.push(AGENT_BUILD_CONTEXT_ASSETS.claude);
|
|
1618
|
+
if (agents.some((agent) => agent.name === "omp")) assets.push(AGENT_BUILD_CONTEXT_ASSETS.omp);
|
|
1707
1619
|
return assets;
|
|
1708
1620
|
}
|
|
1709
1621
|
function renderAgentAssets(agents) {
|
|
1710
1622
|
const hasClaudeCode = agents.some((agent) => agent.name === "claude-code");
|
|
1623
|
+
const hasCodex = agents.some((agent) => agent.name === "codex");
|
|
1624
|
+
const hasAgy = agents.some((agent) => agent.name === "agy");
|
|
1711
1625
|
const hasOmp = agents.some((agent) => agent.name === "omp");
|
|
1712
|
-
if (!hasClaudeCode && !hasOmp) return [];
|
|
1713
|
-
const ownershipCommands = [];
|
|
1626
|
+
if (!hasClaudeCode && !hasCodex && !hasAgy && !hasOmp) return [];
|
|
1714
1627
|
const lines = ["# Copy AI dotfiles"];
|
|
1715
|
-
if (hasClaudeCode)
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
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 && ")}`, "");
|
|
1628
|
+
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");
|
|
1629
|
+
if (hasCodex) lines.push("COPY --chown=devbox:devbox AGENTS.md /home/devbox/.codex/AGENTS.md");
|
|
1630
|
+
if (hasAgy) lines.push("COPY --chown=devbox:devbox AGENTS.md /home/devbox/.gemini/GEMINI.md");
|
|
1631
|
+
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");
|
|
1632
|
+
lines.push("");
|
|
1724
1633
|
return lines;
|
|
1725
1634
|
}
|
|
1726
1635
|
function renderAgentNotifications(agents, notificationsEnabled) {
|
|
@@ -1872,7 +1781,7 @@ async function buildWorkspaceUnlocked(input) {
|
|
|
1872
1781
|
kind: "operational",
|
|
1873
1782
|
code: "global-configuration-missing",
|
|
1874
1783
|
observed: "Global configuration is required before building the Workspace image.",
|
|
1875
|
-
nextAction: "Run devbox
|
|
1784
|
+
nextAction: "Run devbox init to commit Global configuration before devbox build."
|
|
1876
1785
|
});
|
|
1877
1786
|
}
|
|
1878
1787
|
const parsedGlobal = parseGlobalConfiguration(globalSource, catalogCheck.value);
|
|
@@ -1892,7 +1801,7 @@ async function buildWorkspaceUnlocked(input) {
|
|
|
1892
1801
|
agent
|
|
1893
1802
|
});
|
|
1894
1803
|
await mkdir(paths.buildContext, { recursive: true });
|
|
1895
|
-
await
|
|
1804
|
+
await refreshBuildContextAssets(paths.buildContext, buildContextAssets);
|
|
1896
1805
|
await Promise.all([
|
|
1897
1806
|
writeFile(join(paths.buildContext, "Dockerfile"), dockerfile),
|
|
1898
1807
|
writeFile(join(paths.buildContext, ".dockerignore"), renderDockerignore(buildContextAssets, agent.dockerignoreEntries)),
|
|
@@ -1972,7 +1881,7 @@ function renderBuildDockerfile(input) {
|
|
|
1972
1881
|
lines.push("WORKDIR /workspace", "USER devbox", "");
|
|
1973
1882
|
lines.push(...input.node.sandboxUserSetup);
|
|
1974
1883
|
lines.push(...input.agent.installationDockerfileLines);
|
|
1975
|
-
lines.push("USER root", "", "# Copy dotfiles", "COPY .bash_aliases /home/devbox/.bash_aliases", "COPY
|
|
1884
|
+
lines.push("USER root", "", "# Copy dotfiles", "COPY --chown=devbox:devbox .bash_aliases /home/devbox/.bash_aliases", "COPY --chown=devbox:devbox .gitconfig /home/devbox/.gitconfig", "");
|
|
1976
1885
|
lines.push(...input.agent.assetDockerfileLines);
|
|
1977
1886
|
lines.push(...input.agent.notificationDockerfileLines);
|
|
1978
1887
|
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\"]");
|
|
@@ -1989,15 +1898,13 @@ function renderDockerignore(assets, additionalEntries) {
|
|
|
1989
1898
|
...additionalEntries
|
|
1990
1899
|
].join("\n")}\n`;
|
|
1991
1900
|
}
|
|
1992
|
-
async function
|
|
1901
|
+
async function refreshBuildContextAssets(buildContext, assets) {
|
|
1902
|
+
const requiredPaths = new Set(assets.map((asset) => asset.relativePath));
|
|
1903
|
+
for (const asset of Object.values(AGENT_BUILD_CONTEXT_ASSETS)) if (!requiredPaths.has(asset.relativePath)) await rm(join(buildContext, asset.relativePath), { force: true });
|
|
1993
1904
|
await Promise.all(assets.map(async (asset) => {
|
|
1994
1905
|
const destination = join(buildContext, asset.relativePath);
|
|
1995
1906
|
await mkdir(dirname(destination), { recursive: true });
|
|
1996
|
-
|
|
1997
|
-
await writeFile(destination, asset.content, { flag: "wx" });
|
|
1998
|
-
} catch (error) {
|
|
1999
|
-
if (typeof error !== "object" || error === null || !("code" in error) || error.code !== "EEXIST") throw error;
|
|
2000
|
-
}
|
|
1907
|
+
await writeFile(destination, asset.content);
|
|
2001
1908
|
}));
|
|
2002
1909
|
}
|
|
2003
1910
|
function renderEntrypoint(input) {
|
|
@@ -2088,7 +1995,7 @@ function createCli(signal, interactive) {
|
|
|
2088
1995
|
outro("Project is already registered.");
|
|
2089
1996
|
return success({});
|
|
2090
1997
|
}
|
|
2091
|
-
outro(`Next: run ${
|
|
1998
|
+
outro(result.value.globalConfigurationCreated ? `Next: run ${command("devbox build")} to prepare your workspace, then ${command("devbox up")} to start it.` : `Next: run ${command("devbox up")} to start it.`);
|
|
2092
1999
|
return success({});
|
|
2093
2000
|
});
|
|
2094
2001
|
cli.command("config", "Edit Global or Local configuration for the current Project.").action(async () => {
|
|
@@ -2100,7 +2007,7 @@ function createCli(signal, interactive) {
|
|
|
2100
2007
|
prompt
|
|
2101
2008
|
});
|
|
2102
2009
|
if (!result.ok) return result;
|
|
2103
|
-
outro(result.value.changed ?
|
|
2010
|
+
outro(result.value.changed ? `Global configuration updated. Run ${command("devbox build")} to rebuild the Workspace image.` : "Global configuration was not changed.");
|
|
2104
2011
|
return success({});
|
|
2105
2012
|
}
|
|
2106
2013
|
const result = await configureLocalProject({
|
|
@@ -2119,7 +2026,7 @@ function createCli(signal, interactive) {
|
|
|
2119
2026
|
if (!result.ok) return result;
|
|
2120
2027
|
return success({ message: `Built Workspace image: ${result.value.image}` });
|
|
2121
2028
|
});
|
|
2122
|
-
cli.command("rm", "Remove the current
|
|
2029
|
+
cli.command("rm", "Remove the current Project Sandbox and Devbox state.").option("--yes", "Skip the confirmation prompt.").action(async (options = {}) => {
|
|
2123
2030
|
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.");
|
|
2124
2031
|
const result = await removeProject({
|
|
2125
2032
|
signal,
|
|
@@ -2129,22 +2036,6 @@ function createCli(signal, interactive) {
|
|
|
2129
2036
|
if (!result.ok) return result;
|
|
2130
2037
|
return success({ message: result.value.removed ? `Removed Project: ${result.value.root}` : "Project removal was not changed." });
|
|
2131
2038
|
});
|
|
2132
|
-
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 = {}) => {
|
|
2133
|
-
if (!options.missingProjects) return failure({
|
|
2134
|
-
kind: "usage",
|
|
2135
|
-
code: "cleanup-mode-required",
|
|
2136
|
-
observed: "cleanup requires --missing-projects.",
|
|
2137
|
-
nextAction: "Run devbox cleanup --missing-projects."
|
|
2138
|
-
});
|
|
2139
|
-
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.");
|
|
2140
|
-
const result = await cleanupMissingProjects({
|
|
2141
|
-
signal,
|
|
2142
|
-
confirm: prompt.confirm,
|
|
2143
|
-
yes: options.yes
|
|
2144
|
-
});
|
|
2145
|
-
if (!result.ok) return result;
|
|
2146
|
-
return success({ message: result.value.removed ? `Removed Missing-root Projects: ${result.value.roots.join(", ")}` : "No Missing-root Projects were removed." });
|
|
2147
|
-
});
|
|
2148
2039
|
for (const [command, description] of [
|
|
2149
2040
|
["up", "Start the current Project Sandbox."],
|
|
2150
2041
|
["down", "Stop and remove the current Project Sandbox."],
|
|
@@ -2163,7 +2054,7 @@ function createCli(signal, interactive) {
|
|
|
2163
2054
|
}
|
|
2164
2055
|
function present(result) {
|
|
2165
2056
|
if (!result.ok) {
|
|
2166
|
-
process.stderr.write(`${result.error.observed}\n${result.error.nextAction}\n`);
|
|
2057
|
+
process.stderr.write(`${highlightCommands(result.error.observed)}\n${highlightCommands(result.error.nextAction)}\n`);
|
|
2167
2058
|
return result.error.kind === "usage" ? 2 : 1;
|
|
2168
2059
|
}
|
|
2169
2060
|
if (result.value.message !== void 0) process.stdout.write(`${result.value.message}\n`);
|
|
@@ -2196,6 +2087,12 @@ async function main() {
|
|
|
2196
2087
|
function isCacError(error) {
|
|
2197
2088
|
return error instanceof Error && error.name === "CACError";
|
|
2198
2089
|
}
|
|
2090
|
+
function command(value) {
|
|
2091
|
+
return c.cyan(c.bold(value));
|
|
2092
|
+
}
|
|
2093
|
+
function highlightCommands(message) {
|
|
2094
|
+
return message.replace(/\bdevbox (?:init|build|config|up|down|stop|sh|rm)\b/g, command);
|
|
2095
|
+
}
|
|
2199
2096
|
function interactiveFailure(observed, nextAction) {
|
|
2200
2097
|
return failure({
|
|
2201
2098
|
kind: "usage",
|