@ycs77/devbox 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs ADDED
@@ -0,0 +1,1998 @@
1
+ import * as p from "@clack/prompts";
2
+ import { intro, outro } from "@clack/prompts";
3
+ import { cac } from "cac";
4
+ import c from "picocolors";
5
+ import { createHash, randomUUID } from "node:crypto";
6
+ import { lstat, mkdir, readFile, rename, rm, rmdir, writeFile } from "node:fs/promises";
7
+ import { homedir } from "node:os";
8
+ import { basename, dirname, isAbsolute, join, parse, relative, sep } from "node:path";
9
+ import { execFile, spawn } from "node:child_process";
10
+ import { promisify } from "node:util";
11
+ import { parse as parse$1, parseDocument, stringify } from "yaml";
12
+ import { once } from "node:events";
13
+ //#region src/catalog/index.ts
14
+ const NODE_RELEASE_KEYS = [
15
+ "5BE8A3F6C8A5C01D106C0AD820B1A390B168D356",
16
+ "DD792F5973C6DE52C432CBDAC77ABFA00DDBF2B7",
17
+ "CC68F5A3106FF448322E48ED27F5E38D5B0A215F",
18
+ "8FCCA13FEF1D0C2E91008E09770F7A9A5AE15600",
19
+ "890C08DB8579162FEE0DF9DB8BEAB4DFCF555EF4",
20
+ "C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C",
21
+ "108F52B48DB57BB0CC439B2997B01419BD92F80A",
22
+ "A363A499291CBBC940DD62E41F10027AF002F8B0",
23
+ "655F3B5C1FB3FA8D1A0CA6BDE4A7D232B936D2FD"
24
+ ];
25
+ const PACKAGED_NODE_RECIPES = {
26
+ "24": {
27
+ releaseLine: "24",
28
+ version: "24.19.0",
29
+ runtimeRoot: "/opt/devbox/runtimes/node/24",
30
+ trustedReleaseKeys: NODE_RELEASE_KEYS
31
+ },
32
+ "22": {
33
+ releaseLine: "22",
34
+ version: "22.23.2",
35
+ runtimeRoot: "/opt/devbox/runtimes/node/22",
36
+ trustedReleaseKeys: NODE_RELEASE_KEYS
37
+ }
38
+ };
39
+ const PACKAGED_NODE_RELEASE_LINES = ["24", "22"];
40
+ const PACKAGED_AGENTS = {
41
+ "claude-code": {
42
+ home: {
43
+ volumeName: "devbox-claude",
44
+ target: "/home/devbox/.claude"
45
+ },
46
+ installation: {
47
+ url: "https://claude.ai/install.sh",
48
+ shell: "bash"
49
+ },
50
+ supportsNotifications: true,
51
+ supportsSkillInstallation: true
52
+ },
53
+ codex: {
54
+ home: {
55
+ volumeName: "devbox-codex",
56
+ target: "/home/devbox/.codex"
57
+ },
58
+ installation: {
59
+ url: "https://chatgpt.com/codex/install.sh",
60
+ shell: "sh"
61
+ },
62
+ supportsNotifications: true,
63
+ supportsSkillInstallation: true
64
+ },
65
+ agy: {
66
+ home: {
67
+ volumeName: "devbox-agy",
68
+ target: "/home/devbox/.gemini"
69
+ },
70
+ installation: {
71
+ url: "https://antigravity.google/cli/install.sh",
72
+ shell: "bash"
73
+ },
74
+ supportsNotifications: false,
75
+ supportsSkillInstallation: false
76
+ },
77
+ omp: {
78
+ home: {
79
+ volumeName: "devbox-omp",
80
+ target: "/home/devbox/.omp"
81
+ },
82
+ installation: {
83
+ url: "https://omp.sh/install",
84
+ shell: "sh"
85
+ },
86
+ supportsNotifications: true,
87
+ supportsSkillInstallation: false
88
+ }
89
+ };
90
+ const PACKAGED_RUNTIME_CATALOG = {
91
+ runtimes: { node: PACKAGED_NODE_RELEASE_LINES },
92
+ agents: Object.keys(PACKAGED_AGENTS)
93
+ };
94
+ //#endregion
95
+ //#region src/result.ts
96
+ function success(value) {
97
+ return {
98
+ ok: true,
99
+ value
100
+ };
101
+ }
102
+ function failure(error) {
103
+ return {
104
+ ok: false,
105
+ error
106
+ };
107
+ }
108
+ //#endregion
109
+ //#region src/host.ts
110
+ const execFile$1 = promisify(execFile);
111
+ var HostCommandError = class extends Error {
112
+ exitCode;
113
+ constructor(file, args, exitCode) {
114
+ super(`${file} ${args.join(" ")} exited with status ${exitCode}.`);
115
+ this.exitCode = exitCode;
116
+ this.name = "HostCommandError";
117
+ }
118
+ };
119
+ const supportedHostAction = "Start Docker Desktop and ensure Docker Compose is available.";
120
+ function currentHostEnvironment() {
121
+ return {
122
+ run: async (file, args) => {
123
+ await execFile$1(file, args);
124
+ },
125
+ runDirect: async (file, args) => new Promise((resolve, reject) => {
126
+ const command = spawn(file, args, { stdio: "inherit" });
127
+ command.once("error", reject);
128
+ command.once("close", (code) => {
129
+ if (code === 0) {
130
+ resolve();
131
+ return;
132
+ }
133
+ reject(new HostCommandError(file, args, code ?? 1));
134
+ });
135
+ })
136
+ };
137
+ }
138
+ async function validateSupportedHost(environment = currentHostEnvironment()) {
139
+ for (const [file, args, description] of [[
140
+ "docker",
141
+ ["--version"],
142
+ "Docker"
143
+ ], [
144
+ "docker",
145
+ ["compose", "version"],
146
+ "Docker Compose"
147
+ ]]) try {
148
+ await environment.run(file, args);
149
+ } catch {
150
+ return failure({
151
+ kind: "validation",
152
+ code: "missing-host-prerequisite",
153
+ observed: `${description} is unavailable.`,
154
+ nextAction: supportedHostAction
155
+ });
156
+ }
157
+ return success(void 0);
158
+ }
159
+ //#endregion
160
+ //#region src/agent/volumes.ts
161
+ async function ensureSharedAgentVolumes(configuredAgents, environment = currentHostEnvironment()) {
162
+ for (const configuredAgent of configuredAgents) {
163
+ const volumeName = PACKAGED_AGENTS[configuredAgent]?.home.volumeName;
164
+ if (volumeName !== void 0) await environment.run("docker", [
165
+ "volume",
166
+ "create",
167
+ "--driver",
168
+ "local",
169
+ volumeName
170
+ ]);
171
+ }
172
+ }
173
+ //#endregion
174
+ //#region src/configuration/index.ts
175
+ function normalizeCatalog(catalog = PACKAGED_RUNTIME_CATALOG) {
176
+ if (!isRecord(catalog.runtimes) || !Array.isArray(catalog.agents)) return invalid$1("invalid-runtime-catalog", "The packaged Runtime catalog is invalid.");
177
+ const runtimes = {};
178
+ for (const [family, entries] of Object.entries(catalog.runtimes)) {
179
+ if (!isCatalogName(family) || !Array.isArray(entries) || entries.length === 0) return invalid$1("invalid-runtime-catalog", "The packaged Runtime catalog is invalid.");
180
+ const normalizedEntries = [];
181
+ for (const entry of entries) {
182
+ if (typeof entry !== "string" || entry.length === 0 || normalizedEntries.includes(entry)) return invalid$1("invalid-runtime-catalog", "The packaged Runtime catalog is invalid.");
183
+ normalizedEntries.push(entry);
184
+ }
185
+ runtimes[family] = normalizedEntries;
186
+ }
187
+ const agents = [];
188
+ for (const agent of catalog.agents) {
189
+ if (typeof agent !== "string" || !isCatalogName(agent) || agents.includes(agent)) return invalid$1("invalid-runtime-catalog", "The packaged Agent catalog is invalid.");
190
+ agents.push(agent);
191
+ }
192
+ return success({
193
+ runtimes,
194
+ agents
195
+ });
196
+ }
197
+ function defaultGlobalConfiguration(catalog = PACKAGED_RUNTIME_CATALOG) {
198
+ return {
199
+ version: 1,
200
+ node: catalog.runtimes.node?.[0] === void 0 ? [] : [catalog.runtimes.node[0]],
201
+ agent: [],
202
+ agent_notifications: true
203
+ };
204
+ }
205
+ function defaultLocalConfiguration(globalConfiguration) {
206
+ return {
207
+ version: 1,
208
+ node: globalConfiguration.node[0] ?? null
209
+ };
210
+ }
211
+ function parseGlobalConfiguration(source, catalog = PACKAGED_RUNTIME_CATALOG) {
212
+ const parsed = parseYaml(source);
213
+ if (!parsed.ok) return invalid$1("invalid-global-configuration", parsed.observed);
214
+ const document = parsed.value;
215
+ if (!isRecord(document) || !hasExactKeys$1(document, [
216
+ "version",
217
+ "node",
218
+ "agent",
219
+ "agent_notifications"
220
+ ])) return invalid$1("invalid-global-configuration", "Global configuration contains unknown or missing fields.");
221
+ if (document.version !== 1) return invalid$1("invalid-global-configuration", "Global configuration must use version: 1.");
222
+ if (!Array.isArray(document.node) || !Array.isArray(document.agent)) return invalid$1("invalid-global-configuration", "Global configuration has invalid values.");
223
+ if (typeof document.agent_notifications !== "boolean") return invalid$1("invalid-global-configuration", "Global configuration agent_notifications must be true or false.");
224
+ const node = [];
225
+ for (const rawEntry of document.node) {
226
+ const entry = runtimeLine(rawEntry);
227
+ if (entry === void 0 || !catalog.runtimes.node?.includes(entry)) return invalid$1("invalid-global-configuration", `Node Runtime is not in the packaged catalog: ${String(rawEntry)}.`);
228
+ if (node.includes(entry)) return invalid$1("invalid-global-configuration", `Node Runtime is duplicated: ${entry}.`);
229
+ node.push(entry);
230
+ }
231
+ const agent = [];
232
+ for (const rawAgent of document.agent) {
233
+ if (typeof rawAgent !== "string" || !catalog.agents.includes(rawAgent)) return invalid$1("invalid-global-configuration", `Agent is not in the packaged catalog: ${String(rawAgent)}.`);
234
+ if (agent.includes(rawAgent)) return invalid$1("invalid-global-configuration", `Agent is duplicated: ${rawAgent}.`);
235
+ agent.push(rawAgent);
236
+ }
237
+ return success({
238
+ version: 1,
239
+ node,
240
+ agent,
241
+ agent_notifications: document.agent_notifications
242
+ });
243
+ }
244
+ function parseLocalConfiguration(source, globalConfiguration, catalog = PACKAGED_RUNTIME_CATALOG) {
245
+ const parsed = parseYaml(source);
246
+ if (!parsed.ok) return invalid$1("invalid-local-configuration", parsed.observed);
247
+ const document = parsed.value;
248
+ if (!isRecord(document) || !hasExactKeys$1(document, ["version", "node"])) return invalid$1("invalid-local-configuration", "Local configuration contains unknown or missing fields.");
249
+ if (document.version !== 1) return invalid$1("invalid-local-configuration", "Local configuration must use version: 1.");
250
+ const node = runtimeLineOrNull(document.node);
251
+ if (node === void 0) return invalid$1("invalid-local-configuration", `Node Runtime is invalid: ${String(document.node)}.`);
252
+ if (node !== null && !catalog.runtimes.node?.includes(node)) return invalid$1("invalid-local-configuration", `Node Runtime is not in the packaged catalog: ${String(document.node)}.`);
253
+ if (node !== null && !globalConfiguration.node.includes(node)) return invalid$1("unconfigured-runtime-selection", `Local configuration selects a Node Runtime that is not configured globally: ${node}.`);
254
+ return success({
255
+ version: 1,
256
+ node
257
+ });
258
+ }
259
+ function serializeGlobalConfiguration(configuration) {
260
+ return stringify({
261
+ version: 1,
262
+ node: [...configuration.node],
263
+ agent: [...configuration.agent],
264
+ agent_notifications: configuration.agent_notifications
265
+ });
266
+ }
267
+ function serializeLocalConfiguration(configuration) {
268
+ return stringify({
269
+ version: 1,
270
+ node: configuration.node
271
+ });
272
+ }
273
+ function configurationsEqual(left, right) {
274
+ return JSON.stringify(left) === JSON.stringify(right);
275
+ }
276
+ function parseYaml(source) {
277
+ try {
278
+ const document = parseDocument(source, {
279
+ strict: true,
280
+ uniqueKeys: true,
281
+ version: "1.2"
282
+ });
283
+ if (document.errors.length > 0) return {
284
+ ok: false,
285
+ observed: document.errors[0]?.message ?? "YAML is invalid."
286
+ };
287
+ return {
288
+ ok: true,
289
+ value: document.toJS()
290
+ };
291
+ } catch (error) {
292
+ return {
293
+ ok: false,
294
+ observed: error instanceof Error ? error.message : "YAML is invalid."
295
+ };
296
+ }
297
+ }
298
+ function runtimeLine(value) {
299
+ if (typeof value === "string" && value.length > 0) return value;
300
+ if (typeof value === "number" && Number.isInteger(value)) return String(value);
301
+ }
302
+ function runtimeLineOrNull(value) {
303
+ if (value === null) return null;
304
+ return runtimeLine(value);
305
+ }
306
+ function isRecord(value) {
307
+ if (typeof value !== "object" || value === null) return false;
308
+ const prototype = Object.getPrototypeOf(value);
309
+ return prototype === Object.prototype || prototype === null;
310
+ }
311
+ function hasExactKeys$1(value, keys) {
312
+ const expected = new Set(keys);
313
+ const actual = Object.keys(value);
314
+ return actual.length === expected.size && actual.every((key) => expected.has(key));
315
+ }
316
+ function isCatalogName(value) {
317
+ return /^[a-z][a-z0-9-]*$/.test(value);
318
+ }
319
+ function invalid$1(code, observed) {
320
+ return failure({
321
+ kind: "validation",
322
+ code,
323
+ observed,
324
+ nextAction: "Edit the supported version-1 YAML configuration and try again."
325
+ });
326
+ }
327
+ //#endregion
328
+ //#region src/state-lock/index.ts
329
+ var StateLockInterruptedError = class extends Error {
330
+ constructor() {
331
+ super("State lock acquisition interrupted.");
332
+ this.name = "StateLockInterruptedError";
333
+ }
334
+ };
335
+ var StateLockBusyError = class extends Error {
336
+ scope;
337
+ projectRoot;
338
+ constructor(scope, projectRoot) {
339
+ super("A required Devbox command marker is already present.");
340
+ this.name = "StateLockBusyError";
341
+ this.scope = scope;
342
+ this.projectRoot = projectRoot;
343
+ }
344
+ };
345
+ async function withStateLocks(input, operation) {
346
+ const lockRoot = join(input.devboxHome, "locks");
347
+ const targets = input.global ? [{
348
+ path: join(lockRoot, "global"),
349
+ scope: "global"
350
+ }] : [];
351
+ targets.push(...projectLockTargets(lockRoot, input.projectRoots));
352
+ let devboxHomeExisted = true;
353
+ try {
354
+ await lstat(input.devboxHome);
355
+ } catch (error) {
356
+ if (isMissingFileError$1(error)) devboxHomeExisted = false;
357
+ else throw error;
358
+ }
359
+ const acquired = [];
360
+ const acquireTargets = async (targetsToAcquire) => {
361
+ for (const target of targetsToAcquire) {
362
+ if (input.signal?.aborted) throw new StateLockInterruptedError();
363
+ await acquireLock(target);
364
+ acquired.push(target.path);
365
+ }
366
+ };
367
+ try {
368
+ if (input.signal?.aborted) throw new StateLockInterruptedError();
369
+ await mkdir(lockRoot, {
370
+ recursive: true,
371
+ mode: 448
372
+ });
373
+ await acquireTargets(targets);
374
+ if (input.signal?.aborted) throw new StateLockInterruptedError();
375
+ return await operation({ acquireProjectScopes: async (projectRoots) => {
376
+ await acquireTargets(projectLockTargets(lockRoot, projectRoots));
377
+ } });
378
+ } catch (error) {
379
+ if (error instanceof StateLockBusyError) return failure({
380
+ kind: "operational",
381
+ code: "command-lock-busy",
382
+ observed: error.scope === "global" ? "The Global command marker is already occupied." : `The Project command marker is already occupied for ${error.projectRoot}.`,
383
+ nextAction: `Wait for the other Devbox command to finish and try again. If no Devbox process is running, remove the residual marker under ${input.devboxHome}/locks/ and try again.`
384
+ });
385
+ throw error;
386
+ } finally {
387
+ for (const lockPath of acquired.reverse()) await rm(lockPath, {
388
+ recursive: true,
389
+ force: true
390
+ }).catch(() => void 0);
391
+ await rmdir(lockRoot).catch(() => void 0);
392
+ if (!devboxHomeExisted) await rmdir(input.devboxHome).catch(() => void 0);
393
+ }
394
+ }
395
+ function projectLockTargets(lockRoot, projectRoots) {
396
+ const targets = [];
397
+ const sortedRoots = [...new Set(projectRoots)].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
398
+ for (const projectRoot of sortedRoots) {
399
+ const hash = createHash("sha256").update(projectRoot).digest("hex");
400
+ targets.push({
401
+ path: join(lockRoot, `project-${hash}`),
402
+ scope: "project",
403
+ projectRoot
404
+ });
405
+ }
406
+ return targets;
407
+ }
408
+ async function acquireLock(target) {
409
+ try {
410
+ await mkdir(target.path, { mode: 448 });
411
+ } catch (error) {
412
+ if (isFileExistsError(error)) throw new StateLockBusyError(target.scope, target.projectRoot);
413
+ throw error;
414
+ }
415
+ }
416
+ function isFileExistsError(error) {
417
+ return typeof error === "object" && error !== null && error.code === "EEXIST";
418
+ }
419
+ function isMissingFileError$1(error) {
420
+ return typeof error === "object" && error !== null && error.code === "ENOENT";
421
+ }
422
+ //#endregion
423
+ //#region src/workspace/image.ts
424
+ const WORKSPACE_IMAGE = "devbox-workspace:latest";
425
+ //#endregion
426
+ //#region src/project/compose-fragments.ts
427
+ const NOTIFICATION_FRAGMENT = {
428
+ environment: { PULSE_SERVER: "unix:/tmp/pulse-socket" },
429
+ volume: {
430
+ source: "/mnt/wslg/runtime-dir/pulse/native",
431
+ target: "/tmp/pulse-socket",
432
+ readOnly: true
433
+ }
434
+ };
435
+ function selectComposeFragments({ selectedNode, configuredAgents, agentNotifications }) {
436
+ const agents = [];
437
+ let hasNotificationAgent = false;
438
+ for (const agent of configuredAgents) {
439
+ const packagedAgent = PACKAGED_AGENTS[agent];
440
+ if (packagedAgent !== void 0) {
441
+ agents.push({
442
+ agent,
443
+ volume: {
444
+ name: packagedAgent.home.volumeName,
445
+ target: packagedAgent.home.target,
446
+ external: true
447
+ }
448
+ });
449
+ hasNotificationAgent ||= packagedAgent.supportsNotifications;
450
+ }
451
+ }
452
+ return {
453
+ node: selectedNode === null ? void 0 : { environment: { NODE_VERSION: selectedNode } },
454
+ agents,
455
+ notification: agentNotifications && hasNotificationAgent ? NOTIFICATION_FRAGMENT : void 0
456
+ };
457
+ }
458
+ //#endregion
459
+ //#region src/project/compose.ts
460
+ function renderProjectCompose(input) {
461
+ const fragments = selectComposeFragments(input);
462
+ const environment = {
463
+ ...fragments.node?.environment,
464
+ ...fragments.notification?.environment
465
+ };
466
+ const volumes = [
467
+ {
468
+ type: "bind",
469
+ source: input.projectRoot,
470
+ target: `/workspace/${input.sandboxName}`
471
+ },
472
+ ...fragments.agents.map((fragment) => ({
473
+ type: "volume",
474
+ source: fragment.volume.name,
475
+ target: fragment.volume.target
476
+ })),
477
+ ...input.claudeHostConfiguration === void 0 ? [] : [{
478
+ type: "bind",
479
+ source: input.claudeHostConfiguration,
480
+ target: "/home/devbox/.claude.json"
481
+ }],
482
+ ...fragments.notification === void 0 ? [] : [{
483
+ type: "bind",
484
+ source: fragments.notification.volume.source,
485
+ target: fragments.notification.volume.target,
486
+ read_only: fragments.notification.volume.readOnly
487
+ }]
488
+ ];
489
+ const definition = {
490
+ name: input.sandboxName,
491
+ "x-devbox": {
492
+ version: 1,
493
+ project_root: input.projectRoot,
494
+ compose_name: input.sandboxName,
495
+ sandbox_service: "devbox"
496
+ },
497
+ services: { devbox: {
498
+ image: WORKSPACE_IMAGE,
499
+ container_name: input.sandboxName,
500
+ working_dir: `/workspace/${input.sandboxName}`,
501
+ ...Object.keys(environment).length === 0 ? {} : { environment },
502
+ volumes
503
+ } },
504
+ ...fragments.agents.length === 0 ? {} : { volumes: Object.fromEntries(fragments.agents.map((fragment) => [fragment.volume.name, {
505
+ name: fragment.volume.name,
506
+ external: fragment.volume.external
507
+ }])) }
508
+ };
509
+ const source = stringify(definition);
510
+ return validatesProjectCompose(source, input) ? success(source) : failure({
511
+ kind: "operational",
512
+ code: "invalid-project-compose",
513
+ observed: `Devbox generated an invalid Compose definition for Project ${input.projectRoot}.`,
514
+ nextAction: "Run the command again. If the problem continues, report it as a Devbox bug."
515
+ });
516
+ }
517
+ function validatesProjectCompose(source, input) {
518
+ try {
519
+ const definition = parse$1(source);
520
+ if (definition.name !== input.sandboxName) return false;
521
+ const metadata = definition["x-devbox"];
522
+ if (metadata.version !== 1 || metadata.project_root !== input.projectRoot || metadata.compose_name !== input.sandboxName || metadata.sandbox_service !== "devbox") return false;
523
+ const service = definition.services.devbox;
524
+ if (service.image !== "devbox-workspace:latest" || service.container_name !== input.sandboxName || service.working_dir !== `/workspace/${input.sandboxName}` || "build" in service) return false;
525
+ return input.selectedNode === null && service.environment?.NODE_VERSION === void 0 || input.selectedNode !== null && service.environment?.NODE_VERSION === input.selectedNode;
526
+ } catch {
527
+ return false;
528
+ }
529
+ }
530
+ //#endregion
531
+ //#region src/project/registry.ts
532
+ function parseProjectRegistry(source) {
533
+ let document;
534
+ try {
535
+ const parsed = parseDocument(source, {
536
+ strict: true,
537
+ uniqueKeys: true,
538
+ version: "1.2"
539
+ });
540
+ if (parsed.errors.length > 0) return invalid(parsed.errors[0]?.message ?? "Project registry YAML is invalid.");
541
+ document = parsed.toJS();
542
+ } catch (error) {
543
+ return invalid(error instanceof Error ? error.message : "Project registry YAML is invalid.");
544
+ }
545
+ if (!isPlainObject(document) || !hasExactKeys(document, ["version", "projects"]) || document.version !== 1 || !isPlainObject(document.projects)) return invalid("Project registry must contain only version: 1 and projects.");
546
+ const projects = {};
547
+ const assignedIdentities = /* @__PURE__ */ new Set();
548
+ const assignedNames = /* @__PURE__ */ new Set();
549
+ for (const [root, registration] of Object.entries(document.projects)) {
550
+ if (!isAbsolute(root)) return invalid(`Project root must be an exact absolute path: ${root}.`);
551
+ if (!isPlainObject(registration) || !hasExactKeys(registration, ["identity", "name"]) || typeof registration.identity !== "string" || !isSafeStateDirectoryName(registration.identity) || assignedIdentities.has(registration.identity)) return invalid("Sandbox identity assignment is invalid.");
552
+ if (typeof registration.name !== "string" || !isSafeSandboxName(registration.name) || assignedNames.has(registration.name)) return invalid("Sandbox name assignment is invalid.");
553
+ assignedIdentities.add(registration.identity);
554
+ assignedNames.add(registration.name);
555
+ projects[root] = {
556
+ identity: registration.identity,
557
+ name: registration.name
558
+ };
559
+ }
560
+ return success({
561
+ version: 1,
562
+ projects
563
+ });
564
+ }
565
+ function serializeProjectRegistry(registry) {
566
+ const projects = Object.fromEntries(Object.entries(registry.projects).sort(([left], [right]) => {
567
+ if (left < right) return -1;
568
+ if (left > right) return 1;
569
+ return 0;
570
+ }));
571
+ return stringify({
572
+ version: 1,
573
+ projects
574
+ });
575
+ }
576
+ function isSafeStateDirectoryName(name) {
577
+ return /^(?:[A-Za-z0-9_%~-]+)(?:-[2-9][0-9]*)?$/.test(name) && name !== "." && name !== "..";
578
+ }
579
+ function isSafeSandboxName(name) {
580
+ return /^[a-z0-9][a-z0-9_-]+$/.test(name);
581
+ }
582
+ function hasExactKeys(value, keys) {
583
+ const expected = new Set(keys);
584
+ const actual = Object.keys(value);
585
+ return actual.length === expected.size && actual.every((key) => expected.has(key));
586
+ }
587
+ function isPlainObject(value) {
588
+ if (typeof value !== "object" || value === null) return false;
589
+ const prototype = Object.getPrototypeOf(value);
590
+ return prototype === Object.prototype || prototype === null;
591
+ }
592
+ function invalid(observed) {
593
+ return failure({
594
+ kind: "validation",
595
+ code: "invalid-project-registry",
596
+ observed,
597
+ nextAction: "Repair ~/.devbox/projects.yaml using the supported version-1 registry schema."
598
+ });
599
+ }
600
+ //#endregion
601
+ //#region src/project/index.ts
602
+ function devboxPaths(devboxHome = join(homedir(), ".devbox")) {
603
+ return {
604
+ home: devboxHome,
605
+ globalConfiguration: join(devboxHome, "config.yaml"),
606
+ projectRegistry: join(devboxHome, "projects.yaml"),
607
+ projects: join(devboxHome, "projects"),
608
+ buildContext: join(devboxHome, "build"),
609
+ claudeHostConfiguration: join(devboxHome, "agents", "claude", ".claude.json")
610
+ };
611
+ }
612
+ function sandboxIdentity(projectRoot) {
613
+ const root = parse(projectRoot).root;
614
+ const pathWithinRoot = relative(root, projectRoot);
615
+ if (!isAbsolute(projectRoot) || pathWithinRoot === ".." || pathWithinRoot.startsWith(`..${sep}`)) throw new TypeError("Project root must be an absolute path.");
616
+ return pathWithinRoot === "" ? "root" : pathWithinRoot.split(sep).map(escapePathSegment).join("-");
617
+ }
618
+ function projectStateDirectory(sandboxIdentity, devboxHome) {
619
+ if (!isSafeStateDirectoryName(sandboxIdentity)) throw new TypeError("Sandbox identity must be a safe state directory name.");
620
+ return join(devboxHome, "projects", sandboxIdentity);
621
+ }
622
+ function sandboxName(projectRoot) {
623
+ const normalized = basename(projectRoot).toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^[^a-z0-9]+/, "").replace(/-+$/, "");
624
+ return normalized.length < 2 ? `project-${normalized || "root"}` : normalized;
625
+ }
626
+ function escapePathSegment(segment) {
627
+ const bytes = new TextEncoder().encode(segment);
628
+ let escaped = "";
629
+ for (const byte of bytes) if (byte >= 65 && byte <= 90 || byte >= 97 && byte <= 122 || byte >= 48 && byte <= 57 || byte === 45 || byte === 95) escaped += String.fromCharCode(byte);
630
+ else escaped += `%${byte.toString(16).toUpperCase().padStart(2, "0")}`;
631
+ return escaped;
632
+ }
633
+ async function initializeProject(input = {}) {
634
+ return withStateLocks({
635
+ devboxHome: input.devboxHome ?? join(homedir(), ".devbox"),
636
+ global: true,
637
+ projectRoots: [input.root ?? process.cwd()],
638
+ signal: input.signal
639
+ }, () => initializeProjectUnlocked(input));
640
+ }
641
+ async function initializeProjectUnlocked(input = {}) {
642
+ const projectRoot = input.root ?? process.cwd();
643
+ const rootCheck = await validateProjectRoot(projectRoot);
644
+ if (!rootCheck.ok) return rootCheck;
645
+ if (input.signal?.aborted) throw new InterruptedError();
646
+ const hostCheck = await (input.validateHost ?? validateSupportedHost)();
647
+ if (!hostCheck.ok) return hostCheck;
648
+ if (input.signal?.aborted) throw new InterruptedError();
649
+ const catalogCheck = normalizeCatalog(input.catalog);
650
+ if (!catalogCheck.ok) return catalogCheck;
651
+ const catalog = catalogCheck.value;
652
+ const paths = devboxPaths(input.devboxHome);
653
+ const registryState = await readOptionalFile(paths.projectRegistry);
654
+ if (!registryState.ok) return registryState;
655
+ const registryCheck = registryState.value.exists ? parseProjectRegistry(registryState.value.content) : success({
656
+ version: 1,
657
+ projects: {}
658
+ });
659
+ if (!registryCheck.ok) return registryCheck;
660
+ const registry = registryCheck.value;
661
+ const registration = registry.projects[projectRoot];
662
+ let identity;
663
+ let name;
664
+ if (registration !== void 0) {
665
+ identity = registration.identity;
666
+ name = registration.name;
667
+ } else {
668
+ const allocatedIdentity = await allocateSandboxIdentity(projectRoot, paths, registry);
669
+ if (!allocatedIdentity.ok) return allocatedIdentity;
670
+ const allocatedName = allocateSandboxName(projectRoot, registry);
671
+ if (!allocatedName.ok) return allocatedName;
672
+ identity = allocatedIdentity.value;
673
+ name = allocatedName.value;
674
+ }
675
+ const stateDirectory = projectStateDirectory(identity, paths.home);
676
+ const registered = registration !== void 0;
677
+ const globalState = await readOptionalFile(paths.globalConfiguration);
678
+ if (!globalState.ok) return globalState;
679
+ if (registered && !globalState.value.exists) return missingConfiguration("global", paths.globalConfiguration);
680
+ let globalConfiguration;
681
+ let globalChanged = false;
682
+ if (globalState.value.exists) {
683
+ const parsedGlobal = parseGlobalConfiguration(globalState.value.content, catalog);
684
+ if (!parsedGlobal.ok) return parsedGlobal;
685
+ globalConfiguration = parsedGlobal.value;
686
+ } else if (input.initialGlobalConfiguration !== void 0) {
687
+ const initialGlobal = validateGlobalObject(input.initialGlobalConfiguration, catalog);
688
+ if (!initialGlobal.ok) return initialGlobal;
689
+ globalConfiguration = initialGlobal.value;
690
+ globalChanged = true;
691
+ } else {
692
+ globalConfiguration = defaultGlobalConfiguration(catalog);
693
+ if (input.prompt?.editGlobal) {
694
+ const checkedGlobal = validateGlobalObject(await input.prompt.editGlobal(globalConfiguration, catalog), catalog);
695
+ if (!checkedGlobal.ok) return checkedGlobal;
696
+ globalConfiguration = checkedGlobal.value;
697
+ }
698
+ globalChanged = true;
699
+ }
700
+ const localPath = join(stateDirectory, "config.yaml");
701
+ const localState = registered ? await readOptionalFile(localPath) : success({
702
+ exists: false,
703
+ content: ""
704
+ });
705
+ if (!localState.ok) return localState;
706
+ if (registered && !localState.value.exists) return missingConfiguration("local", localPath);
707
+ let localConfiguration;
708
+ let localChanged = false;
709
+ if (localState.value.exists) {
710
+ const parsedLocal = parseLocalConfiguration(localState.value.content, globalConfiguration, catalog);
711
+ if (!parsedLocal.ok) return parsedLocal;
712
+ localConfiguration = parsedLocal.value;
713
+ } else if (input.initialLocalConfiguration !== void 0) {
714
+ const initialLocal = validateLocalObject(input.initialLocalConfiguration, globalConfiguration, catalog);
715
+ if (!initialLocal.ok) return initialLocal;
716
+ localConfiguration = initialLocal.value;
717
+ localChanged = true;
718
+ } else {
719
+ localConfiguration = defaultLocalConfiguration(globalConfiguration);
720
+ if (input.prompt?.editLocal) {
721
+ const checkedLocal = validateLocalObject(await input.prompt.editLocal(localConfiguration, catalog, globalConfiguration), globalConfiguration, catalog);
722
+ if (!checkedLocal.ok) return checkedLocal;
723
+ localConfiguration = checkedLocal.value;
724
+ }
725
+ localChanged = true;
726
+ }
727
+ const composeWrite = await composeStateWrite({
728
+ projectRoot,
729
+ sandboxName: name,
730
+ stateDirectory,
731
+ devboxHome: paths.home,
732
+ globalConfiguration,
733
+ localConfiguration
734
+ });
735
+ if (!composeWrite.ok) return composeWrite;
736
+ if (registered) {
737
+ if (!composeWrite.value.previous.exists || composeWrite.value.content !== composeWrite.value.previous.content) {
738
+ const written = await writeStateTransaction([composeWrite.value]);
739
+ if (!written.ok) return written;
740
+ }
741
+ return success({
742
+ root: projectRoot,
743
+ stateDirectory,
744
+ sandboxName: name,
745
+ created: false,
746
+ confirmed: true
747
+ });
748
+ }
749
+ if (!await (input.confirm ?? input.prompt?.confirm ?? (async () => true))("Save configuration?")) return success({
750
+ root: projectRoot,
751
+ stateDirectory,
752
+ sandboxName: name,
753
+ created: false,
754
+ confirmed: false
755
+ });
756
+ const nextRegistry = {
757
+ version: 1,
758
+ projects: {
759
+ ...registry.projects,
760
+ [projectRoot]: {
761
+ identity,
762
+ name
763
+ }
764
+ }
765
+ };
766
+ const written = await writeStateTransaction([
767
+ ...globalChanged ? [{
768
+ path: paths.globalConfiguration,
769
+ content: serializeGlobalConfiguration(globalConfiguration),
770
+ previous: globalState.value
771
+ }] : [],
772
+ ...localChanged ? [{
773
+ path: localPath,
774
+ content: serializeLocalConfiguration(localConfiguration),
775
+ previous: localState.value
776
+ }] : [],
777
+ {
778
+ path: paths.projectRegistry,
779
+ content: serializeProjectRegistry(nextRegistry),
780
+ previous: registryState.value
781
+ },
782
+ composeWrite.value
783
+ ]);
784
+ if (!written.ok) return written;
785
+ return success({
786
+ root: projectRoot,
787
+ stateDirectory,
788
+ sandboxName: name,
789
+ created: true,
790
+ confirmed: true
791
+ });
792
+ }
793
+ async function configureLocalProject(input = {}) {
794
+ return withStateLocks({
795
+ devboxHome: input.devboxHome ?? join(homedir(), ".devbox"),
796
+ global: true,
797
+ projectRoots: [input.root ?? process.cwd()],
798
+ signal: input.signal
799
+ }, () => configureLocalProjectUnlocked(input));
800
+ }
801
+ async function configureLocalProjectUnlocked(input = {}) {
802
+ if (input.signal?.aborted) throw new InterruptedError();
803
+ const catalogCheck = normalizeCatalog(input.catalog);
804
+ if (!catalogCheck.ok) return catalogCheck;
805
+ const catalog = catalogCheck.value;
806
+ const projectRoot = input.root ?? process.cwd();
807
+ const paths = devboxPaths(input.devboxHome);
808
+ const registryCheck = await readRegistry(paths.projectRegistry);
809
+ if (!registryCheck.ok) return registryCheck;
810
+ const registration = registryCheck.value.projects[projectRoot];
811
+ if (registration === void 0) return notRegistered(projectRoot);
812
+ const globalState = await readOptionalFile(paths.globalConfiguration);
813
+ if (!globalState.ok) return globalState;
814
+ if (!globalState.value.exists) return missingConfiguration("global", paths.globalConfiguration);
815
+ const globalCheck = parseGlobalConfiguration(globalState.value.content, catalog);
816
+ if (!globalCheck.ok) return globalCheck;
817
+ const stateDirectory = projectStateDirectory(registration.identity, paths.home);
818
+ const localPath = join(stateDirectory, "config.yaml");
819
+ const localState = await readOptionalFile(localPath);
820
+ if (!localState.ok) return localState;
821
+ if (!localState.value.exists) return missingConfiguration("local", localPath);
822
+ const localCheck = parseLocalConfiguration(localState.value.content, globalCheck.value, catalog);
823
+ if (!localCheck.ok) return localCheck;
824
+ let nextConfiguration = input.nextConfiguration ?? localCheck.value;
825
+ if (input.nextConfiguration === void 0 && input.prompt?.editLocal) nextConfiguration = await input.prompt.editLocal(localCheck.value, catalog, globalCheck.value);
826
+ const nextCheck = validateLocalObject(nextConfiguration, globalCheck.value, catalog);
827
+ if (!nextCheck.ok) return nextCheck;
828
+ if (configurationsEqual(localCheck.value, nextCheck.value)) return success({
829
+ scope: "local",
830
+ root: projectRoot,
831
+ changed: false
832
+ });
833
+ if (!await (input.confirm ?? input.prompt?.confirm ?? (async () => true))("Save Project configuration?", {
834
+ title: "Review configuration",
835
+ sections: [{
836
+ title: `Project: ${projectRoot}`,
837
+ current: serializeLocalConfiguration(localCheck.value).trim(),
838
+ next: serializeLocalConfiguration(nextCheck.value).trim()
839
+ }]
840
+ })) return success({
841
+ scope: "local",
842
+ root: projectRoot,
843
+ changed: false
844
+ });
845
+ if (input.signal?.aborted) throw new InterruptedError();
846
+ const composeWrite = await composeStateWrite({
847
+ projectRoot,
848
+ sandboxName: registration.name,
849
+ stateDirectory,
850
+ devboxHome: paths.home,
851
+ globalConfiguration: globalCheck.value,
852
+ localConfiguration: nextCheck.value
853
+ });
854
+ if (!composeWrite.ok) return composeWrite;
855
+ const written = await writeStateTransaction([{
856
+ path: localPath,
857
+ content: serializeLocalConfiguration(nextCheck.value),
858
+ previous: localState.value
859
+ }, composeWrite.value]);
860
+ if (!written.ok) return written;
861
+ return success({
862
+ scope: "local",
863
+ root: projectRoot,
864
+ changed: true
865
+ });
866
+ }
867
+ async function configureGlobal(input = {}) {
868
+ return withStateLocks({
869
+ devboxHome: input.devboxHome ?? join(homedir(), ".devbox"),
870
+ global: true,
871
+ projectRoots: [],
872
+ signal: input.signal
873
+ }, (context) => configureGlobalUnlocked(input, context));
874
+ }
875
+ async function configureGlobalUnlocked(input = {}, locks) {
876
+ if (input.signal?.aborted) throw new InterruptedError();
877
+ const catalogCheck = normalizeCatalog(input.catalog);
878
+ if (!catalogCheck.ok) return catalogCheck;
879
+ const catalog = catalogCheck.value;
880
+ const paths = devboxPaths(input.devboxHome);
881
+ const globalState = await readOptionalFile(paths.globalConfiguration);
882
+ if (!globalState.ok) return globalState;
883
+ let currentConfiguration;
884
+ if (globalState.value.exists) {
885
+ const currentCheck = parseGlobalConfiguration(globalState.value.content, catalog);
886
+ if (!currentCheck.ok) return currentCheck;
887
+ currentConfiguration = currentCheck.value;
888
+ } else currentConfiguration = defaultGlobalConfiguration(catalog);
889
+ const registryState = await readOptionalFile(paths.projectRegistry);
890
+ if (!registryState.ok) return registryState;
891
+ const registryCheck = registryState.value.exists ? parseProjectRegistry(registryState.value.content) : success({
892
+ version: 1,
893
+ projects: {}
894
+ });
895
+ if (!registryCheck.ok) return registryCheck;
896
+ await locks.acquireProjectScopes(Object.keys(registryCheck.value.projects));
897
+ let nextConfiguration = input.nextConfiguration ?? currentConfiguration;
898
+ if (input.nextConfiguration === void 0 && input.prompt?.editGlobal) nextConfiguration = await input.prompt.editGlobal(currentConfiguration, catalog);
899
+ const nextCheck = validateGlobalObject(nextConfiguration, catalog);
900
+ if (!nextCheck.ok) return nextCheck;
901
+ const localConfigurations = /* @__PURE__ */ new Map();
902
+ const localConfigurationSources = /* @__PURE__ */ new Map();
903
+ for (const [root, registration] of Object.entries(registryCheck.value.projects)) {
904
+ const localPath = join(paths.projects, registration.identity, "config.yaml");
905
+ const localState = await readOptionalFile(localPath);
906
+ if (!localState.ok) return localState;
907
+ if (!localState.value.exists) return missingConfiguration("local", localPath);
908
+ const localCheck = parseLocalConfiguration(localState.value.content, currentConfiguration, catalog);
909
+ if (!localCheck.ok) return localCheck;
910
+ localConfigurations.set(root, localCheck.value);
911
+ localConfigurationSources.set(root, localState.value.content);
912
+ }
913
+ const removedNode = new Set(currentConfiguration.node.filter((entry) => !nextCheck.value.node.includes(entry)));
914
+ const replacementConfigurations = /* @__PURE__ */ new Map();
915
+ for (const [root, localConfiguration] of localConfigurations) {
916
+ if (localConfiguration.node === null || !removedNode.has(localConfiguration.node)) continue;
917
+ let replacement = input.nextLocalConfigurations?.[root];
918
+ if (replacement === void 0 && input.prompt?.editLocal) replacement = await input.prompt.editLocal(localConfiguration, catalog, nextCheck.value);
919
+ if (replacement === void 0) return failure({
920
+ kind: "validation",
921
+ code: "node-replacement-required",
922
+ observed: `Global Node Runtime ${localConfiguration.node} is selected by Project ${root}.`,
923
+ nextAction: `Select a replacement Node Runtime or none for Project ${root}.`
924
+ });
925
+ const replacementCheck = validateLocalObject(replacement, nextCheck.value, catalog);
926
+ if (!replacementCheck.ok) return replacementCheck;
927
+ replacementConfigurations.set(root, replacementCheck.value);
928
+ }
929
+ if (!(!globalState.value.exists || !configurationsEqual(currentConfiguration, nextCheck.value)) && replacementConfigurations.size === 0) return success({
930
+ scope: "global",
931
+ changed: false
932
+ });
933
+ if (!await (input.confirm ?? input.prompt?.confirm ?? (async () => true))("Save Global configuration?", {
934
+ title: "Review configuration",
935
+ sections: [{
936
+ title: "Global configuration",
937
+ current: serializeGlobalConfiguration(currentConfiguration).trim(),
938
+ next: serializeGlobalConfiguration(nextCheck.value).trim()
939
+ }, ...[...replacementConfigurations].map(([root, configuration]) => ({
940
+ title: `Project: ${root}`,
941
+ current: serializeLocalConfiguration(localConfigurations.get(root)).trim(),
942
+ next: serializeLocalConfiguration(configuration).trim()
943
+ }))]
944
+ })) return success({
945
+ scope: "global",
946
+ changed: false
947
+ });
948
+ if (input.signal?.aborted) throw new InterruptedError();
949
+ const composeWrites = [];
950
+ for (const [root, registration] of Object.entries(registryCheck.value.projects)) {
951
+ const composeWrite = await composeStateWrite({
952
+ projectRoot: root,
953
+ sandboxName: registration.name,
954
+ stateDirectory: projectStateDirectory(registration.identity, paths.home),
955
+ devboxHome: paths.home,
956
+ globalConfiguration: nextCheck.value,
957
+ localConfiguration: replacementConfigurations.get(root) ?? localConfigurations.get(root)
958
+ });
959
+ if (!composeWrite.ok) return composeWrite;
960
+ if (!composeWrite.value.previous.exists || composeWrite.value.content !== composeWrite.value.previous.content) composeWrites.push(composeWrite.value);
961
+ }
962
+ const written = await writeStateTransaction([
963
+ {
964
+ path: paths.globalConfiguration,
965
+ content: serializeGlobalConfiguration(nextCheck.value),
966
+ previous: globalState.value
967
+ },
968
+ ...[...replacementConfigurations].map(([root, configuration]) => ({
969
+ path: join(paths.projects, registryCheck.value.projects[root].identity, "config.yaml"),
970
+ content: serializeLocalConfiguration(configuration),
971
+ previous: {
972
+ exists: true,
973
+ content: localConfigurationSources.get(root)
974
+ }
975
+ })),
976
+ ...composeWrites
977
+ ]);
978
+ if (!written.ok) return written;
979
+ return success({
980
+ scope: "global",
981
+ changed: true
982
+ });
983
+ }
984
+ async function removeProject(input = {}) {
985
+ return withStateLocks({
986
+ devboxHome: input.devboxHome ?? join(homedir(), ".devbox"),
987
+ global: true,
988
+ projectRoots: [input.root ?? process.cwd()],
989
+ signal: input.signal
990
+ }, () => removeProjectUnlocked(input));
991
+ }
992
+ async function removeProjectUnlocked(input = {}) {
993
+ if (input.signal?.aborted) throw new InterruptedError();
994
+ const projectRoot = input.root ?? process.cwd();
995
+ const rootCheck = await validateProjectRoot(projectRoot);
996
+ if (!rootCheck.ok) return rootCheck;
997
+ const paths = devboxPaths(input.devboxHome);
998
+ const registryCheck = await readRegistry(paths.projectRegistry);
999
+ if (!registryCheck.ok) return registryCheck;
1000
+ const registration = registryCheck.value.projects[projectRoot];
1001
+ if (registration === void 0) return notRegistered(projectRoot);
1002
+ if (!input.yes) {
1003
+ if (!await (input.confirm ?? (async () => false))(`Remove Project ${projectRoot} and its Devbox state?`)) return success({
1004
+ root: projectRoot,
1005
+ removed: false
1006
+ });
1007
+ }
1008
+ if (input.signal?.aborted) throw new InterruptedError();
1009
+ const stateDirectory = projectStateDirectory(registration.identity, paths.home);
1010
+ try {
1011
+ await rm(stateDirectory, {
1012
+ recursive: true,
1013
+ force: true
1014
+ });
1015
+ } catch {
1016
+ return failure({
1017
+ kind: "operational",
1018
+ code: "project-state-removal-failed",
1019
+ observed: `Devbox could not remove Project state: ${stateDirectory}.`,
1020
+ nextAction: "Check write access to ~/.devbox and run devbox rm --yes again."
1021
+ });
1022
+ }
1023
+ const projects = { ...registryCheck.value.projects };
1024
+ delete projects[projectRoot];
1025
+ const written = await writeAtomically(paths.projectRegistry, serializeProjectRegistry({
1026
+ version: 1,
1027
+ projects
1028
+ }));
1029
+ if (!written.ok) return written;
1030
+ return success({
1031
+ root: projectRoot,
1032
+ removed: true
1033
+ });
1034
+ }
1035
+ async function cleanupMissingProjects(input = {}) {
1036
+ return withStateLocks({
1037
+ devboxHome: input.devboxHome ?? join(homedir(), ".devbox"),
1038
+ global: true,
1039
+ projectRoots: [],
1040
+ signal: input.signal
1041
+ }, async (locks) => {
1042
+ const discovery = await discoverMissingProjects(input);
1043
+ if (!discovery.ok) return discovery;
1044
+ await locks.acquireProjectScopes(discovery.value.missingRoots);
1045
+ return cleanupMissingProjectsUnlocked(input, discovery.value);
1046
+ });
1047
+ }
1048
+ async function discoverMissingProjects(input) {
1049
+ if (input.signal?.aborted) throw new InterruptedError();
1050
+ const paths = devboxPaths(input.devboxHome);
1051
+ const registryState = await readOptionalFile(paths.projectRegistry);
1052
+ if (!registryState.ok) return registryState;
1053
+ if (!registryState.value.exists) return success({
1054
+ paths,
1055
+ registry: {
1056
+ version: 1,
1057
+ projects: {}
1058
+ },
1059
+ missingRoots: []
1060
+ });
1061
+ const registryCheck = parseProjectRegistry(registryState.value.content);
1062
+ if (!registryCheck.ok) return registryCheck;
1063
+ const missingRoots = [];
1064
+ for (const root of Object.keys(registryCheck.value.projects)) {
1065
+ if (input.signal?.aborted) throw new InterruptedError();
1066
+ const exists = await projectRootExists(root);
1067
+ if (exists === void 0) return failure({
1068
+ kind: "operational",
1069
+ code: "project-root-observation-failed",
1070
+ observed: `Devbox could not inspect Project root: ${root}.`,
1071
+ nextAction: "Check access to the registered Project root and try cleanup again."
1072
+ });
1073
+ if (!exists) missingRoots.push(root);
1074
+ }
1075
+ return success({
1076
+ paths,
1077
+ registry: registryCheck.value,
1078
+ missingRoots
1079
+ });
1080
+ }
1081
+ async function cleanupMissingProjectsUnlocked(input, discovery) {
1082
+ if (input.signal?.aborted) throw new InterruptedError();
1083
+ if (discovery.missingRoots.length === 0) return success({
1084
+ roots: [],
1085
+ removed: false
1086
+ });
1087
+ if (!input.yes) {
1088
+ if (!await (input.confirm ?? (async () => false))(`Remove Missing-root Project registrations?\n${discovery.missingRoots.map((root) => `- ${root}`).join("\n")}`)) return success({
1089
+ roots: [],
1090
+ removed: false
1091
+ });
1092
+ }
1093
+ const removedRoots = [];
1094
+ const projects = { ...discovery.registry.projects };
1095
+ for (const root of discovery.missingRoots) {
1096
+ const rootExists = await projectRootExists(root);
1097
+ if (rootExists === void 0) return failure({
1098
+ kind: "operational",
1099
+ code: "project-root-observation-failed",
1100
+ observed: `Devbox could not recheck Project root: ${root}.`,
1101
+ nextAction: "Check access to the registered Project root and try cleanup again."
1102
+ });
1103
+ if (rootExists) continue;
1104
+ const registration = projects[root];
1105
+ if (registration === void 0) continue;
1106
+ try {
1107
+ await rm(projectStateDirectory(registration.identity, discovery.paths.home), {
1108
+ recursive: true,
1109
+ force: true
1110
+ });
1111
+ } catch {
1112
+ return failure({
1113
+ kind: "operational",
1114
+ code: "missing-project-state-removal-failed",
1115
+ observed: `Devbox could not remove Missing-root Project state for ${root}.`,
1116
+ nextAction: "Check write access to ~/.devbox and try cleanup again."
1117
+ });
1118
+ }
1119
+ delete projects[root];
1120
+ removedRoots.push(root);
1121
+ }
1122
+ if (removedRoots.length === 0) return success({
1123
+ roots: [],
1124
+ removed: false
1125
+ });
1126
+ const written = await writeAtomically(discovery.paths.projectRegistry, serializeProjectRegistry({
1127
+ version: 1,
1128
+ projects
1129
+ }));
1130
+ if (!written.ok) return written;
1131
+ return success({
1132
+ roots: removedRoots,
1133
+ removed: true
1134
+ });
1135
+ }
1136
+ async function runSandboxLifecycle(command, input = {}) {
1137
+ const projectRoot = input.root ?? process.cwd();
1138
+ return withStateLocks({
1139
+ devboxHome: input.devboxHome ?? join(homedir(), ".devbox"),
1140
+ global: false,
1141
+ projectRoots: [projectRoot],
1142
+ signal: input.signal
1143
+ }, () => runSandboxLifecycleUnlocked(command, input, projectRoot));
1144
+ }
1145
+ async function runSandboxLifecycleUnlocked(command, input, projectRoot) {
1146
+ if (input.signal?.aborted) throw new InterruptedError();
1147
+ const rootCheck = await validateProjectRoot(projectRoot);
1148
+ if (!rootCheck.ok) return rootCheck;
1149
+ const paths = devboxPaths(input.devboxHome);
1150
+ const registryCheck = await readRegistry(paths.projectRegistry);
1151
+ if (!registryCheck.ok) return registryCheck;
1152
+ const registration = registryCheck.value.projects[projectRoot];
1153
+ if (registration === void 0) return notRegistered(projectRoot);
1154
+ const environment = input.environment ?? currentHostEnvironment();
1155
+ if (command === "up") {
1156
+ const globalState = await readOptionalFile(paths.globalConfiguration);
1157
+ if (!globalState.ok) return globalState;
1158
+ if (!globalState.value.exists) return missingConfiguration("global", paths.globalConfiguration);
1159
+ const globalCheck = parseGlobalConfiguration(globalState.value.content);
1160
+ if (!globalCheck.ok) return globalCheck;
1161
+ await ensureSharedAgentVolumes(globalCheck.value.agent, environment);
1162
+ }
1163
+ const composePath = join(projectStateDirectory(registration.identity, paths.home), "compose.yaml");
1164
+ const operation = command === "up" ? ["up", "-d"] : command === "sh" ? [
1165
+ "exec",
1166
+ "--user",
1167
+ "devbox",
1168
+ "devbox",
1169
+ "bash"
1170
+ ] : [command];
1171
+ await (environment.runDirect ?? environment.run)("docker", [
1172
+ "compose",
1173
+ "--file",
1174
+ composePath,
1175
+ ...operation
1176
+ ]);
1177
+ return success(void 0);
1178
+ }
1179
+ async function validateProjectRoot(projectRoot) {
1180
+ if (!isAbsolute(projectRoot)) return failure({
1181
+ kind: "validation",
1182
+ code: "invalid-project-root",
1183
+ observed: `The current Project directory is not absolute: ${projectRoot}.`,
1184
+ nextAction: "Run the command from an existing directory."
1185
+ });
1186
+ try {
1187
+ if (!(await lstat(projectRoot)).isDirectory()) return failure({
1188
+ kind: "validation",
1189
+ code: "invalid-project-root",
1190
+ observed: `The current Project path is not a directory: ${projectRoot}.`,
1191
+ nextAction: "Run the command from an existing directory."
1192
+ });
1193
+ } catch {
1194
+ return failure({
1195
+ kind: "validation",
1196
+ code: "missing-project-root",
1197
+ observed: `The current Project directory does not exist: ${projectRoot}.`,
1198
+ nextAction: "Change to an existing directory and run the command again."
1199
+ });
1200
+ }
1201
+ return success(void 0);
1202
+ }
1203
+ async function readRegistry(path) {
1204
+ const state = await readOptionalFile(path);
1205
+ if (!state.ok) return state;
1206
+ if (!state.value.exists) return failure({
1207
+ kind: "validation",
1208
+ code: "project-not-registered",
1209
+ observed: "No Project registry exists for this Devbox installation.",
1210
+ nextAction: "Run devbox init from the exact Project directory first."
1211
+ });
1212
+ return parseProjectRegistry(state.value.content);
1213
+ }
1214
+ async function allocateSandboxIdentity(projectRoot, paths, registry) {
1215
+ const identity = sandboxIdentity(projectRoot);
1216
+ for (let suffix = 1; suffix < 1e6; suffix += 1) {
1217
+ const candidate = suffix === 1 ? identity : `${identity}-${suffix}`;
1218
+ if (!isSafeStateDirectoryName(candidate) || Object.values(registry.projects).some((entry) => entry.identity === candidate)) continue;
1219
+ try {
1220
+ await lstat(projectStateDirectory(candidate, paths.home));
1221
+ } catch (error) {
1222
+ if (isMissingFileError(error)) return success(candidate);
1223
+ return failure({
1224
+ kind: "operational",
1225
+ code: "state-directory-observation-failed",
1226
+ observed: `Devbox could not inspect Project state directory: ${projectStateDirectory(candidate, paths.home)}.`,
1227
+ nextAction: "Check write access to ~/.devbox and run devbox init again."
1228
+ });
1229
+ }
1230
+ }
1231
+ return failure({
1232
+ kind: "operational",
1233
+ code: "state-directory-allocation-failed",
1234
+ observed: `Devbox could not allocate a unique Project state directory for ${projectRoot}.`,
1235
+ nextAction: "Remove stale Devbox state only through its supported command and try again."
1236
+ });
1237
+ }
1238
+ function allocateSandboxName(projectRoot, registry) {
1239
+ const name = sandboxName(projectRoot);
1240
+ for (let suffix = 1; suffix < 1e6; suffix += 1) {
1241
+ const candidate = suffix === 1 ? name : `${name}-${suffix}`;
1242
+ if (isSafeSandboxName(candidate) && !Object.values(registry.projects).some((entry) => entry.name === candidate)) return success(candidate);
1243
+ }
1244
+ return failure({
1245
+ kind: "operational",
1246
+ code: "sandbox-name-allocation-failed",
1247
+ observed: `Devbox could not allocate a unique Sandbox name for ${projectRoot}.`,
1248
+ nextAction: "Remove stale Devbox state only through its supported command and try again."
1249
+ });
1250
+ }
1251
+ async function projectRootExists(projectRoot) {
1252
+ try {
1253
+ return (await lstat(projectRoot)).isDirectory();
1254
+ } catch (error) {
1255
+ if (isMissingFileError(error)) return false;
1256
+ return;
1257
+ }
1258
+ }
1259
+ async function readOptionalFile(path) {
1260
+ try {
1261
+ return success({
1262
+ exists: true,
1263
+ content: await readFile(path, "utf8")
1264
+ });
1265
+ } catch (error) {
1266
+ if (isMissingFileError(error)) return success({
1267
+ exists: false,
1268
+ content: ""
1269
+ });
1270
+ return failure({
1271
+ kind: "operational",
1272
+ code: "state-read-failed",
1273
+ observed: `Devbox could not read state file: ${path}.`,
1274
+ nextAction: "Check write access to ~/.devbox and try again."
1275
+ });
1276
+ }
1277
+ }
1278
+ async function writeAtomically(path, content) {
1279
+ const staged = await stageStateWrite(path, content, "tmp");
1280
+ if (!staged.ok) return staged;
1281
+ try {
1282
+ await rename(staged.value, path);
1283
+ return success(void 0);
1284
+ } catch {
1285
+ await rm(staged.value, { force: true }).catch(() => void 0);
1286
+ return failure({
1287
+ kind: "operational",
1288
+ code: "state-write-failed",
1289
+ observed: `Devbox could not atomically write state file: ${path}.`,
1290
+ nextAction: "Check write access to ~/.devbox and run the command again."
1291
+ });
1292
+ }
1293
+ }
1294
+ async function stageStateWrite(path, content, suffix) {
1295
+ const directory = dirname(path);
1296
+ const temporaryPath = join(directory, `.${basename(path)}-${process.pid}-${randomUUID()}.${suffix}`);
1297
+ try {
1298
+ await mkdir(directory, {
1299
+ recursive: true,
1300
+ mode: 448
1301
+ });
1302
+ await writeFile(temporaryPath, content, {
1303
+ encoding: "utf8",
1304
+ flag: "wx",
1305
+ mode: 384
1306
+ });
1307
+ return success(temporaryPath);
1308
+ } catch {
1309
+ await rm(temporaryPath, { force: true }).catch(() => void 0);
1310
+ return failure({
1311
+ kind: "operational",
1312
+ code: "state-write-failed",
1313
+ observed: `Devbox could not atomically write state file: ${path}.`,
1314
+ nextAction: "Check write access to ~/.devbox and run the command again."
1315
+ });
1316
+ }
1317
+ }
1318
+ async function composeStateWrite(input) {
1319
+ const claudeHostConfiguration = await ensureClaudeHostConfiguration(input.devboxHome, input.globalConfiguration);
1320
+ if (!claudeHostConfiguration.ok) return claudeHostConfiguration;
1321
+ const rendered = renderProjectCompose({
1322
+ projectRoot: input.projectRoot,
1323
+ sandboxName: input.sandboxName,
1324
+ selectedNode: input.localConfiguration.node,
1325
+ configuredAgents: input.globalConfiguration.agent,
1326
+ agentNotifications: input.globalConfiguration.agent_notifications,
1327
+ claudeHostConfiguration: claudeHostConfiguration.value
1328
+ });
1329
+ if (!rendered.ok) return rendered;
1330
+ const path = join(input.stateDirectory, "compose.yaml");
1331
+ const previous = await readOptionalFile(path);
1332
+ if (!previous.ok) return previous;
1333
+ return success({
1334
+ path,
1335
+ content: rendered.value,
1336
+ previous: previous.value
1337
+ });
1338
+ }
1339
+ async function ensureClaudeHostConfiguration(devboxHome, globalConfiguration) {
1340
+ if (!globalConfiguration.agent.includes("claude-code")) return success(void 0);
1341
+ const path = devboxPaths(devboxHome).claudeHostConfiguration;
1342
+ try {
1343
+ if ((await lstat(path)).isFile()) return success(path);
1344
+ return failure({
1345
+ kind: "operational",
1346
+ code: "claude-host-configuration-invalid",
1347
+ observed: `Claude host configuration is not a regular file: ${path}.`,
1348
+ nextAction: "Replace it with a regular file, then run the command again."
1349
+ });
1350
+ } catch (error) {
1351
+ if (!isMissingFileError(error)) return failure({
1352
+ kind: "operational",
1353
+ code: "claude-host-configuration-unreadable",
1354
+ observed: `Devbox could not inspect Claude host configuration: ${path}.`,
1355
+ nextAction: "Check access to ~/.devbox and run the command again."
1356
+ });
1357
+ }
1358
+ try {
1359
+ await mkdir(dirname(path), {
1360
+ recursive: true,
1361
+ mode: 448
1362
+ });
1363
+ await writeFile(path, "{}\n", {
1364
+ encoding: "utf8",
1365
+ flag: "wx",
1366
+ mode: 384
1367
+ });
1368
+ return success(path);
1369
+ } catch (error) {
1370
+ if (error.code === "EEXIST") return ensureClaudeHostConfiguration(devboxHome, globalConfiguration);
1371
+ return failure({
1372
+ kind: "operational",
1373
+ code: "claude-host-configuration-write-failed",
1374
+ observed: `Devbox could not create Claude host configuration: ${path}.`,
1375
+ nextAction: "Check write access to ~/.devbox and run the command again."
1376
+ });
1377
+ }
1378
+ }
1379
+ async function writeStateTransaction(writes) {
1380
+ const staged = [];
1381
+ for (const write of writes) {
1382
+ const stagedWrite = await stageStateWrite(write.path, write.content, "transaction.tmp");
1383
+ if (!stagedWrite.ok) {
1384
+ await Promise.all(staged.map((stage) => rm(stage.path, { force: true }).catch(() => void 0)));
1385
+ return stagedWrite;
1386
+ }
1387
+ staged.push({
1388
+ write,
1389
+ path: stagedWrite.value
1390
+ });
1391
+ }
1392
+ const published = [];
1393
+ try {
1394
+ for (const stage of staged) {
1395
+ await rename(stage.path, stage.write.path);
1396
+ published.push(stage.write);
1397
+ }
1398
+ return success(void 0);
1399
+ } catch {
1400
+ let restored = true;
1401
+ for (const write of published.reverse()) if (write.previous.exists) {
1402
+ const restoredWrite = await writeAtomically(write.path, write.previous.content);
1403
+ restored &&= restoredWrite.ok;
1404
+ } else try {
1405
+ await rm(write.path, { force: true });
1406
+ } catch {
1407
+ restored = false;
1408
+ }
1409
+ await Promise.all(staged.map((stage) => rm(stage.path, { force: true }).catch(() => void 0)));
1410
+ return failure({
1411
+ kind: "operational",
1412
+ code: "state-write-failed",
1413
+ observed: restored ? "Devbox could not publish Project state; the previous state was restored." : "Devbox could not publish Project state or restore the previous state.",
1414
+ nextAction: "Check write access to ~/.devbox and run the command again."
1415
+ });
1416
+ }
1417
+ }
1418
+ function validateGlobalObject(configuration, catalog) {
1419
+ try {
1420
+ return parseGlobalConfiguration(serializeGlobalConfiguration(configuration), catalog);
1421
+ } catch {
1422
+ return failure({
1423
+ kind: "validation",
1424
+ code: "invalid-global-configuration",
1425
+ observed: "Global configuration has invalid values.",
1426
+ nextAction: "Edit the supported version-1 Global YAML configuration and try again."
1427
+ });
1428
+ }
1429
+ }
1430
+ function validateLocalObject(configuration, globalConfiguration, catalog) {
1431
+ try {
1432
+ return parseLocalConfiguration(serializeLocalConfiguration(configuration), globalConfiguration, catalog);
1433
+ } catch {
1434
+ return failure({
1435
+ kind: "validation",
1436
+ code: "invalid-local-configuration",
1437
+ observed: "Local configuration has invalid values.",
1438
+ nextAction: "Edit the supported version-1 Local YAML configuration and try again."
1439
+ });
1440
+ }
1441
+ }
1442
+ function notRegistered(projectRoot) {
1443
+ return failure({
1444
+ kind: "validation",
1445
+ code: "project-not-registered",
1446
+ observed: `The exact Project directory is not registered: ${projectRoot}.`,
1447
+ nextAction: "Run devbox init from this exact existing directory first."
1448
+ });
1449
+ }
1450
+ function missingConfiguration(scope, path) {
1451
+ return failure({
1452
+ kind: "validation",
1453
+ code: `missing-${scope}-configuration`,
1454
+ observed: `Devbox could not find the ${scope} configuration: ${path}.`,
1455
+ nextAction: `Restore the supported version-1 ${scope} YAML configuration and try again.`
1456
+ });
1457
+ }
1458
+ function isMissingFileError(error) {
1459
+ return typeof error === "object" && error !== null && error.code === "ENOENT";
1460
+ }
1461
+ var InterruptedError = class extends Error {
1462
+ constructor() {
1463
+ super("Devbox command interrupted.");
1464
+ }
1465
+ };
1466
+ //#endregion
1467
+ //#region src/configuration/prompter.ts
1468
+ function createConfigurationPrompter({ signal, input, output }) {
1469
+ const common = {
1470
+ signal,
1471
+ input,
1472
+ output
1473
+ };
1474
+ return {
1475
+ confirm: async (message, details) => {
1476
+ if (details !== void 0) p.note(renderConfirmationDetails(details), c.bold(c.cyan(details.title)), common);
1477
+ return promptValue(p.confirm({
1478
+ message,
1479
+ ...common
1480
+ }));
1481
+ },
1482
+ selectConfigurationScope: async () => promptValue(p.select({
1483
+ message: "Configuration scope",
1484
+ options: [{
1485
+ value: "local",
1486
+ label: "Current Project"
1487
+ }, {
1488
+ value: "global",
1489
+ label: "Global"
1490
+ }],
1491
+ ...common
1492
+ })),
1493
+ editGlobal: async (configuration, catalog) => {
1494
+ p.log.step(`${c.bold(c.cyan("Global configuration"))}\n${c.dim("Choose the runtimes and agents available to every Project.")}`, common);
1495
+ return {
1496
+ version: 1,
1497
+ node: await promptValue(p.multiselect({
1498
+ message: "Configured Node release lines",
1499
+ options: (catalog.runtimes.node ?? []).map((value) => ({ value })),
1500
+ initialValues: [...configuration.node],
1501
+ required: false,
1502
+ ...common
1503
+ })),
1504
+ agent: await promptValue(p.multiselect({
1505
+ message: "Configured Agents",
1506
+ options: catalog.agents.map((value) => ({ value })),
1507
+ initialValues: [...configuration.agent],
1508
+ required: false,
1509
+ ...common
1510
+ })),
1511
+ agent_notifications: await promptValue(p.confirm({
1512
+ message: "Enable Agent notifications",
1513
+ initialValue: configuration.agent_notifications,
1514
+ ...common
1515
+ }))
1516
+ };
1517
+ },
1518
+ editLocal: async (configuration, catalog, globalConfiguration) => {
1519
+ p.log.step(`${c.bold(c.cyan("Project configuration"))}\n${c.dim("Choose the runtime for this Project.")}`, common);
1520
+ return {
1521
+ version: 1,
1522
+ node: await promptValue(p.select({
1523
+ message: "Selected Node release line",
1524
+ options: [{
1525
+ value: null,
1526
+ label: "None"
1527
+ }, ...(globalConfiguration.node ?? []).map((value) => ({
1528
+ value,
1529
+ label: value
1530
+ }))],
1531
+ initialValue: configuration.node,
1532
+ ...common
1533
+ }))
1534
+ };
1535
+ }
1536
+ };
1537
+ }
1538
+ function renderConfirmationDetails(details) {
1539
+ return details.sections.map((section) => [
1540
+ c.bold(section.title),
1541
+ `${c.yellow(c.dim("Current"))}\n${section.current}`,
1542
+ `${c.bold(c.green("New"))}\n${section.next}`
1543
+ ].join("\n\n")).join("\n\n");
1544
+ }
1545
+ async function promptValue(prompt) {
1546
+ const value = await prompt;
1547
+ if (p.isCancel(value)) throw new InterruptedError();
1548
+ return value;
1549
+ }
1550
+ //#endregion
1551
+ //#region src/logo.ts
1552
+ const ASCII_ART = [
1553
+ "██████╗ ███████╗██╗ ██╗██████╗ ██████╗ ██╗ ██╗",
1554
+ "██╔══██╗██╔════╝██║ ██║██╔══██╗██╔═══██╗╚██╗██╔╝",
1555
+ "██║ ██║█████╗ ██║ ██║██████╔╝██║ ██║ ╚███╔╝ ",
1556
+ "██║ ██║██╔══╝ ╚██╗ ██╔╝██╔══██╗██║ ██║ ██╔██╗ ",
1557
+ "██████╔╝███████╗ ╚████╔╝ ██████╔╝╚██████╔╝██╔╝ ██╗",
1558
+ "╚═════╝ ╚══════╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝"
1559
+ ];
1560
+ const COLORS = [
1561
+ `\x1B[38;5;51m`,
1562
+ `\x1B[38;5;45m`,
1563
+ `\x1B[38;5;39m`,
1564
+ `\x1B[38;5;32m`,
1565
+ `\x1B[38;5;25m`,
1566
+ `\x1B[38;5;26m`
1567
+ ];
1568
+ const REST = "\x1B[0m";
1569
+ function showLogo() {
1570
+ for (let i = 0; i < ASCII_ART.length; i++) console.log(`${COLORS[i]}${ASCII_ART[i]}${REST}`);
1571
+ }
1572
+ //#endregion
1573
+ //#region \0devbox-workspace-image-default:.bash_aliases.mjs
1574
+ var _devbox_workspace_image_default__bash_aliases_default = "alias ..='cd ..'\nalias ...='cd ../..'\nalias c='clear'\nalias g='git'\nalias ccc='claude --dangerously-skip-permissions'\nalias ccx='codex --sandbox workspace-write --ask-for-approval never'\nalias ggg='agy --dangerously-skip-permissions'\n\nfunction mkcd() {\n mkdir \"$1\"\n cd \"$1\"\n}\n\n# Laravel Artisan\nalias pa='php artisan'\nalias pav='php artisan serve'\nalias pam='php artisan migrate'\nalias pams='php artisan migrate --seed'\nalias pamr='php artisan migrate:rollback'\nalias pamf='php artisan migrate:fresh'\nalias pamfs='php artisan migrate:fresh --seed'\nalias pas='php artisan db:seed'\nalias par='php artisan route:list'\nalias paq='php artisan queue:work'\nalias pah='php artisan horizon'\nalias pat='php artisan tinker'\nalias phpunit='./vendor/bin/phpunit --colors=always'\nalias pu='phpunit'\nalias pest='./vendor/bin/pest --colors=always'\nalias p='pest'\nalias pint='./vendor/bin/pint'\nalias phpstan='./vendor/bin/phpstan analyse --ansi'\nalias rector='./vendor/bin/rector'\n\n# NPM (requires @antfu/ni)\nalias nv='nr dev' # nd exists in @antfu/ni\nalias nb='nr build'\nalias nll='nr lint' # nl is a Linux command\nalias nlf='nr lint --fix'\nalias nf='nr fmt'\nalias nfc='nr fmt --check'\nalias nc='nr check'\nalias nt='nr test'\nalias ntr='nr test --run'\n";
1575
+ //#endregion
1576
+ //#region \0devbox-workspace-image-default:.claude/settings.json.mjs
1577
+ 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";
1578
+ //#endregion
1579
+ //#region \0devbox-workspace-image-default:.gitconfig.mjs
1580
+ var _devbox_workspace_image_default__gitconfig_default = "[core]\n autocrlf = input\n fileMode = false\n pager = less -r\n quotepath = false\n[init]\n defaultBranch = main\n[color]\n diff = auto\n status = auto\n branch = auto\n[alias]\n cf = config\n cfl = config --list\n s = status\n a = add .\n l = log --oneline --graph\n la = log --oneline --graph --all\n last = log -1 HEAD\n c = commit\n cm = commit -m\n ca = commit --amend -m\n can = commit --amend --no-edit\n co = checkout\n cp = cherry-pick\n b = branch\n m = merge\n mnf = merge --no-ff\n rs = reset\n rs0h = reset HEAD --hard\n rs1 = reset HEAD~1\n rs1h = reset HEAD~1 --hard\n rb = rebase\n rbo = rebase --onto\n rbi = rebase -i\n rbc = rebase --continue\n rba = rebase --abort\n rm = remote\n r = remote -v\n ra = remote add\n rr = remote remove\n rp = remote prune\n f = fetch\n fp = fetch --prune\n p = push\n pl = pull\n";
1581
+ //#endregion
1582
+ //#region \0devbox-workspace-image-default:.omp/agent/config.yml.mjs
1583
+ 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";
1584
+ //#endregion
1585
+ //#region src/workspace/build.ts
1586
+ const BASE_WORKSPACE_IMAGE_DEFAULTS = [{
1587
+ relativePath: ".bash_aliases",
1588
+ content: _devbox_workspace_image_default__bash_aliases_default
1589
+ }, {
1590
+ relativePath: ".gitconfig",
1591
+ content: _devbox_workspace_image_default__gitconfig_default
1592
+ }];
1593
+ const CLAUDE_SETTINGS_DEFAULT = {
1594
+ relativePath: ".claude/settings.json",
1595
+ content: settings_json_default
1596
+ };
1597
+ const OMP_AGENT_CONFIG_DEFAULT = {
1598
+ relativePath: ".omp/agent/config.yml",
1599
+ content: config_yml_default
1600
+ };
1601
+ async function buildWorkspace(input = {}) {
1602
+ return withStateLocks({
1603
+ devboxHome: input.devboxHome ?? join(homedir(), ".devbox"),
1604
+ global: true,
1605
+ projectRoots: [],
1606
+ signal: input.signal
1607
+ }, () => buildWorkspaceUnlocked(input));
1608
+ }
1609
+ async function buildWorkspaceUnlocked(input) {
1610
+ if (input.signal?.aborted) throw new InterruptedError();
1611
+ const catalogCheck = normalizeCatalog();
1612
+ if (!catalogCheck.ok) return catalogCheck;
1613
+ const paths = devboxPaths(input.devboxHome);
1614
+ let globalSource;
1615
+ try {
1616
+ globalSource = await readFile(paths.globalConfiguration, "utf8");
1617
+ } catch {
1618
+ return failure({
1619
+ kind: "operational",
1620
+ code: "global-configuration-missing",
1621
+ observed: "Global configuration is required before building the Workspace image.",
1622
+ nextAction: "Run devbox config to commit Global configuration before devbox build."
1623
+ });
1624
+ }
1625
+ const parsedGlobal = parseGlobalConfiguration(globalSource, catalogCheck.value);
1626
+ if (!parsedGlobal.ok) return parsedGlobal;
1627
+ const globalConfiguration = parsedGlobal.value;
1628
+ const agents = globalConfiguration.agent.map((name) => ({
1629
+ name,
1630
+ recipe: PACKAGED_AGENTS[name]
1631
+ }));
1632
+ const buildContextAssets = selectBuildContextAssets(agents);
1633
+ const nodeRuntimes = globalConfiguration.node.map((releaseLine) => PACKAGED_NODE_RECIPES[releaseLine]);
1634
+ const dockerfile = renderBuildDockerfile({
1635
+ nodeRuntimes,
1636
+ buildNodeRuntime: buildNodeRuntimeRecipe(globalConfiguration),
1637
+ agents,
1638
+ skillAgents: globalConfiguration.agent.filter((agent) => PACKAGED_AGENTS[agent]?.supportsSkillInstallation === true)
1639
+ });
1640
+ await mkdir(paths.buildContext, { recursive: true });
1641
+ await seedBuildContextAssets(paths.buildContext, buildContextAssets);
1642
+ await Promise.all([
1643
+ writeFile(join(paths.buildContext, "Dockerfile"), dockerfile),
1644
+ writeFile(join(paths.buildContext, ".dockerignore"), renderDockerignore(buildContextAssets)),
1645
+ writeFile(join(paths.buildContext, "entrypoint.sh"), renderEntrypoint({
1646
+ nodeRuntimes,
1647
+ agents
1648
+ })),
1649
+ writeFile(join(paths.buildContext, "supervisord.conf"), renderSupervisorConfiguration())
1650
+ ]);
1651
+ if (input.signal?.aborted) throw new InterruptedError();
1652
+ const buildResult = await (input.executeDockerBuild ?? runDockerBuild)({
1653
+ context: paths.buildContext,
1654
+ image: WORKSPACE_IMAGE,
1655
+ noCache: input.noCache === true
1656
+ });
1657
+ if (!buildResult.ok) return buildResult;
1658
+ return success({ image: WORKSPACE_IMAGE });
1659
+ }
1660
+ function buildNodeRuntimeRecipe(globalConfiguration) {
1661
+ let buildNodeRuntime;
1662
+ for (const releaseLine of globalConfiguration.node) {
1663
+ const recipe = PACKAGED_NODE_RECIPES[releaseLine];
1664
+ if (buildNodeRuntime === void 0 || Number.parseFloat(releaseLine) > Number.parseFloat(buildNodeRuntime.releaseLine)) buildNodeRuntime = recipe;
1665
+ }
1666
+ return buildNodeRuntime;
1667
+ }
1668
+ function renderBuildDockerfile(input) {
1669
+ const lines = [
1670
+ "# Generated by devbox build. Machine-owned Workspace build context.",
1671
+ "FROM ubuntu:24.04",
1672
+ "",
1673
+ "ENV DEBIAN_FRONTEND=noninteractive",
1674
+ "ENV TZ=UTC",
1675
+ "ENV LANG=C.UTF-8",
1676
+ "",
1677
+ "RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone",
1678
+ "",
1679
+ "# Install the Base profile packages and build tools",
1680
+ "RUN set -eux \\",
1681
+ " && apt-get update && apt-get upgrade -y \\",
1682
+ " && apt-get install -y --no-install-recommends \\",
1683
+ " ca-certificates curl wget git supervisor openssh-client \\",
1684
+ " zip unzip jq less vim ripgrep fd-find procps lsof iproute2 \\",
1685
+ " dnsutils netcat-openbsd rsync tree tzdata bubblewrap \\",
1686
+ " build-essential python3 python-is-python3 pkg-config \\",
1687
+ " gnupg dirmngr xz-utils pulseaudio-utils sudo gosu \\",
1688
+ " && apt-get -y autoremove \\",
1689
+ " && apt-get clean \\",
1690
+ " && rm -rf /var/lib/apt/lists/*",
1691
+ "",
1692
+ "# Create the devbox user and group",
1693
+ "RUN userdel -r ubuntu \\",
1694
+ " && groupadd -f -g 1000 devbox \\",
1695
+ " && useradd -m -s /bin/bash -N -g 1000 -u 1000 devbox \\",
1696
+ " && echo \"devbox ALL=(ALL) NOPASSWD:ALL\" > /etc/sudoers.d/devbox \\",
1697
+ " && chmod 0440 /etc/sudoers.d/devbox",
1698
+ ""
1699
+ ];
1700
+ for (const recipe of input.nodeRuntimes) lines.push(...renderNodeRuntimeStage(recipe));
1701
+ lines.push("WORKDIR /workspace", "USER devbox", "");
1702
+ if (input.buildNodeRuntime !== void 0) lines.push("# Configure pnpm for the non-root Sandbox user", "RUN mkdir -p /home/devbox/.config/pnpm /home/devbox/.pnpm-store \\", " && printf 'storeDir: /home/devbox/.pnpm-store\\n' > /home/devbox/.config/pnpm/config.yaml \\", " && chown devbox:devbox /home/devbox/.config/pnpm/config.yaml", "");
1703
+ for (const agent of input.agents) lines.push(`# Install ${agent.name}`, `RUN mkdir -p ${agent.recipe.home.target} \\`, ` && curl -fsSL ${agent.recipe.installation.url} | ${agent.recipe.installation.shell}`, "");
1704
+ if (input.agents.length > 0) lines.push("# Set the PATH to include AI tools", "ENV PATH=\"/home/devbox/.local/bin:${PATH}\"", "");
1705
+ if (input.buildNodeRuntime !== void 0 && input.skillAgents.length > 0) {
1706
+ const skillAgentArguments = input.skillAgents.map((agent) => `-a ${agent}`).join(" ");
1707
+ lines.push("# Install Agent Skills", "RUN set -eux \\", " && mkdir -p /home/devbox/.agents/skills \\", ` && export PATH="${input.buildNodeRuntime.runtimeRoot}/bin:$PATH" \\`, ` && npx -y skills add ycs77/skills -g ${skillAgentArguments} -s '*' -y`, "");
1708
+ }
1709
+ lines.push("USER root", "", "# Copy dotfiles", "COPY .bash_aliases /home/devbox/.bash_aliases", "COPY .gitconfig /home/devbox/.gitconfig", "RUN chown devbox:devbox /home/devbox/.bash_aliases \\", " && chown devbox:devbox /home/devbox/.gitconfig", "");
1710
+ const hasClaudeCode = input.agents.some((agent) => agent.name === "claude-code");
1711
+ const hasOmp = input.agents.some((agent) => agent.name === "omp");
1712
+ if (hasClaudeCode || hasOmp) {
1713
+ const ownershipCommands = [];
1714
+ lines.push("# Copy AI dotfiles");
1715
+ if (hasClaudeCode) {
1716
+ lines.push("COPY .claude/settings.json /home/devbox/.claude/settings.json");
1717
+ ownershipCommands.push("chown devbox:devbox /home/devbox/.claude/settings.json");
1718
+ }
1719
+ if (hasOmp) {
1720
+ lines.push("COPY .omp/agent/config.yml /home/devbox/.omp/agent/config.yml");
1721
+ ownershipCommands.push("chown devbox:devbox /home/devbox/.omp/agent", "chown devbox:devbox /home/devbox/.omp/agent/config.yml");
1722
+ }
1723
+ lines.push(`RUN ${ownershipCommands.join(" \\\n && ")}`, "");
1724
+ }
1725
+ lines.push("COPY entrypoint.sh /usr/local/bin/entrypoint.sh", "COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf", "RUN chmod +x /usr/local/bin/entrypoint.sh", "", "ENTRYPOINT [\"/usr/local/bin/entrypoint.sh\"]");
1726
+ return `${lines.join("\n")}\n`;
1727
+ }
1728
+ function renderDockerignore(assets) {
1729
+ const lines = [
1730
+ "# Machine-owned Workspace build context",
1731
+ "*",
1732
+ "!Dockerfile",
1733
+ "!entrypoint.sh",
1734
+ "!supervisord.conf",
1735
+ ...assets.map((asset) => `!${asset.relativePath}`)
1736
+ ];
1737
+ if (assets.some((asset) => asset.relativePath === ".claude/settings.json")) lines.push("!.claude", "!.claude/settings.json");
1738
+ if (assets.some((asset) => asset.relativePath === ".omp/agent/config.yml")) lines.push("!.omp", "!.omp/agent", "!.omp/agent/config.yml");
1739
+ return `${lines.join("\n")}\n`;
1740
+ }
1741
+ async function seedBuildContextAssets(buildContext, assets) {
1742
+ await Promise.all(assets.map(async (asset) => {
1743
+ const destination = join(buildContext, asset.relativePath);
1744
+ await mkdir(dirname(destination), { recursive: true });
1745
+ try {
1746
+ await writeFile(destination, asset.content, { flag: "wx" });
1747
+ } catch (error) {
1748
+ if (typeof error !== "object" || error === null || !("code" in error) || error.code !== "EEXIST") throw error;
1749
+ }
1750
+ }));
1751
+ }
1752
+ function selectBuildContextAssets(agents) {
1753
+ const assets = [...BASE_WORKSPACE_IMAGE_DEFAULTS];
1754
+ if (agents.some((agent) => agent.name === "claude-code")) assets.push(CLAUDE_SETTINGS_DEFAULT);
1755
+ if (agents.some((agent) => agent.name === "omp")) assets.push(OMP_AGENT_CONFIG_DEFAULT);
1756
+ return assets;
1757
+ }
1758
+ function renderEntrypoint(input) {
1759
+ const lines = [
1760
+ "#!/bin/sh",
1761
+ "set -eu",
1762
+ ""
1763
+ ];
1764
+ if (input.nodeRuntimes.length > 0) lines.push("# Set the Node.js release line to use", "NODE_VERSION=\"${NODE_VERSION:-}\"", "if [ -n \"$NODE_VERSION\" ]; then", " case \"$NODE_VERSION\" in", " *[!0-9]*)", " echo \"NODE_VERSION must be a numeric release line: $NODE_VERSION\" >&2", " exit 1", " ;;", " esac", "", " NODE_RUNTIME_ROOT=\"/opt/devbox/runtimes/node/$NODE_VERSION\"", " if [ ! -x \"$NODE_RUNTIME_ROOT/bin/node\" ]; then", " echo \"Node.js release line $NODE_VERSION is not installed.\" >&2", " exit 1", " fi", "", " export NODE_RUNTIME_ROOT", " export PATH=\"$NODE_RUNTIME_ROOT/bin:$PATH\"", "fi", "");
1765
+ if (input.agents.some((agent) => agent.name === "agy")) lines.push("# Link shared Agent Skills", "if [ ! -L \"/home/devbox/.gemini/skills\" ]; then", " ln -sfn /home/devbox/.agents/skills /home/devbox/.gemini/skills", " chown -h devbox:devbox /home/devbox/.gemini/skills", "fi", "");
1766
+ lines.push("if [ $# -gt 0 ]; then", " exec gosu devbox \"$@\"", "else", " exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf", "fi");
1767
+ return `${lines.join("\n")}\n`;
1768
+ }
1769
+ function renderSupervisorConfiguration() {
1770
+ return `${[
1771
+ "[supervisord]",
1772
+ "nodaemon=true",
1773
+ "user=root",
1774
+ "logfile=/var/log/supervisor/supervisord.log",
1775
+ "pidfile=/var/run/supervisord.pid",
1776
+ "",
1777
+ "[program:idle]",
1778
+ "command=/bin/sleep infinity",
1779
+ "autorestart=false",
1780
+ "startsecs=0",
1781
+ "stopsignal=TERM",
1782
+ "stopasgroup=true",
1783
+ "killasgroup=true",
1784
+ "stdout_logfile=/dev/null",
1785
+ "stderr_logfile=/dev/null"
1786
+ ].join("\n")}\n`;
1787
+ }
1788
+ function renderNodeRuntimeStage(recipe) {
1789
+ const keys = recipe.trustedReleaseKeys.map((key) => ` ${key}`).join(" \\\n");
1790
+ return [
1791
+ `# Install Node.js ${recipe.releaseLine}`,
1792
+ "RUN ARCH= OPENSSL_ARCH= && dpkgArch=\"$(dpkg --print-architecture)\" \\",
1793
+ " && case \"${dpkgArch##*-}\" in \\",
1794
+ " amd64) ARCH='x64' OPENSSL_ARCH='linux-x86_64';; \\",
1795
+ " ppc64el) ARCH='ppc64le' OPENSSL_ARCH='linux-ppc64le';; \\",
1796
+ " s390x) ARCH='s390x' OPENSSL_ARCH='linux*-s390x';; \\",
1797
+ " arm64) ARCH='arm64' OPENSSL_ARCH='linux-aarch64';; \\",
1798
+ " *) echo \"unsupported architecture\"; exit 1 ;; \\",
1799
+ " esac \\",
1800
+ " && set -eux \\",
1801
+ ` && NODE_RUNTIME_VERSION=${recipe.version} \\`,
1802
+ ` && NODE_RUNTIME_ROOT=${recipe.runtimeRoot} \\`,
1803
+ ` && NODE_ARCHIVE_URL="https://nodejs.org/dist/v\${NODE_RUNTIME_VERSION}/node-v\${NODE_RUNTIME_VERSION}-linux-\${ARCH}.tar.xz" \\`,
1804
+ ` && NODE_CHECKSUMS_URL="https://nodejs.org/dist/v\${NODE_RUNTIME_VERSION}/SHASUMS256.txt.asc" \\`,
1805
+ " && mkdir -p \"$NODE_RUNTIME_ROOT\" /tmp/node-source \\",
1806
+ " && cd /tmp/node-source \\",
1807
+ " && export GNUPGHOME=\"$(mktemp -d)\" \\",
1808
+ " && for key in \\",
1809
+ `${keys} \\`,
1810
+ " ; do \\",
1811
+ " { gpg --batch --no-options --keyserver hkps://keys.openpgp.org --recv-keys \"$key\" && gpg --batch --no-options --fingerprint \"$key\"; } || \\",
1812
+ " { gpg --batch --no-options --keyserver keyserver.ubuntu.com --recv-keys \"$key\" && gpg --batch --no-options --fingerprint \"$key\"; }; \\",
1813
+ " done \\",
1814
+ " && archive=\"node-v${NODE_RUNTIME_VERSION}-linux-${ARCH}.tar.xz\" \\",
1815
+ " && curl -fsSL --retry 3 -o \"$archive\" \"$NODE_ARCHIVE_URL\" \\",
1816
+ " && curl -fsSL --retry 3 -o SHASUMS256.txt.asc \"$NODE_CHECKSUMS_URL\" \\",
1817
+ " && gpg --batch --no-options --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \\",
1818
+ " && gpgconf --kill all \\",
1819
+ " && rm -rf \"$GNUPGHOME\" \\",
1820
+ " && awk -v archive=\"$archive\" \\",
1821
+ " '$2 == archive { print; found = 1; exit } END { if (!found) exit 1 }' \\",
1822
+ " SHASUMS256.txt > \"$archive.sha256\" \\",
1823
+ " && sha256sum --check \"$archive.sha256\" \\",
1824
+ " && tar --extract --xz --file \"$archive\" --directory \"$NODE_RUNTIME_ROOT\" \\",
1825
+ " --strip-components=1 --no-same-owner \\",
1826
+ " && rm \"$archive\" SHASUMS256.txt.asc SHASUMS256.txt \"$archive.sha256\" \\",
1827
+ " && find \"$NODE_RUNTIME_ROOT/include/node/openssl/archs\" -mindepth 1 -maxdepth 1 ! -name \"$OPENSSL_ARCH\" -exec rm -rf {} \\; \\",
1828
+ " && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \\",
1829
+ " && export PATH=\"$NODE_RUNTIME_ROOT/bin:$PATH\" \\",
1830
+ " && npm install -g npm \\",
1831
+ " && npm uninstall -g corepack \\",
1832
+ " && npm install -g yarn pnpm @antfu/ni \\",
1833
+ " && \"$NODE_RUNTIME_ROOT/bin/node\" --version \\",
1834
+ " && \"$NODE_RUNTIME_ROOT/bin/npm\" --version \\",
1835
+ " && rm -rf /tmp/node-source",
1836
+ ""
1837
+ ];
1838
+ }
1839
+ async function runDockerBuild(invocation, signal) {
1840
+ const build = spawn("docker", [
1841
+ "build",
1842
+ ...invocation.noCache ? ["--no-cache"] : [],
1843
+ "--tag",
1844
+ invocation.image,
1845
+ "--file",
1846
+ join(invocation.context, "Dockerfile"),
1847
+ invocation.context
1848
+ ], { stdio: "inherit" });
1849
+ const interrupt = () => build.kill("SIGINT");
1850
+ signal?.addEventListener("abort", interrupt, { once: true });
1851
+ let spawnFailed = false;
1852
+ build.on("error", () => {
1853
+ spawnFailed = true;
1854
+ });
1855
+ const [exitCode] = await once(build, "close");
1856
+ signal?.removeEventListener("abort", interrupt);
1857
+ if (signal?.aborted) throw new InterruptedError();
1858
+ if (spawnFailed || exitCode !== 0) return failure({
1859
+ kind: "operational",
1860
+ code: "docker-build-failed",
1861
+ observed: `Docker failed to build ${invocation.image}.`,
1862
+ nextAction: "Inspect the Docker output above; the prior Workspace image is unchanged."
1863
+ });
1864
+ return success(void 0);
1865
+ }
1866
+ //#endregion
1867
+ //#region src/cli.ts
1868
+ function createCli(signal, interactive) {
1869
+ const prompt = createConfigurationPrompter({ signal });
1870
+ const cli = cac("devbox");
1871
+ cli.command("").action(() => cli.outputHelp());
1872
+ cli.command("init", "Register the current directory as a Devbox Project.").action(async () => {
1873
+ if (!interactive) return interactiveFailure("devbox init requires an interactive terminal.", "Run devbox init from a TTY before it writes Project state.");
1874
+ console.log();
1875
+ showLogo();
1876
+ console.log();
1877
+ intro(c.bgCyan(" devbox "));
1878
+ const result = await initializeProject({
1879
+ signal,
1880
+ prompt
1881
+ });
1882
+ if (!result.ok) return result;
1883
+ if (result.value.confirmed === false) {
1884
+ outro("Project registration was not changed.");
1885
+ return success({});
1886
+ }
1887
+ if (!result.value.created) {
1888
+ outro("Project is already registered.");
1889
+ return success({});
1890
+ }
1891
+ outro(`Next: run ${c.cyan(c.bold("devbox build"))} to prepare your workspace, then ${c.cyan(c.bold("devbox up"))} to start it.`);
1892
+ return success({});
1893
+ });
1894
+ cli.command("config", "Edit Global or Local configuration for the current Project.").action(async () => {
1895
+ if (!interactive) return interactiveFailure("devbox config requires an interactive terminal.", "Run devbox config from a TTY, or edit the supported Global or Local YAML directly.");
1896
+ intro(c.bgCyan(" devbox "));
1897
+ if (await prompt.selectConfigurationScope() === "global") {
1898
+ const result = await configureGlobal({
1899
+ signal,
1900
+ prompt
1901
+ });
1902
+ if (!result.ok) return result;
1903
+ outro(result.value.changed ? "Global configuration updated." : "Global configuration was not changed.");
1904
+ return success({});
1905
+ }
1906
+ const result = await configureLocalProject({
1907
+ signal,
1908
+ prompt
1909
+ });
1910
+ if (!result.ok) return result;
1911
+ outro(result.value.changed ? "Local configuration updated." : "Local configuration was not changed.");
1912
+ return success({});
1913
+ });
1914
+ cli.command("build", "Build the shared Workspace image from Global configuration.").option("--cache, --no-cache", "Use Docker layer cache.", { default: true }).action(async (options = {}) => {
1915
+ const result = await buildWorkspace({
1916
+ signal,
1917
+ noCache: options.cache === false
1918
+ });
1919
+ if (!result.ok) return result;
1920
+ return success({ message: `Built Workspace image: ${result.value.image}` });
1921
+ });
1922
+ cli.command("rm", "Remove the current registered Project and its state.").option("--yes", "Skip the confirmation prompt.").action(async (options = {}) => {
1923
+ if (!interactive && !options.yes) return interactiveFailure("devbox rm requires a TTY or --yes.", "Run devbox rm from a TTY, or pass --yes to confirm removal.");
1924
+ const result = await removeProject({
1925
+ signal,
1926
+ confirm: prompt.confirm,
1927
+ yes: options.yes
1928
+ });
1929
+ if (!result.ok) return result;
1930
+ return success({ message: result.value.removed ? `Removed Project: ${result.value.root}` : "Project removal was not changed." });
1931
+ });
1932
+ cli.command("cleanup", "Remove explicitly selected disposable Devbox state.").option("--missing-projects", "Remove Missing-root Project registrations.").option("--yes", "Skip the confirmation prompt.").action(async (options = {}) => {
1933
+ if (!options.missingProjects) return failure({
1934
+ kind: "usage",
1935
+ code: "cleanup-mode-required",
1936
+ observed: "cleanup requires --missing-projects.",
1937
+ nextAction: "Run devbox cleanup --missing-projects."
1938
+ });
1939
+ if (!interactive && !options.yes) return interactiveFailure("devbox cleanup --missing-projects requires a TTY or --yes.", "Run devbox cleanup --missing-projects from a TTY, or pass --yes to confirm removal.");
1940
+ const result = await cleanupMissingProjects({
1941
+ signal,
1942
+ confirm: prompt.confirm,
1943
+ yes: options.yes
1944
+ });
1945
+ if (!result.ok) return result;
1946
+ return success({ message: result.value.removed ? `Removed Missing-root Projects: ${result.value.roots.join(", ")}` : "No Missing-root Projects were removed." });
1947
+ });
1948
+ for (const [command, description] of [
1949
+ ["up", "Start the current Project Sandbox."],
1950
+ ["down", "Stop and remove the current Project Sandbox."],
1951
+ ["stop", "Stop the current Project Sandbox."],
1952
+ ["sh", "Open a shell in the current Project Sandbox."]
1953
+ ]) cli.command(command, description).action(async () => {
1954
+ const result = await runSandboxLifecycle(command, { signal });
1955
+ return result.ok ? success({}) : result;
1956
+ });
1957
+ cli.help();
1958
+ return cli;
1959
+ }
1960
+ function present(result) {
1961
+ if (!result.ok) {
1962
+ process.stderr.write(`${result.error.observed}\n${result.error.nextAction}\n`);
1963
+ return result.error.kind === "usage" ? 2 : 1;
1964
+ }
1965
+ if (result.value.message !== void 0) process.stdout.write(`${result.value.message}\n`);
1966
+ return 0;
1967
+ }
1968
+ async function main() {
1969
+ const abortController = new AbortController();
1970
+ const interrupt = () => abortController.abort();
1971
+ process.once("SIGINT", interrupt);
1972
+ const cli = createCli(abortController.signal, process.stdin.isTTY === true && process.stdout.isTTY === true);
1973
+ try {
1974
+ cli.parse(process.argv, { run: false });
1975
+ const result = await cli.runMatchedCommand();
1976
+ return result === void 0 ? 0 : present(result);
1977
+ } catch (error) {
1978
+ if (error instanceof InterruptedError || abortController.signal.aborted) {
1979
+ process.stderr.write("Devbox command interrupted.\n");
1980
+ return 130;
1981
+ }
1982
+ if (error instanceof HostCommandError) return error.exitCode;
1983
+ throw error;
1984
+ } finally {
1985
+ process.off("SIGINT", interrupt);
1986
+ }
1987
+ }
1988
+ function interactiveFailure(observed, nextAction) {
1989
+ return failure({
1990
+ kind: "usage",
1991
+ code: "interactive-terminal-required",
1992
+ observed,
1993
+ nextAction
1994
+ });
1995
+ }
1996
+ process.exitCode = await main();
1997
+ //#endregion
1998
+ export {};