@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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.mjs +406 -400
  3. 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 once after setup or whenever you change global configuration:
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 { parse as parse$1, parseDocument, stringify } from "yaml";
11
+ import { createHash, randomUUID } from "node:crypto";
12
12
  import { once } from "node:events";
13
13
  //#region package.json
14
- var version = "0.7.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 = [
@@ -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$1(error)) devboxHomeExisted = false;
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$1(error) {
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
- let identity;
723
- let name;
724
- if (registration !== void 0) {
725
- identity = registration.identity;
726
- name = registration.name;
727
- } else {
728
- const allocatedIdentity = await allocateSandboxIdentity(projectRoot, paths, registry);
729
- if (!allocatedIdentity.ok) return allocatedIdentity;
730
- const allocatedName = allocateSandboxName(projectRoot, registry);
731
- if (!allocatedName.ok) return allocatedName;
732
- identity = allocatedIdentity.value;
733
- name = allocatedName.value;
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
- let localChanged = false;
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 composeWrite = await composeStateWrite({
788
- projectRoot,
789
- sandboxName: name,
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 (!composeWrite.ok) return composeWrite;
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 writeStateTransaction([
827
- ...globalChanged ? [{
828
- path: paths.globalConfiguration,
829
- content: serializeGlobalConfiguration(globalConfiguration),
830
- previous: globalState.value
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
- composeWrite.value
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
- if (configurationsEqual(localCheck.value, nextCheck.value)) return success({
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 composeWrite = await composeStateWrite({
907
- projectRoot,
908
- sandboxName: registration.name,
909
- stateDirectory,
1094
+ const written = await publishProjectState({
910
1095
  devboxHome: paths.home,
911
1096
  globalConfiguration: globalCheck.value,
912
- localConfiguration: nextCheck.value
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: true
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
- if (!(!globalState.value.exists || !configurationsEqual(currentConfiguration, nextCheck.value)) && replacementConfigurations.size === 0) return success({
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 composeWrites = [];
1010
- for (const [root, registration] of Object.entries(registryCheck.value.projects)) {
1011
- const composeWrite = await composeStateWrite({
1012
- projectRoot: root,
1013
- sandboxName: registration.name,
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
- devboxHome: paths.home,
1016
- globalConfiguration: nextCheck.value,
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
- ...composeWrites
1037
- ]);
1203
+ } : void 0
1204
+ }))
1205
+ });
1038
1206
  if (!written.ok) return written;
1039
1207
  return success({
1040
1208
  scope: "global",
1041
- changed: true
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 globalState = await readOptionalFile(paths.globalConfiguration);
1126
- if (!globalState.ok) return globalState;
1127
- if (!globalState.value.exists) return missingConfiguration("global", paths.globalConfiguration);
1128
- const globalCheck = parseGlobalConfiguration(globalState.value.content);
1129
- if (!globalCheck.ok) return globalCheck;
1130
- await ensureSharedAgentVolumes(globalCheck.value.agent, environment);
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
- relativePath: "AGENTS.md",
1601
- content: AGENTS_md_default
1602
- });
1603
- if (agents.some((agent) => agent.name === "claude-code")) assets.push({
1604
- relativePath: ".claude/settings.json",
1605
- content: settings_json_default
1606
- });
1607
- if (agents.some((agent) => agent.name === "omp")) assets.push({
1608
- relativePath: ".omp/agent/config.yml",
1609
- content: config_yml_default
1610
- });
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 seedBuildContextAssets(paths.buildContext, buildContextAssets);
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 seedBuildContextAssets(buildContext, assets) {
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
- try {
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 () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycs77/devbox",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "A Lucas Yang's opinionated devbox for AI agents",
5
5
  "keywords": [
6
6
  "agent",