@ycs77/devbox 0.7.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 +1 -1
- package/dist/cli.mjs +406 -400
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ Published ports accept one mapping per line. The following are the supported por
|
|
|
56
56
|
- `5173:5173` maps host port `5173` to container port `5173`.
|
|
57
57
|
- `APP_PORT:5173:5173` maps the host port from the `APP_PORT` environment variable (default port: `5173`) to container port `5173`.
|
|
58
58
|
|
|
59
|
-
Build the shared Workspace image
|
|
59
|
+
Build the shared Workspace image after setup, changes to global configuration, or a Devbox package upgrade:
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
62
|
devbox build
|
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.7.
|
|
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 = [
|
|
@@ -100,85 +204,6 @@ const PACKAGED_RUNTIME_CATALOG = {
|
|
|
100
204
|
agents: Object.keys(PACKAGED_AGENTS)
|
|
101
205
|
};
|
|
102
206
|
//#endregion
|
|
103
|
-
//#region src/result.ts
|
|
104
|
-
function success(value) {
|
|
105
|
-
return {
|
|
106
|
-
ok: true,
|
|
107
|
-
value
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
function failure(error) {
|
|
111
|
-
return {
|
|
112
|
-
ok: false,
|
|
113
|
-
error
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
//#endregion
|
|
117
|
-
//#region src/host.ts
|
|
118
|
-
const execFile$1 = promisify(execFile);
|
|
119
|
-
var HostCommandError = class extends Error {
|
|
120
|
-
exitCode;
|
|
121
|
-
constructor(file, args, exitCode) {
|
|
122
|
-
super(`${file} ${args.join(" ")} exited with status ${exitCode}.`);
|
|
123
|
-
this.exitCode = exitCode;
|
|
124
|
-
this.name = "HostCommandError";
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
const supportedHostAction = "Start Docker Desktop and ensure Docker Compose is available.";
|
|
128
|
-
function currentHostEnvironment() {
|
|
129
|
-
return {
|
|
130
|
-
run: async (file, args) => {
|
|
131
|
-
await execFile$1(file, args);
|
|
132
|
-
},
|
|
133
|
-
runDirect: async (file, args) => new Promise((resolve, reject) => {
|
|
134
|
-
const command = spawn(file, args, { stdio: "inherit" });
|
|
135
|
-
command.once("error", reject);
|
|
136
|
-
command.once("close", (code) => {
|
|
137
|
-
if (code === 0) {
|
|
138
|
-
resolve();
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
reject(new HostCommandError(file, args, code ?? 1));
|
|
142
|
-
});
|
|
143
|
-
})
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
async function validateSupportedHost(environment = currentHostEnvironment()) {
|
|
147
|
-
for (const [file, args, description] of [[
|
|
148
|
-
"docker",
|
|
149
|
-
["--version"],
|
|
150
|
-
"Docker"
|
|
151
|
-
], [
|
|
152
|
-
"docker",
|
|
153
|
-
["compose", "version"],
|
|
154
|
-
"Docker Compose"
|
|
155
|
-
]]) try {
|
|
156
|
-
await environment.run(file, args);
|
|
157
|
-
} catch {
|
|
158
|
-
return failure({
|
|
159
|
-
kind: "validation",
|
|
160
|
-
code: "missing-host-prerequisite",
|
|
161
|
-
observed: `${description} is unavailable.`,
|
|
162
|
-
nextAction: supportedHostAction
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
return success(void 0);
|
|
166
|
-
}
|
|
167
|
-
//#endregion
|
|
168
|
-
//#region src/agent/volumes.ts
|
|
169
|
-
async function ensureSharedAgentVolumes(configuredAgents, environment = currentHostEnvironment()) {
|
|
170
|
-
for (const configuredAgent of configuredAgents) {
|
|
171
|
-
const volumeName = PACKAGED_AGENTS[configuredAgent]?.home.volumeName;
|
|
172
|
-
if (volumeName !== void 0) await environment.run("docker", [
|
|
173
|
-
"volume",
|
|
174
|
-
"create",
|
|
175
|
-
"--driver",
|
|
176
|
-
"local",
|
|
177
|
-
volumeName
|
|
178
|
-
]);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
//#endregion
|
|
182
207
|
//#region src/configuration/index.ts
|
|
183
208
|
function normalizeCatalog(catalog = PACKAGED_RUNTIME_CATALOG) {
|
|
184
209
|
if (!isRecord(catalog.runtimes) || !Array.isArray(catalog.agents)) return invalid$1("invalid-runtime-catalog", "The packaged Runtime catalog is invalid.");
|
|
@@ -404,7 +429,7 @@ async function withStateLocks(input, operation) {
|
|
|
404
429
|
try {
|
|
405
430
|
await lstat(input.devboxHome);
|
|
406
431
|
} catch (error) {
|
|
407
|
-
if (isMissingFileError$
|
|
432
|
+
if (isMissingFileError$2(error)) devboxHomeExisted = false;
|
|
408
433
|
else throw error;
|
|
409
434
|
}
|
|
410
435
|
const acquired = [];
|
|
@@ -467,7 +492,7 @@ async function acquireLock(target) {
|
|
|
467
492
|
function isFileExistsError(error) {
|
|
468
493
|
return typeof error === "object" && error !== null && error.code === "EEXIST";
|
|
469
494
|
}
|
|
470
|
-
function isMissingFileError$
|
|
495
|
+
function isMissingFileError$2(error) {
|
|
471
496
|
return typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
472
497
|
}
|
|
473
498
|
//#endregion
|
|
@@ -649,13 +674,207 @@ function isPlainObject(value) {
|
|
|
649
674
|
const prototype = Object.getPrototypeOf(value);
|
|
650
675
|
return prototype === Object.prototype || prototype === null;
|
|
651
676
|
}
|
|
652
|
-
function invalid(observed) {
|
|
653
|
-
return failure({
|
|
654
|
-
kind: "validation",
|
|
655
|
-
code: "invalid-project-registry",
|
|
656
|
-
observed,
|
|
657
|
-
nextAction: "Repair ~/.devbox/projects.yaml using the supported version-1 registry schema."
|
|
658
|
-
});
|
|
677
|
+
function invalid(observed) {
|
|
678
|
+
return failure({
|
|
679
|
+
kind: "validation",
|
|
680
|
+
code: "invalid-project-registry",
|
|
681
|
+
observed,
|
|
682
|
+
nextAction: "Repair ~/.devbox/projects.yaml using the supported version-1 registry schema."
|
|
683
|
+
});
|
|
684
|
+
}
|
|
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";
|
|
659
878
|
}
|
|
660
879
|
//#endregion
|
|
661
880
|
//#region src/project/index.ts
|
|
@@ -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
|
}
|
|
@@ -885,11 +1077,7 @@ async function configureLocalProjectUnlocked(input = {}) {
|
|
|
885
1077
|
if (input.nextConfiguration === void 0 && input.prompt?.editLocal) nextConfiguration = await input.prompt.editLocal(localCheck.value, catalog, globalCheck.value);
|
|
886
1078
|
const nextCheck = validateLocalObject(nextConfiguration, globalCheck.value, catalog);
|
|
887
1079
|
if (!nextCheck.ok) return nextCheck;
|
|
888
|
-
|
|
889
|
-
scope: "local",
|
|
890
|
-
root: projectRoot,
|
|
891
|
-
changed: false
|
|
892
|
-
});
|
|
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 = {}) {
|
|
@@ -1121,15 +1289,19 @@ async function runSandboxCommandUnlocked(command, input, projectRoot) {
|
|
|
1121
1289
|
const registration = registryCheck.value.projects[projectRoot];
|
|
1122
1290
|
if (registration === void 0) return notRegistered(projectRoot);
|
|
1123
1291
|
const environment = input.environment ?? currentHostEnvironment();
|
|
1292
|
+
const composePath = join(projectStateDirectory(registration.identity, paths.home), "compose.yaml");
|
|
1124
1293
|
if (command === "up") {
|
|
1125
|
-
const
|
|
1126
|
-
if (!
|
|
1127
|
-
if (!
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
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;
|
|
1131
1304
|
}
|
|
1132
|
-
const composePath = join(projectStateDirectory(registration.identity, paths.home), "compose.yaml");
|
|
1133
1305
|
const operation = command === "up" ? ["up", "-d"] : command === "sh" ? [
|
|
1134
1306
|
"exec",
|
|
1135
1307
|
"--user",
|
|
@@ -1217,166 +1389,6 @@ function allocateSandboxName(projectRoot, registry) {
|
|
|
1217
1389
|
nextAction: "Remove stale Devbox state only through its supported command and try again."
|
|
1218
1390
|
});
|
|
1219
1391
|
}
|
|
1220
|
-
async function readOptionalFile(path) {
|
|
1221
|
-
try {
|
|
1222
|
-
return success({
|
|
1223
|
-
exists: true,
|
|
1224
|
-
content: await readFile(path, "utf8")
|
|
1225
|
-
});
|
|
1226
|
-
} catch (error) {
|
|
1227
|
-
if (isMissingFileError(error)) return success({
|
|
1228
|
-
exists: false,
|
|
1229
|
-
content: ""
|
|
1230
|
-
});
|
|
1231
|
-
return failure({
|
|
1232
|
-
kind: "operational",
|
|
1233
|
-
code: "state-read-failed",
|
|
1234
|
-
observed: `Devbox could not read state file: ${path}.`,
|
|
1235
|
-
nextAction: "Check write access to ~/.devbox and try again."
|
|
1236
|
-
});
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
async function writeAtomically(path, content) {
|
|
1240
|
-
const staged = await stageStateWrite(path, content, "tmp");
|
|
1241
|
-
if (!staged.ok) return staged;
|
|
1242
|
-
try {
|
|
1243
|
-
await rename(staged.value, path);
|
|
1244
|
-
return success(void 0);
|
|
1245
|
-
} catch {
|
|
1246
|
-
await rm(staged.value, { force: true }).catch(() => void 0);
|
|
1247
|
-
return failure({
|
|
1248
|
-
kind: "operational",
|
|
1249
|
-
code: "state-write-failed",
|
|
1250
|
-
observed: `Devbox could not atomically write state file: ${path}.`,
|
|
1251
|
-
nextAction: "Check write access to ~/.devbox and run the command again."
|
|
1252
|
-
});
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
async function stageStateWrite(path, content, suffix) {
|
|
1256
|
-
const directory = dirname(path);
|
|
1257
|
-
const temporaryPath = join(directory, `.${basename(path)}-${process.pid}-${randomUUID()}.${suffix}`);
|
|
1258
|
-
try {
|
|
1259
|
-
await mkdir(directory, {
|
|
1260
|
-
recursive: true,
|
|
1261
|
-
mode: 448
|
|
1262
|
-
});
|
|
1263
|
-
await writeFile(temporaryPath, content, {
|
|
1264
|
-
encoding: "utf8",
|
|
1265
|
-
flag: "wx",
|
|
1266
|
-
mode: 384
|
|
1267
|
-
});
|
|
1268
|
-
return success(temporaryPath);
|
|
1269
|
-
} catch {
|
|
1270
|
-
await rm(temporaryPath, { force: true }).catch(() => void 0);
|
|
1271
|
-
return failure({
|
|
1272
|
-
kind: "operational",
|
|
1273
|
-
code: "state-write-failed",
|
|
1274
|
-
observed: `Devbox could not atomically write state file: ${path}.`,
|
|
1275
|
-
nextAction: "Check write access to ~/.devbox and run the command again."
|
|
1276
|
-
});
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
async function composeStateWrite(input) {
|
|
1280
|
-
const claudeHostConfiguration = await ensureClaudeHostConfiguration(input.devboxHome, input.globalConfiguration);
|
|
1281
|
-
if (!claudeHostConfiguration.ok) return claudeHostConfiguration;
|
|
1282
|
-
const rendered = renderProjectCompose({
|
|
1283
|
-
projectRoot: input.projectRoot,
|
|
1284
|
-
sandboxName: input.sandboxName,
|
|
1285
|
-
selectedNode: input.localConfiguration.node,
|
|
1286
|
-
configuredAgents: input.globalConfiguration.agent,
|
|
1287
|
-
agentNotifications: input.globalConfiguration.agent_notifications,
|
|
1288
|
-
claudeHostConfiguration: claudeHostConfiguration.value,
|
|
1289
|
-
ports: input.localConfiguration.ports
|
|
1290
|
-
});
|
|
1291
|
-
if (!rendered.ok) return rendered;
|
|
1292
|
-
const path = join(input.stateDirectory, "compose.yaml");
|
|
1293
|
-
const previous = await readOptionalFile(path);
|
|
1294
|
-
if (!previous.ok) return previous;
|
|
1295
|
-
return success({
|
|
1296
|
-
path,
|
|
1297
|
-
content: rendered.value,
|
|
1298
|
-
previous: previous.value
|
|
1299
|
-
});
|
|
1300
|
-
}
|
|
1301
|
-
async function ensureClaudeHostConfiguration(devboxHome, globalConfiguration) {
|
|
1302
|
-
if (!globalConfiguration.agent.includes("claude-code")) return success(void 0);
|
|
1303
|
-
const path = devboxPaths(devboxHome).claudeHostConfiguration;
|
|
1304
|
-
try {
|
|
1305
|
-
if ((await lstat(path)).isFile()) return success(path);
|
|
1306
|
-
return failure({
|
|
1307
|
-
kind: "operational",
|
|
1308
|
-
code: "claude-host-configuration-invalid",
|
|
1309
|
-
observed: `Claude host configuration is not a regular file: ${path}.`,
|
|
1310
|
-
nextAction: "Replace it with a regular file, then run the command again."
|
|
1311
|
-
});
|
|
1312
|
-
} catch (error) {
|
|
1313
|
-
if (!isMissingFileError(error)) return failure({
|
|
1314
|
-
kind: "operational",
|
|
1315
|
-
code: "claude-host-configuration-unreadable",
|
|
1316
|
-
observed: `Devbox could not inspect Claude host configuration: ${path}.`,
|
|
1317
|
-
nextAction: "Check access to ~/.devbox and run the command again."
|
|
1318
|
-
});
|
|
1319
|
-
}
|
|
1320
|
-
try {
|
|
1321
|
-
await mkdir(dirname(path), {
|
|
1322
|
-
recursive: true,
|
|
1323
|
-
mode: 448
|
|
1324
|
-
});
|
|
1325
|
-
await writeFile(path, "{}\n", {
|
|
1326
|
-
encoding: "utf8",
|
|
1327
|
-
flag: "wx",
|
|
1328
|
-
mode: 384
|
|
1329
|
-
});
|
|
1330
|
-
return success(path);
|
|
1331
|
-
} catch (error) {
|
|
1332
|
-
if (error.code === "EEXIST") return ensureClaudeHostConfiguration(devboxHome, globalConfiguration);
|
|
1333
|
-
return failure({
|
|
1334
|
-
kind: "operational",
|
|
1335
|
-
code: "claude-host-configuration-write-failed",
|
|
1336
|
-
observed: `Devbox could not create Claude host configuration: ${path}.`,
|
|
1337
|
-
nextAction: "Check write access to ~/.devbox and run the command again."
|
|
1338
|
-
});
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
async function writeStateTransaction(writes) {
|
|
1342
|
-
const staged = [];
|
|
1343
|
-
for (const write of writes) {
|
|
1344
|
-
const stagedWrite = await stageStateWrite(write.path, write.content, "transaction.tmp");
|
|
1345
|
-
if (!stagedWrite.ok) {
|
|
1346
|
-
await Promise.all(staged.map((stage) => rm(stage.path, { force: true }).catch(() => void 0)));
|
|
1347
|
-
return stagedWrite;
|
|
1348
|
-
}
|
|
1349
|
-
staged.push({
|
|
1350
|
-
write,
|
|
1351
|
-
path: stagedWrite.value
|
|
1352
|
-
});
|
|
1353
|
-
}
|
|
1354
|
-
const published = [];
|
|
1355
|
-
try {
|
|
1356
|
-
for (const stage of staged) {
|
|
1357
|
-
await rename(stage.path, stage.write.path);
|
|
1358
|
-
published.push(stage.write);
|
|
1359
|
-
}
|
|
1360
|
-
return success(void 0);
|
|
1361
|
-
} catch {
|
|
1362
|
-
let restored = true;
|
|
1363
|
-
for (const write of published.reverse()) if (write.previous.exists) {
|
|
1364
|
-
const restoredWrite = await writeAtomically(write.path, write.previous.content);
|
|
1365
|
-
restored &&= restoredWrite.ok;
|
|
1366
|
-
} else try {
|
|
1367
|
-
await rm(write.path, { force: true });
|
|
1368
|
-
} catch {
|
|
1369
|
-
restored = false;
|
|
1370
|
-
}
|
|
1371
|
-
await Promise.all(staged.map((stage) => rm(stage.path, { force: true }).catch(() => void 0)));
|
|
1372
|
-
return failure({
|
|
1373
|
-
kind: "operational",
|
|
1374
|
-
code: "state-write-failed",
|
|
1375
|
-
observed: restored ? "Devbox could not publish Project state; the previous state was restored." : "Devbox could not publish Project state or restore the previous state.",
|
|
1376
|
-
nextAction: "Check write access to ~/.devbox and run the command again."
|
|
1377
|
-
});
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
1392
|
function validateGlobalObject(configuration, catalog) {
|
|
1381
1393
|
try {
|
|
1382
1394
|
return parseGlobalConfiguration(serializeGlobalConfiguration(configuration), catalog);
|
|
@@ -1548,16 +1560,21 @@ function showLogo() {
|
|
|
1548
1560
|
for (let i = 0; i < ASCII_ART.length; i++) console.log(`${COLORS[i]}${ASCII_ART[i]}${REST}`);
|
|
1549
1561
|
}
|
|
1550
1562
|
//#endregion
|
|
1551
|
-
//#region \0devbox-image-default:/home/runner/work/devbox/devbox/src/agent/image-defaults/.claude/settings.json.mjs
|
|
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";
|
|
1553
|
-
//#endregion
|
|
1554
|
-
//#region \0devbox-image-default:/home/runner/work/devbox/devbox/src/agent/image-defaults/.omp/agent/config.yml.mjs
|
|
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";
|
|
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
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
|
+
};
|
|
1561
1578
|
function createAgentWorkspaceContribution(input) {
|
|
1562
1579
|
const agents = input.configuredAgents.map((name) => ({
|
|
1563
1580
|
name,
|
|
@@ -1596,18 +1613,9 @@ function selectDockerignoreEntries(assets) {
|
|
|
1596
1613
|
}
|
|
1597
1614
|
function selectBuildContextAssets(agents) {
|
|
1598
1615
|
const assets = [];
|
|
1599
|
-
if (agents.length > 0) assets.push(
|
|
1600
|
-
|
|
1601
|
-
|
|
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
|
-
});
|
|
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);
|
|
1611
1619
|
return assets;
|
|
1612
1620
|
}
|
|
1613
1621
|
function renderAgentAssets(agents) {
|
|
@@ -1793,7 +1801,7 @@ async function buildWorkspaceUnlocked(input) {
|
|
|
1793
1801
|
agent
|
|
1794
1802
|
});
|
|
1795
1803
|
await mkdir(paths.buildContext, { recursive: true });
|
|
1796
|
-
await
|
|
1804
|
+
await refreshBuildContextAssets(paths.buildContext, buildContextAssets);
|
|
1797
1805
|
await Promise.all([
|
|
1798
1806
|
writeFile(join(paths.buildContext, "Dockerfile"), dockerfile),
|
|
1799
1807
|
writeFile(join(paths.buildContext, ".dockerignore"), renderDockerignore(buildContextAssets, agent.dockerignoreEntries)),
|
|
@@ -1890,15 +1898,13 @@ function renderDockerignore(assets, additionalEntries) {
|
|
|
1890
1898
|
...additionalEntries
|
|
1891
1899
|
].join("\n")}\n`;
|
|
1892
1900
|
}
|
|
1893
|
-
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 });
|
|
1894
1904
|
await Promise.all(assets.map(async (asset) => {
|
|
1895
1905
|
const destination = join(buildContext, asset.relativePath);
|
|
1896
1906
|
await mkdir(dirname(destination), { recursive: true });
|
|
1897
|
-
|
|
1898
|
-
await writeFile(destination, asset.content, { flag: "wx" });
|
|
1899
|
-
} catch (error) {
|
|
1900
|
-
if (typeof error !== "object" || error === null || !("code" in error) || error.code !== "EEXIST") throw error;
|
|
1901
|
-
}
|
|
1907
|
+
await writeFile(destination, asset.content);
|
|
1902
1908
|
}));
|
|
1903
1909
|
}
|
|
1904
1910
|
function renderEntrypoint(input) {
|
|
@@ -1989,7 +1995,7 @@ function createCli(signal, interactive) {
|
|
|
1989
1995
|
outro("Project is already registered.");
|
|
1990
1996
|
return success({});
|
|
1991
1997
|
}
|
|
1992
|
-
outro(`Next: run ${command("devbox build")} to prepare your workspace, then ${command("devbox up")} to start it.`);
|
|
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.`);
|
|
1993
1999
|
return success({});
|
|
1994
2000
|
});
|
|
1995
2001
|
cli.command("config", "Edit Global or Local configuration for the current Project.").action(async () => {
|