@boxes-dev/dvb 0.2.4

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 (201) hide show
  1. package/dist/bin/dvb.cjs +28232 -0
  2. package/dist/bin/dvb.d.ts +2 -0
  3. package/dist/bin/dvb.d.ts.map +1 -0
  4. package/dist/bin/dvb.js +71 -0
  5. package/dist/bin/dvb.js.map +1 -0
  6. package/dist/bin/dvbd.cjs +11816 -0
  7. package/dist/bin/dvbd.d.ts +2 -0
  8. package/dist/bin/dvbd.d.ts.map +1 -0
  9. package/dist/bin/dvbd.js +20 -0
  10. package/dist/bin/dvbd.js.map +1 -0
  11. package/dist/codex/services-schema.json +59 -0
  12. package/dist/codex/setup-schema.json +176 -0
  13. package/dist/devbox/auth.d.ts +17 -0
  14. package/dist/devbox/auth.d.ts.map +1 -0
  15. package/dist/devbox/auth.js +302 -0
  16. package/dist/devbox/auth.js.map +1 -0
  17. package/dist/devbox/cli.d.ts +2 -0
  18. package/dist/devbox/cli.d.ts.map +1 -0
  19. package/dist/devbox/cli.js +142 -0
  20. package/dist/devbox/cli.js.map +1 -0
  21. package/dist/devbox/commands/agent.d.ts +21 -0
  22. package/dist/devbox/commands/agent.d.ts.map +1 -0
  23. package/dist/devbox/commands/agent.js +257 -0
  24. package/dist/devbox/commands/agent.js.map +1 -0
  25. package/dist/devbox/commands/boxSelect.d.ts +14 -0
  26. package/dist/devbox/commands/boxSelect.d.ts.map +1 -0
  27. package/dist/devbox/commands/boxSelect.js +82 -0
  28. package/dist/devbox/commands/boxSelect.js.map +1 -0
  29. package/dist/devbox/commands/connect.d.ts +8 -0
  30. package/dist/devbox/commands/connect.d.ts.map +1 -0
  31. package/dist/devbox/commands/connect.js +1369 -0
  32. package/dist/devbox/commands/connect.js.map +1 -0
  33. package/dist/devbox/commands/daemon.d.ts +4 -0
  34. package/dist/devbox/commands/daemon.d.ts.map +1 -0
  35. package/dist/devbox/commands/daemon.js +36 -0
  36. package/dist/devbox/commands/daemon.js.map +1 -0
  37. package/dist/devbox/commands/destroy.d.ts +2 -0
  38. package/dist/devbox/commands/destroy.d.ts.map +1 -0
  39. package/dist/devbox/commands/destroy.js +131 -0
  40. package/dist/devbox/commands/destroy.js.map +1 -0
  41. package/dist/devbox/commands/init/args.d.ts +12 -0
  42. package/dist/devbox/commands/init/args.d.ts.map +1 -0
  43. package/dist/devbox/commands/init/args.js +42 -0
  44. package/dist/devbox/commands/init/args.js.map +1 -0
  45. package/dist/devbox/commands/init/codex/artifacts.d.ts +26 -0
  46. package/dist/devbox/commands/init/codex/artifacts.d.ts.map +1 -0
  47. package/dist/devbox/commands/init/codex/artifacts.js +108 -0
  48. package/dist/devbox/commands/init/codex/artifacts.js.map +1 -0
  49. package/dist/devbox/commands/init/codex/index.d.ts +35 -0
  50. package/dist/devbox/commands/init/codex/index.d.ts.map +1 -0
  51. package/dist/devbox/commands/init/codex/index.js +147 -0
  52. package/dist/devbox/commands/init/codex/index.js.map +1 -0
  53. package/dist/devbox/commands/init/codex/local.d.ts +19 -0
  54. package/dist/devbox/commands/init/codex/local.d.ts.map +1 -0
  55. package/dist/devbox/commands/init/codex/local.js +189 -0
  56. package/dist/devbox/commands/init/codex/local.js.map +1 -0
  57. package/dist/devbox/commands/init/codex/plan.d.ts +61 -0
  58. package/dist/devbox/commands/init/codex/plan.d.ts.map +1 -0
  59. package/dist/devbox/commands/init/codex/plan.js +39 -0
  60. package/dist/devbox/commands/init/codex/plan.js.map +1 -0
  61. package/dist/devbox/commands/init/codex/prompts.d.ts +6 -0
  62. package/dist/devbox/commands/init/codex/prompts.d.ts.map +1 -0
  63. package/dist/devbox/commands/init/codex/prompts.js +26 -0
  64. package/dist/devbox/commands/init/codex/prompts.js.map +1 -0
  65. package/dist/devbox/commands/init/codex/remote.d.ts +56 -0
  66. package/dist/devbox/commands/init/codex/remote.d.ts.map +1 -0
  67. package/dist/devbox/commands/init/codex/remote.js +395 -0
  68. package/dist/devbox/commands/init/codex/remote.js.map +1 -0
  69. package/dist/devbox/commands/init/codex/template.d.ts +3 -0
  70. package/dist/devbox/commands/init/codex/template.d.ts.map +1 -0
  71. package/dist/devbox/commands/init/codex/template.js +3 -0
  72. package/dist/devbox/commands/init/codex/template.js.map +1 -0
  73. package/dist/devbox/commands/init/index.d.ts +2 -0
  74. package/dist/devbox/commands/init/index.d.ts.map +1 -0
  75. package/dist/devbox/commands/init/index.js +1117 -0
  76. package/dist/devbox/commands/init/index.js.map +1 -0
  77. package/dist/devbox/commands/init/packaging.d.ts +5 -0
  78. package/dist/devbox/commands/init/packaging.d.ts.map +1 -0
  79. package/dist/devbox/commands/init/packaging.js +86 -0
  80. package/dist/devbox/commands/init/packaging.js.map +1 -0
  81. package/dist/devbox/commands/init/registry.d.ts +14 -0
  82. package/dist/devbox/commands/init/registry.d.ts.map +1 -0
  83. package/dist/devbox/commands/init/registry.js +61 -0
  84. package/dist/devbox/commands/init/registry.js.map +1 -0
  85. package/dist/devbox/commands/init/remote.d.ts +38 -0
  86. package/dist/devbox/commands/init/remote.d.ts.map +1 -0
  87. package/dist/devbox/commands/init/remote.js +554 -0
  88. package/dist/devbox/commands/init/remote.js.map +1 -0
  89. package/dist/devbox/commands/init/repo.d.ts +31 -0
  90. package/dist/devbox/commands/init/repo.d.ts.map +1 -0
  91. package/dist/devbox/commands/init/repo.js +164 -0
  92. package/dist/devbox/commands/init/repo.js.map +1 -0
  93. package/dist/devbox/commands/init/scripts.d.ts +3 -0
  94. package/dist/devbox/commands/init/scripts.d.ts.map +1 -0
  95. package/dist/devbox/commands/init/scripts.js +15 -0
  96. package/dist/devbox/commands/init/scripts.js.map +1 -0
  97. package/dist/devbox/commands/init/ssh.d.ts +19 -0
  98. package/dist/devbox/commands/init/ssh.d.ts.map +1 -0
  99. package/dist/devbox/commands/init/ssh.js +270 -0
  100. package/dist/devbox/commands/init/ssh.js.map +1 -0
  101. package/dist/devbox/commands/init/state.d.ts +31 -0
  102. package/dist/devbox/commands/init/state.d.ts.map +1 -0
  103. package/dist/devbox/commands/init/state.js +25 -0
  104. package/dist/devbox/commands/init/state.js.map +1 -0
  105. package/dist/devbox/commands/list.d.ts +2 -0
  106. package/dist/devbox/commands/list.d.ts.map +1 -0
  107. package/dist/devbox/commands/list.js +213 -0
  108. package/dist/devbox/commands/list.js.map +1 -0
  109. package/dist/devbox/commands/listFormatting.d.ts +27 -0
  110. package/dist/devbox/commands/listFormatting.d.ts.map +1 -0
  111. package/dist/devbox/commands/listFormatting.js +155 -0
  112. package/dist/devbox/commands/listFormatting.js.map +1 -0
  113. package/dist/devbox/commands/logout.d.ts +2 -0
  114. package/dist/devbox/commands/logout.d.ts.map +1 -0
  115. package/dist/devbox/commands/logout.js +45 -0
  116. package/dist/devbox/commands/logout.js.map +1 -0
  117. package/dist/devbox/commands/mount.d.ts +3 -0
  118. package/dist/devbox/commands/mount.d.ts.map +1 -0
  119. package/dist/devbox/commands/mount.js +144 -0
  120. package/dist/devbox/commands/mount.js.map +1 -0
  121. package/dist/devbox/commands/mountSsh.d.ts +12 -0
  122. package/dist/devbox/commands/mountSsh.d.ts.map +1 -0
  123. package/dist/devbox/commands/mountSsh.js +137 -0
  124. package/dist/devbox/commands/mountSsh.js.map +1 -0
  125. package/dist/devbox/commands/ports.d.ts +2 -0
  126. package/dist/devbox/commands/ports.d.ts.map +1 -0
  127. package/dist/devbox/commands/ports.js +145 -0
  128. package/dist/devbox/commands/ports.js.map +1 -0
  129. package/dist/devbox/commands/services.d.ts +2 -0
  130. package/dist/devbox/commands/services.d.ts.map +1 -0
  131. package/dist/devbox/commands/services.js +552 -0
  132. package/dist/devbox/commands/services.js.map +1 -0
  133. package/dist/devbox/commands/servicesToml.d.ts +13 -0
  134. package/dist/devbox/commands/servicesToml.d.ts.map +1 -0
  135. package/dist/devbox/commands/servicesToml.js +198 -0
  136. package/dist/devbox/commands/servicesToml.js.map +1 -0
  137. package/dist/devbox/commands/sessionUtils.d.ts +10 -0
  138. package/dist/devbox/commands/sessionUtils.d.ts.map +1 -0
  139. package/dist/devbox/commands/sessionUtils.js +48 -0
  140. package/dist/devbox/commands/sessionUtils.js.map +1 -0
  141. package/dist/devbox/commands/sessions.d.ts +2 -0
  142. package/dist/devbox/commands/sessions.d.ts.map +1 -0
  143. package/dist/devbox/commands/sessions.js +425 -0
  144. package/dist/devbox/commands/sessions.js.map +1 -0
  145. package/dist/devbox/commands/setup.d.ts +2 -0
  146. package/dist/devbox/commands/setup.d.ts.map +1 -0
  147. package/dist/devbox/commands/setup.js +183 -0
  148. package/dist/devbox/commands/setup.js.map +1 -0
  149. package/dist/devbox/commands/wezterm.d.ts +3 -0
  150. package/dist/devbox/commands/wezterm.d.ts.map +1 -0
  151. package/dist/devbox/commands/wezterm.js +878 -0
  152. package/dist/devbox/commands/wezterm.js.map +1 -0
  153. package/dist/devbox/completions/cache.d.ts +59 -0
  154. package/dist/devbox/completions/cache.d.ts.map +1 -0
  155. package/dist/devbox/completions/cache.js +122 -0
  156. package/dist/devbox/completions/cache.js.map +1 -0
  157. package/dist/devbox/completions/index.d.ts +14 -0
  158. package/dist/devbox/completions/index.d.ts.map +1 -0
  159. package/dist/devbox/completions/index.js +796 -0
  160. package/dist/devbox/completions/index.js.map +1 -0
  161. package/dist/devbox/controlPlane.d.ts +44 -0
  162. package/dist/devbox/controlPlane.d.ts.map +1 -0
  163. package/dist/devbox/controlPlane.js +310 -0
  164. package/dist/devbox/controlPlane.js.map +1 -0
  165. package/dist/devbox/daemonClient.d.ts +28 -0
  166. package/dist/devbox/daemonClient.d.ts.map +1 -0
  167. package/dist/devbox/daemonClient.js +276 -0
  168. package/dist/devbox/daemonClient.js.map +1 -0
  169. package/dist/devbox/latency.d.ts +25 -0
  170. package/dist/devbox/latency.d.ts.map +1 -0
  171. package/dist/devbox/latency.js +186 -0
  172. package/dist/devbox/latency.js.map +1 -0
  173. package/dist/devbox/logger.d.ts +2 -0
  174. package/dist/devbox/logger.d.ts.map +1 -0
  175. package/dist/devbox/logger.js +3 -0
  176. package/dist/devbox/logger.js.map +1 -0
  177. package/dist/devbox/terminal/osc.d.ts +10 -0
  178. package/dist/devbox/terminal/osc.d.ts.map +1 -0
  179. package/dist/devbox/terminal/osc.js +88 -0
  180. package/dist/devbox/terminal/osc.js.map +1 -0
  181. package/dist/devbox/types.d.ts +4 -0
  182. package/dist/devbox/types.d.ts.map +1 -0
  183. package/dist/devbox/types.js +2 -0
  184. package/dist/devbox/types.js.map +1 -0
  185. package/dist/devbox/ui/colors.d.ts +19 -0
  186. package/dist/devbox/ui/colors.d.ts.map +1 -0
  187. package/dist/devbox/ui/colors.js +22 -0
  188. package/dist/devbox/ui/colors.js.map +1 -0
  189. package/dist/devbox/ui/statusLine.d.ts +14 -0
  190. package/dist/devbox/ui/statusLine.d.ts.map +1 -0
  191. package/dist/devbox/ui/statusLine.js +90 -0
  192. package/dist/devbox/ui/statusLine.js.map +1 -0
  193. package/dist/devbox/weztermMux.d.ts +11 -0
  194. package/dist/devbox/weztermMux.d.ts.map +1 -0
  195. package/dist/devbox/weztermMux.js +11 -0
  196. package/dist/devbox/weztermMux.js.map +1 -0
  197. package/dist/prompts/local-scan.md +32 -0
  198. package/dist/prompts/local-services-scan.md +17 -0
  199. package/dist/prompts/remote-apply.md +370 -0
  200. package/dist/scripts/fix-bashrc.sh +48 -0
  201. package/package.json +38 -0
@@ -0,0 +1,1117 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import path from "node:path";
3
+ import fs from "node:fs/promises";
4
+ import os from "node:os";
5
+ import readline from "node:readline/promises";
6
+ import { resolveSocketInfo } from "@boxes-dev/core";
7
+ import { createSecretStore, loadConfig, createSpritesClient, fingerprintFromOrigin, fingerprintFromRootCommit, normalizeGitRemoteUrl, resolveSpritesApiUrl, SpritesApiError, slugify, } from "@boxes-dev/core";
8
+ import { DAEMON_TIMEOUT_MS, ensureDaemonRunning, requestJson, requireDaemonFeatures, } from "../../daemonClient.js";
9
+ import { ensureSpritesToken } from "../../auth.js";
10
+ import { fetchSpriteDaemonRelease, getConvexUrl, issueSpriteDaemonToken, } from "../../controlPlane.js";
11
+ import { logger } from "../../logger.js";
12
+ import { createStatusLine } from "../../ui/statusLine.js";
13
+ import { parseInitArgs } from "./args.js";
14
+ import { confirmCopyWorktree, findRepoRoot, mapGlobalGitConfigDestinations, readGlobalGitConfigFiles, readHeadState, readNullSeparatedPaths, readRepoOrigin, readRootCommit, readWorktreeState, resolveGitCommonDir, runCommand, } from "./repo.js";
15
+ import { createFileListArchive, createGitMetaArchive, writePatch } from "./packaging.js";
16
+ import { bootstrapDevbox, ensureSpriteDaemonService, expandHome, ensureWeztermMuxService, installSpriteDaemon, installWeztermMux, shellQuote, writeRemoteCodexConfig, } from "./remote.js";
17
+ import { ensureDevboxToml, readRepoMarker, writeRepoMarker } from "./registry.js";
18
+ import { readInitState, writeInitState } from "./state.js";
19
+ import { ensureSshConfig, ensureSshKey, copyToClipboard, openBrowser, parseGitRemote, readRemoteOrigin, setRemoteOrigin, verifySshAuth, } from "./ssh.js";
20
+ import { ensureKnownHostsFile, ensureLocalMountKey, ensureRemoteMountAccess, ensureSshdService, readLocalMountPublicKey, } from "../mountSsh.js";
21
+ import { createSetupArtifacts, promptForPlanApproval, promptForServicesApproval, readSetupPlan, readServicesPlan, runLocalServicesScan, runLocalSetupScan, runRemoteCodexSetup, uploadSetupPlan, writeSetupPlan, writeSetupSchema, writeServicesPlan, writeServicesSchema, } from "./codex/index.js";
22
+ import { mergeServicesToml, splitShellCommand, } from "../servicesToml.js";
23
+ const resolveInitStatus = (steps, complete) => {
24
+ if (complete || steps?.codexApplied)
25
+ return "complete";
26
+ if (steps?.setupUploaded && steps?.workdirProvisioned) {
27
+ return "remote_ready";
28
+ }
29
+ return "local_required";
30
+ };
31
+ const mergeLocalPaths = (existing, repoRoot) => {
32
+ const merged = new Set(existing ?? []);
33
+ merged.add(repoRoot);
34
+ return [...merged];
35
+ };
36
+ const buildServicesTomlUpdates = (services) => {
37
+ const updates = {};
38
+ for (const service of services) {
39
+ const name = service.name.trim();
40
+ if (!name) {
41
+ throw new Error("Service name is required in services.json.");
42
+ }
43
+ const parts = splitShellCommand(service.command ?? "");
44
+ const [cmd, ...args] = parts;
45
+ if (!cmd) {
46
+ throw new Error(`Service "${name}" is missing a command.`);
47
+ }
48
+ updates[name] = {
49
+ name,
50
+ cmd,
51
+ ...(args.length > 0 ? { args } : {}),
52
+ ...(service.httpPort !== null ? { httpPort: service.httpPort } : {}),
53
+ };
54
+ }
55
+ return updates;
56
+ };
57
+ const writeRemoteServicesToml = async ({ client, canonical, workdir, services, }) => {
58
+ if (services.length === 0)
59
+ return;
60
+ let baseContent = "";
61
+ try {
62
+ const bytes = await client.readFile(canonical, {
63
+ path: "devbox.toml",
64
+ workingDir: workdir,
65
+ });
66
+ baseContent = Buffer.from(bytes).toString("utf8");
67
+ }
68
+ catch (error) {
69
+ if (!(error instanceof SpritesApiError && error.status === 404)) {
70
+ throw error;
71
+ }
72
+ }
73
+ const updates = buildServicesTomlUpdates(services);
74
+ if (Object.keys(updates).length === 0)
75
+ return;
76
+ const merged = mergeServicesToml(baseContent, updates);
77
+ await client.writeFile(canonical, path.posix.join(workdir.replace(/\/$/, ""), "devbox.toml"), Buffer.from(merged));
78
+ };
79
+ const promptYesNo = async (question, defaultYes = false) => {
80
+ if (!process.stdin.isTTY) {
81
+ return defaultYes;
82
+ }
83
+ const suffix = defaultYes ? " (Y/n): " : " (y/N): ";
84
+ const rl = readline.createInterface({
85
+ input: process.stdin,
86
+ output: process.stdout,
87
+ });
88
+ const answer = await rl.question(`${question}${suffix}`);
89
+ rl.close();
90
+ const normalized = answer.trim().toLowerCase();
91
+ if (!normalized)
92
+ return defaultYes;
93
+ return normalized === "y" || normalized === "yes";
94
+ };
95
+ const promptToContinue = async (message) => {
96
+ if (!process.stdin.isTTY) {
97
+ return false;
98
+ }
99
+ const rl = readline.createInterface({
100
+ input: process.stdin,
101
+ output: process.stdout,
102
+ });
103
+ await rl.question(message);
104
+ rl.close();
105
+ return true;
106
+ };
107
+ const ensureWorkdirOwnership = async ({ client, canonical, workdir, status, json, }) => {
108
+ const checkResult = await client.exec(canonical, [
109
+ "/bin/bash",
110
+ "-lc",
111
+ `if [ -d ${shellQuote(workdir)} ]; then stat -c %U ${shellQuote(workdir)}; fi`,
112
+ ]);
113
+ if (checkResult.exitCode !== 0)
114
+ return;
115
+ const owner = checkResult.stdout.trim();
116
+ if (!owner || owner === "sprite")
117
+ return;
118
+ status.stage("Fixing workdir ownership");
119
+ const chownResult = await client.exec(canonical, [
120
+ "/bin/bash",
121
+ "-lc",
122
+ `sudo -n chown -R sprite:sprite ${shellQuote(workdir)}`,
123
+ ]);
124
+ if (chownResult.exitCode !== 0) {
125
+ logger.warn("workdir_chown_failed", {
126
+ box: canonical,
127
+ workdir,
128
+ error: chownResult.stderr || chownResult.stdout,
129
+ });
130
+ if (!json) {
131
+ status.stop();
132
+ console.warn("Warning: failed to update workdir ownership. Git may refuse to operate until ownership is fixed.");
133
+ }
134
+ }
135
+ };
136
+ export const runInit = async (args) => {
137
+ const parsed = parseInitArgs(args);
138
+ const statusEnabled = process.stderr.isTTY && !parsed.json;
139
+ const status = createStatusLine({ enabled: statusEnabled });
140
+ const run = async () => {
141
+ status.stage("Detecting repository");
142
+ const cwd = process.cwd();
143
+ const repoRoot = await findRepoRoot(cwd);
144
+ const repoName = path.basename(repoRoot);
145
+ const slug = slugify(repoName);
146
+ const localHomeDir = process.env.HOME ?? os.homedir();
147
+ const repoMarker = await readRepoMarker(repoRoot);
148
+ const origin = await readRepoOrigin(repoRoot);
149
+ const normalizedOrigin = origin ? normalizeGitRemoteUrl(origin) : null;
150
+ const fingerprint = origin
151
+ ? fingerprintFromOrigin(origin)
152
+ : repoMarker?.fingerprint ??
153
+ fingerprintFromRootCommit(await readRootCommit(repoRoot), randomUUID());
154
+ let initState = await readInitState(repoRoot);
155
+ const initFingerprintMismatch = Boolean(initState?.fingerprint) && initState?.fingerprint !== fingerprint;
156
+ if (parsed.resume) {
157
+ if (initFingerprintMismatch) {
158
+ throw new Error("Init state does not match this repo. Remove .devbox/init-state.json and run `dvb init` again.");
159
+ }
160
+ if (!initState) {
161
+ throw new Error("No init state to resume. Run `dvb init` to start a new init.");
162
+ }
163
+ if (initState.complete) {
164
+ throw new Error("Init already completed. Run `dvb init` to start a new init.");
165
+ }
166
+ }
167
+ if (initFingerprintMismatch) {
168
+ initState = null;
169
+ }
170
+ const wantsResume = Boolean(parsed.resume);
171
+ if (parsed.force && initState && !initState.complete && !wantsResume) {
172
+ initState = null;
173
+ }
174
+ const shouldResume = Boolean(wantsResume && initState && !initState.complete);
175
+ if (!parsed.codexSetupOnly &&
176
+ !wantsResume &&
177
+ initState &&
178
+ !initState.complete &&
179
+ !parsed.force) {
180
+ throw new Error("Previous init is incomplete. Run `dvb init --resume` to finish the previous init or `dvb init --force` to restart.");
181
+ }
182
+ const ensureInitState = () => {
183
+ if (!initState || initState.fingerprint !== fingerprint) {
184
+ initState = {
185
+ version: 1,
186
+ fingerprint,
187
+ steps: {},
188
+ updatedAt: new Date().toISOString(),
189
+ };
190
+ }
191
+ return initState;
192
+ };
193
+ const updateInitState = async (update) => {
194
+ const base = ensureInitState();
195
+ initState = {
196
+ ...base,
197
+ ...update,
198
+ steps: {
199
+ ...base.steps,
200
+ ...(update.steps ?? {}),
201
+ },
202
+ updatedAt: new Date().toISOString(),
203
+ };
204
+ await writeInitState(repoRoot, initState);
205
+ };
206
+ if (parsed.codexSetupOnly) {
207
+ if (!repoMarker?.canonical) {
208
+ throw new Error("Repo is not initialized. Run `dvb init` first.");
209
+ }
210
+ const setupDir = path.join(repoRoot, ".devbox");
211
+ const setupPath = path.join(setupDir, "setup.json");
212
+ const servicesPath = path.join(setupDir, "services.json");
213
+ try {
214
+ await fs.access(setupPath);
215
+ }
216
+ catch {
217
+ throw new Error("Missing .devbox/setup.json. Run `dvb init` first.");
218
+ }
219
+ const localArtifactsBundlePath = path.join(setupDir, "setup-artifacts.tgz");
220
+ const localArtifactsManifestPath = path.join(setupDir, "setup-artifacts.json");
221
+ let artifactsBundlePath = null;
222
+ let artifactsManifestPath = null;
223
+ try {
224
+ await fs.access(localArtifactsBundlePath);
225
+ await fs.access(localArtifactsManifestPath);
226
+ artifactsBundlePath = localArtifactsBundlePath;
227
+ artifactsManifestPath = localArtifactsManifestPath;
228
+ }
229
+ catch {
230
+ // artifacts are optional in setup-only flow
231
+ }
232
+ status.stage("Starting dvbd");
233
+ const socketInfo = resolveSocketInfo();
234
+ await ensureDaemonRunning(socketInfo.socketPath);
235
+ await requireDaemonFeatures(socketInfo.socketPath, ["ports"]);
236
+ status.stage("Loading devbox config");
237
+ const config = await loadConfig(process.env.HOME ? { homeDir: process.env.HOME } : undefined);
238
+ const store = await createSecretStore(config?.tokenStore, process.env.HOME ? { homeDir: process.env.HOME } : undefined);
239
+ const token = await store.getToken();
240
+ if (!token) {
241
+ throw new Error("Sprites token missing. Run `dvb setup` first.");
242
+ }
243
+ const apiBaseUrl = resolveSpritesApiUrl(config);
244
+ const client = createSpritesClient({
245
+ apiBaseUrl,
246
+ token,
247
+ });
248
+ const canonical = repoMarker.canonical;
249
+ let expandedWorkdir = expandHome(`~/${slug}`);
250
+ try {
251
+ const metaRaw = await client.readFile(canonical, {
252
+ path: `/home/sprite/.devbox/projects/${fingerprint}.json`,
253
+ });
254
+ const meta = JSON.parse(Buffer.from(metaRaw).toString("utf8"));
255
+ if (meta.workdir) {
256
+ expandedWorkdir = expandHome(meta.workdir);
257
+ }
258
+ }
259
+ catch {
260
+ // ignore missing project metadata
261
+ }
262
+ const remoteSetupPath = path.posix.join(expandedWorkdir, ".devbox", "setup.json");
263
+ const remoteArtifactsBundlePath = path.posix.join(expandedWorkdir, ".devbox", "setup-artifacts.tgz");
264
+ const remoteArtifactsManifestPath = path.posix.join(expandedWorkdir, ".devbox", "setup-artifacts.json");
265
+ const pathSetup = 'export PATH="$(npm bin -g 2>/dev/null):$PATH"';
266
+ let servicesPlan = null;
267
+ try {
268
+ servicesPlan = await readServicesPlan(servicesPath);
269
+ }
270
+ catch {
271
+ servicesPlan = null;
272
+ }
273
+ await uploadSetupPlan({
274
+ client,
275
+ canonical,
276
+ localSetupPath: setupPath,
277
+ remoteSetupPath,
278
+ localArtifactsBundlePath: artifactsBundlePath,
279
+ localArtifactsManifestPath: artifactsManifestPath,
280
+ remoteArtifactsBundlePath: artifactsBundlePath
281
+ ? remoteArtifactsBundlePath
282
+ : null,
283
+ remoteArtifactsManifestPath: artifactsBundlePath
284
+ ? remoteArtifactsManifestPath
285
+ : null,
286
+ status,
287
+ });
288
+ await runRemoteCodexSetup({
289
+ client,
290
+ canonical,
291
+ expandedWorkdir,
292
+ remoteSetupPath,
293
+ remoteArtifactsBundlePath,
294
+ remoteArtifactsManifestPath,
295
+ socketInfo,
296
+ status,
297
+ pathSetup,
298
+ entrypoints: servicesPlan?.appEntrypoints ?? [],
299
+ });
300
+ status.stop();
301
+ if (parsed.json) {
302
+ console.log(JSON.stringify({ ok: true }, null, 2));
303
+ return;
304
+ }
305
+ console.log("Codex setup complete.");
306
+ return;
307
+ }
308
+ status.stage("Starting dvbd");
309
+ const socketInfo = resolveSocketInfo();
310
+ await ensureDaemonRunning(socketInfo.socketPath);
311
+ await requireDaemonFeatures(socketInfo.socketPath, ["registry"]);
312
+ status.stage("Checking sprites");
313
+ const existingProject = await requestJson(socketInfo.socketPath, "GET", `/registry/project?fingerprint=${encodeURIComponent(fingerprint)}`, DAEMON_TIMEOUT_MS.registry);
314
+ const existingEntry = existingProject.body.project ?? null;
315
+ if (existingEntry && !parsed.force) {
316
+ if (shouldResume) {
317
+ if (initState?.canonical &&
318
+ existingEntry.canonical !== initState.canonical) {
319
+ throw new Error(`Repo already initialized (box: ${existingEntry.canonical}) and does not match pending init (box: ${initState.canonical}).`);
320
+ }
321
+ }
322
+ else if (origin) {
323
+ const displayOrigin = normalizedOrigin ?? origin;
324
+ throw new Error(`Repo already initialized for origin ${JSON.stringify(displayOrigin)} (box: ${existingEntry.canonical}).`);
325
+ }
326
+ else {
327
+ throw new Error(`Repo already initialized (box: ${existingEntry.canonical}).`);
328
+ }
329
+ }
330
+ const alias = parsed.alias ?? initState?.alias ?? existingEntry?.alias ?? slug;
331
+ const canonicalHint = parsed.name ?? initState?.canonical ?? existingEntry?.canonical;
332
+ const workdir = `~/${slug}`;
333
+ const expandedWorkdir = expandHome(workdir);
334
+ const remoteSetupPath = path.posix.join(expandedWorkdir, ".devbox", "setup.json");
335
+ const remoteArtifactsBundlePath = path.posix.join(expandedWorkdir, ".devbox", "setup-artifacts.tgz");
336
+ const remoteArtifactsManifestPath = path.posix.join(expandedWorkdir, ".devbox", "setup-artifacts.json");
337
+ const pathSetup = 'export PATH="$(npm bin -g 2>/dev/null):$PATH"';
338
+ status.stage("Loading devbox config");
339
+ const config = await loadConfig(process.env.HOME ? { homeDir: process.env.HOME } : undefined);
340
+ const store = await createSecretStore(config?.tokenStore, process.env.HOME ? { homeDir: process.env.HOME } : undefined);
341
+ const { token, controlPlaneToken } = await ensureSpritesToken(store, (message) => status.stage(message));
342
+ const apiBaseUrl = resolveSpritesApiUrl(config);
343
+ const client = createSpritesClient({
344
+ apiBaseUrl,
345
+ token,
346
+ });
347
+ const username = os.userInfo().username;
348
+ let canonical = (shouldResume && initState?.canonical ? initState.canonical : null) ??
349
+ canonicalHint ??
350
+ `${username}-${slug}`;
351
+ const createSprite = async (name) => {
352
+ try {
353
+ await client.createSprite(name);
354
+ return "created";
355
+ }
356
+ catch (error) {
357
+ if (error instanceof SpritesApiError) {
358
+ const body = error.body.toLowerCase();
359
+ if (error.status === 400 ||
360
+ error.status === 409 ||
361
+ body.includes("exists") ||
362
+ body.includes("already")) {
363
+ return "exists";
364
+ }
365
+ }
366
+ throw error;
367
+ }
368
+ };
369
+ const skipCreate = shouldResume && initState?.steps.spritesCreated && initState.canonical;
370
+ if (!skipCreate) {
371
+ status.stage("Creating devbox");
372
+ const createResult = await createSprite(canonical);
373
+ if (createResult === "exists" && !parsed.force) {
374
+ if (canonicalHint) {
375
+ throw new Error(`Sprite already exists: ${canonical}`);
376
+ }
377
+ const suffix = fingerprint.slice(0, 6);
378
+ canonical = `${canonical}-${suffix}`;
379
+ status.stage("Resolving devbox name");
380
+ const second = await createSprite(canonical);
381
+ if (second === "exists") {
382
+ throw new Error(`Sprite already exists: ${canonical}`);
383
+ }
384
+ }
385
+ await updateInitState({
386
+ canonical,
387
+ alias,
388
+ workdir,
389
+ steps: { spritesCreated: true },
390
+ });
391
+ }
392
+ const aliasLookup = await requestJson(socketInfo.socketPath, "GET", `/registry/alias?alias=${encodeURIComponent(alias)}`, DAEMON_TIMEOUT_MS.registry);
393
+ if (aliasLookup.body.canonical &&
394
+ aliasLookup.body.canonical !== canonical &&
395
+ !parsed.force) {
396
+ throw new Error(`Alias already in use: ${alias}`);
397
+ }
398
+ const projectCreatedAt = existingEntry?.createdAt ?? new Date().toISOString();
399
+ const projectLocalPaths = mergeLocalPaths(existingEntry?.localPaths, repoRoot);
400
+ const projectOrigin = normalizedOrigin ?? existingEntry?.origin ?? undefined;
401
+ const buildProjectEntry = (initStatus) => ({
402
+ fingerprint,
403
+ canonical,
404
+ alias,
405
+ origin: projectOrigin,
406
+ createdAt: projectCreatedAt,
407
+ initStatus,
408
+ initUpdatedAt: new Date().toISOString(),
409
+ localPaths: projectLocalPaths,
410
+ });
411
+ const updateRegistryProjectStatus = async (initStatus) => {
412
+ try {
413
+ await requestJson(socketInfo.socketPath, "POST", "/registry/upsert", DAEMON_TIMEOUT_MS.registry, {
414
+ project: buildProjectEntry(initStatus),
415
+ });
416
+ }
417
+ catch (error) {
418
+ logger.warn("registry_project_update_failed", {
419
+ box: canonical,
420
+ status: initStatus,
421
+ error: String(error),
422
+ });
423
+ }
424
+ };
425
+ status.stage("Bootstrapping devbox");
426
+ try {
427
+ await bootstrapDevbox(client, canonical);
428
+ }
429
+ catch (error) {
430
+ logger.warn("devbox_bootstrap_failed", {
431
+ box: canonical,
432
+ error: String(error),
433
+ });
434
+ if (!parsed.json) {
435
+ status.stop();
436
+ const message = error instanceof Error && error.message
437
+ ? error.message
438
+ : String(error);
439
+ console.warn(`Warning: devbox bootstrap failed. ${message}`);
440
+ }
441
+ }
442
+ const initialStatus = resolveInitStatus(initState?.steps, initState?.complete);
443
+ if (!shouldResume || !initState?.steps.registrySynced) {
444
+ status.stage("Syncing sprites");
445
+ await requestJson(socketInfo.socketPath, "POST", "/registry/upsert", DAEMON_TIMEOUT_MS.registry, {
446
+ project: buildProjectEntry(initialStatus),
447
+ box: {
448
+ canonical,
449
+ org: config?.org,
450
+ createdAt: new Date().toISOString(),
451
+ },
452
+ alias: { alias, canonical },
453
+ });
454
+ await updateInitState({
455
+ canonical,
456
+ alias,
457
+ workdir,
458
+ steps: { registrySynced: true },
459
+ });
460
+ }
461
+ if (!shouldResume || !initState?.steps.daemonInstalled) {
462
+ status.stage("Installing sprite daemon");
463
+ try {
464
+ const convexUrl = getConvexUrl();
465
+ if (!controlPlaneToken) {
466
+ throw new Error("Control plane session required to install daemon.");
467
+ }
468
+ if (!convexUrl) {
469
+ throw new Error("Convex URL unavailable.");
470
+ }
471
+ const release = await fetchSpriteDaemonRelease(controlPlaneToken);
472
+ if (!release) {
473
+ throw new Error("No sprite daemon release available.");
474
+ }
475
+ const heartbeatToken = await issueSpriteDaemonToken(controlPlaneToken, canonical);
476
+ if (!heartbeatToken) {
477
+ throw new Error("Daemon token unavailable.");
478
+ }
479
+ await installSpriteDaemon({
480
+ client,
481
+ spriteName: canonical,
482
+ release,
483
+ convexUrl,
484
+ heartbeatToken,
485
+ });
486
+ await updateInitState({ steps: { daemonInstalled: true } });
487
+ }
488
+ catch (error) {
489
+ logger.warn("sprite_daemon_install_failed", {
490
+ box: canonical,
491
+ error: String(error),
492
+ });
493
+ if (!parsed.json) {
494
+ status.stop();
495
+ const message = error instanceof Error && error.message
496
+ ? error.message
497
+ : String(error);
498
+ console.warn(`Warning: failed to install sprite daemon. ${message}`);
499
+ }
500
+ }
501
+ }
502
+ status.stage("Ensuring sprite daemon service");
503
+ try {
504
+ await ensureSpriteDaemonService({ client, spriteName: canonical });
505
+ await updateInitState({ steps: { daemonServiceEnsured: true } });
506
+ }
507
+ catch (error) {
508
+ logger.warn("sprite_daemon_service_failed", {
509
+ box: canonical,
510
+ error: String(error),
511
+ });
512
+ if (!parsed.json) {
513
+ status.stop();
514
+ const message = error instanceof Error && error.message
515
+ ? error.message
516
+ : String(error);
517
+ console.warn(`Warning: failed to ensure sprite daemon service. ${message}`);
518
+ }
519
+ }
520
+ const setupDir = path.join(repoRoot, ".devbox");
521
+ const setupPath = path.join(setupDir, "setup.json");
522
+ const servicesPath = path.join(setupDir, "services.json");
523
+ let setupArtifacts = null;
524
+ const nonInteractive = !process.stdin.isTTY || parsed.json;
525
+ const skipSetupPlan = shouldResume && initState?.steps.setupPlanWritten;
526
+ const skipServicesPlan = shouldResume && initState?.steps.servicesPlanWritten;
527
+ const skipServicesConfig = shouldResume && initState?.steps.servicesConfigWritten;
528
+ const skipSetupUpload = nonInteractive || (shouldResume && initState?.steps.setupUploaded);
529
+ const skipCodexApply = nonInteractive || (shouldResume && initState?.steps.codexApplied);
530
+ let approvedPlan = null;
531
+ let approvedServices = null;
532
+ const setupTempDir = await fs.mkdtemp(path.join(os.tmpdir(), "devbox-setup-"));
533
+ try {
534
+ await fs.mkdir(setupDir, { recursive: true });
535
+ if (!skipSetupPlan || !skipServicesPlan) {
536
+ let setupSchemaPath = null;
537
+ let servicesSchemaPath = null;
538
+ if (!skipSetupPlan) {
539
+ setupSchemaPath = await writeSetupSchema(setupTempDir);
540
+ }
541
+ if (!skipServicesPlan) {
542
+ servicesSchemaPath = await writeServicesSchema(setupTempDir);
543
+ }
544
+ if (!skipSetupPlan && !setupSchemaPath) {
545
+ throw new Error("Setup schema path missing.");
546
+ }
547
+ if (!skipServicesPlan && !servicesSchemaPath) {
548
+ throw new Error("Services schema path missing.");
549
+ }
550
+ status.stage("Analyzing local environment");
551
+ await Promise.all([
552
+ skipSetupPlan
553
+ ? Promise.resolve()
554
+ : runLocalSetupScan({
555
+ cwd: repoRoot,
556
+ schemaPath: setupSchemaPath,
557
+ outputPath: setupPath,
558
+ homeDir: localHomeDir,
559
+ onProgress: (message) => {
560
+ const base = "Analyzing local environment — setup";
561
+ status.stage(`${base} — ${message}`);
562
+ },
563
+ }),
564
+ skipServicesPlan
565
+ ? Promise.resolve()
566
+ : runLocalServicesScan({
567
+ cwd: repoRoot,
568
+ schemaPath: servicesSchemaPath,
569
+ outputPath: servicesPath,
570
+ homeDir: localHomeDir,
571
+ onProgress: (message) => {
572
+ const base = "Analyzing local environment — services";
573
+ status.stage(`${base} — ${message}`);
574
+ },
575
+ }),
576
+ ]);
577
+ }
578
+ if (!skipSetupPlan) {
579
+ const setupPlan = await readSetupPlan(setupPath);
580
+ if (nonInteractive) {
581
+ approvedPlan = setupPlan;
582
+ }
583
+ else {
584
+ if (!process.stdin.isTTY || parsed.json) {
585
+ throw new Error("Interactive terminal required to approve setup.");
586
+ }
587
+ status.stage("Reviewing setup findings");
588
+ status.stop();
589
+ approvedPlan = await promptForPlanApproval(setupPlan);
590
+ }
591
+ await writeSetupPlan(setupPath, approvedPlan);
592
+ await updateInitState({ steps: { setupPlanWritten: true } });
593
+ }
594
+ else {
595
+ approvedPlan = await readSetupPlan(setupPath);
596
+ }
597
+ if (!skipServicesPlan) {
598
+ const servicesPlan = await readServicesPlan(servicesPath);
599
+ if (nonInteractive) {
600
+ approvedServices = servicesPlan;
601
+ }
602
+ else {
603
+ if (!process.stdin.isTTY || parsed.json) {
604
+ throw new Error("Interactive terminal required to approve services.");
605
+ }
606
+ status.stage("Reviewing run services findings");
607
+ status.stop();
608
+ approvedServices = await promptForServicesApproval(servicesPlan);
609
+ }
610
+ await writeServicesPlan(servicesPath, approvedServices);
611
+ await updateInitState({ steps: { servicesPlanWritten: true } });
612
+ }
613
+ else {
614
+ approvedServices = await readServicesPlan(servicesPath);
615
+ }
616
+ if (!skipSetupUpload) {
617
+ status.stage("Packaging setup artifacts");
618
+ setupArtifacts = await createSetupArtifacts({
619
+ repoRoot,
620
+ plan: approvedPlan,
621
+ outputDir: setupDir,
622
+ tempDir: setupTempDir,
623
+ homeDir: localHomeDir,
624
+ });
625
+ }
626
+ }
627
+ finally {
628
+ await fs.rm(setupTempDir, { recursive: true, force: true });
629
+ }
630
+ const skipProvision = shouldResume && initState?.steps.workdirProvisioned;
631
+ if (!skipProvision || !skipSetupUpload) {
632
+ if (!skipProvision) {
633
+ status.stage("Packaging repo");
634
+ }
635
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "devbox-init-"));
636
+ const bundlePath = path.join(tempDir, "repo.bundle");
637
+ const archivePath = path.join(tempDir, "repo.tgz");
638
+ const gitMetaPath = path.join(tempDir, "git-meta.tgz");
639
+ const gitMetaListPath = path.join(tempDir, "git-meta.list");
640
+ const stagedPatchPath = path.join(tempDir, "staged.patch");
641
+ const unstagedPatchPath = path.join(tempDir, "unstaged.patch");
642
+ const untrackedPath = path.join(tempDir, "untracked.tgz");
643
+ const untrackedListPath = path.join(tempDir, "untracked.list");
644
+ const globalGitConfigSources = await readGlobalGitConfigFiles(repoRoot);
645
+ const globalGitConfigMappings = mapGlobalGitConfigDestinations(globalGitConfigSources, localHomeDir);
646
+ try {
647
+ const remoteBundlePath = "/home/sprite/.devbox/upload.bundle";
648
+ const remoteArchivePath = "/home/sprite/.devbox/upload.tgz";
649
+ const remoteGitMetaPath = "/home/sprite/.devbox/git-meta.tgz";
650
+ const remoteStagedPatchPath = "/home/sprite/.devbox/staged.patch";
651
+ const remoteUnstagedPatchPath = "/home/sprite/.devbox/unstaged.patch";
652
+ const remoteUntrackedPath = "/home/sprite/.devbox/untracked.tgz";
653
+ status.stage("Checking remote git");
654
+ const gitCheck = await client.exec(canonical, [
655
+ "/bin/bash",
656
+ "-lc",
657
+ "git --version",
658
+ ]);
659
+ const remoteHasGit = gitCheck.exitCode === 0;
660
+ const remoteDirs = new Set();
661
+ remoteDirs.add(path.posix.dirname(remoteBundlePath));
662
+ remoteDirs.add(path.posix.dirname(remoteArchivePath));
663
+ remoteDirs.add(path.posix.dirname(remoteGitMetaPath));
664
+ remoteDirs.add(path.posix.dirname(remoteStagedPatchPath));
665
+ remoteDirs.add(path.posix.dirname(remoteUnstagedPatchPath));
666
+ remoteDirs.add(path.posix.dirname(remoteUntrackedPath));
667
+ for (const mapping of globalGitConfigMappings) {
668
+ remoteDirs.add(path.posix.dirname(mapping.dest));
669
+ }
670
+ if (remoteDirs.size > 0) {
671
+ status.stage("Preparing remote directories");
672
+ const prepResult = await client.exec(canonical, [
673
+ "/bin/bash",
674
+ "-lc",
675
+ `mkdir -p ${[...remoteDirs].map(shellQuote).join(" ")}`,
676
+ ]);
677
+ if (prepResult.exitCode !== 0) {
678
+ throw new Error(prepResult.stderr || "Failed to prepare remote dirs");
679
+ }
680
+ }
681
+ let headState = null;
682
+ let gitCommonDir = "";
683
+ let worktreeState = null;
684
+ let copyWorktree = false;
685
+ if (!skipProvision) {
686
+ status.stage("Inspecting git state");
687
+ headState = await readHeadState(repoRoot);
688
+ const resolved = await resolveGitCommonDir(repoRoot);
689
+ gitCommonDir = resolved.commonDir;
690
+ worktreeState = await readWorktreeState(repoRoot);
691
+ const hasWorktreeChanges = worktreeState.staged.length > 0 ||
692
+ worktreeState.unstaged.length > 0 ||
693
+ worktreeState.untracked.length > 0;
694
+ if (hasWorktreeChanges && process.stdin.isTTY && !parsed.json) {
695
+ status.stop();
696
+ copyWorktree = await confirmCopyWorktree(worktreeState);
697
+ }
698
+ else if (hasWorktreeChanges) {
699
+ copyWorktree = true;
700
+ }
701
+ }
702
+ let gitMetaCreated = false;
703
+ let stagedPatchCreated = false;
704
+ let unstagedPatchCreated = false;
705
+ let untrackedCreated = false;
706
+ if (remoteHasGit) {
707
+ if (!skipProvision) {
708
+ status.stage("Packaging repo bundle");
709
+ await runCommand(repoRoot, "git", [
710
+ "bundle",
711
+ "create",
712
+ bundlePath,
713
+ "--all",
714
+ ]);
715
+ }
716
+ if (!skipProvision) {
717
+ status.stage("Packaging git metadata");
718
+ gitMetaCreated = await createGitMetaArchive(gitCommonDir, gitMetaPath, gitMetaListPath);
719
+ }
720
+ if (copyWorktree && worktreeState) {
721
+ status.stage("Packaging worktree changes");
722
+ stagedPatchCreated = await writePatch(repoRoot, ["diff", "--binary", "--cached"], stagedPatchPath);
723
+ unstagedPatchCreated = await writePatch(repoRoot, ["diff", "--binary"], unstagedPatchPath);
724
+ untrackedCreated = await createFileListArchive(repoRoot, worktreeState.untracked, untrackedPath, untrackedListPath);
725
+ }
726
+ if (!skipProvision) {
727
+ status.stage("Uploading repo bundle");
728
+ const bundleData = await fs.readFile(bundlePath);
729
+ await client.writeFile(canonical, remoteBundlePath, bundleData);
730
+ }
731
+ if (gitMetaCreated) {
732
+ status.stage("Uploading git metadata");
733
+ const gitMetaData = await fs.readFile(gitMetaPath);
734
+ await client.writeFile(canonical, remoteGitMetaPath, gitMetaData);
735
+ }
736
+ if (stagedPatchCreated) {
737
+ status.stage("Uploading staged changes");
738
+ await client.writeFile(canonical, remoteStagedPatchPath, await fs.readFile(stagedPatchPath));
739
+ }
740
+ if (unstagedPatchCreated) {
741
+ status.stage("Uploading unstaged changes");
742
+ await client.writeFile(canonical, remoteUnstagedPatchPath, await fs.readFile(unstagedPatchPath));
743
+ }
744
+ if (untrackedCreated) {
745
+ status.stage("Uploading untracked files");
746
+ await client.writeFile(canonical, remoteUntrackedPath, await fs.readFile(untrackedPath));
747
+ }
748
+ if (!skipProvision && globalGitConfigMappings.length > 0) {
749
+ status.stage("Uploading git config");
750
+ for (const mapping of globalGitConfigMappings) {
751
+ const data = await fs.readFile(mapping.source);
752
+ await client.writeFile(canonical, mapping.dest, data);
753
+ }
754
+ }
755
+ if (!skipProvision) {
756
+ const backup = `${expandedWorkdir}.bak-${Date.now()}`;
757
+ const checkoutCommand = headState?.branch
758
+ ? `git checkout -B ${shellQuote(headState.branch)} ${shellQuote(headState.commit)}`
759
+ : `git checkout --detach ${shellQuote(headState?.commit ?? "")}`;
760
+ status.stage("Provisioning workdir");
761
+ const remoteCommand = [
762
+ "set -euo pipefail",
763
+ "unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE",
764
+ `if [ -d ${shellQuote(expandedWorkdir)} ]; then`,
765
+ parsed.force
766
+ ? ` mv ${shellQuote(expandedWorkdir)} ${shellQuote(backup)}`
767
+ : ` echo "Target exists: ${expandedWorkdir}" >&2; exit 1`,
768
+ "fi",
769
+ `mkdir -p ${shellQuote(path.dirname(expandedWorkdir))}`,
770
+ `git init -b devbox-init ${shellQuote(expandedWorkdir)}`,
771
+ `cd ${shellQuote(expandedWorkdir)}`,
772
+ `git fetch ${shellQuote(remoteBundlePath)} 'refs/*:refs/*'`,
773
+ `if [ -f ${shellQuote(remoteGitMetaPath)} ]; then`,
774
+ ` tar -xzf ${shellQuote(remoteGitMetaPath)} -C .git`,
775
+ "fi",
776
+ checkoutCommand,
777
+ `if [ -f ${shellQuote(remoteStagedPatchPath)} ]; then`,
778
+ ` git apply --index ${shellQuote(remoteStagedPatchPath)}`,
779
+ "fi",
780
+ `if [ -f ${shellQuote(remoteUnstagedPatchPath)} ]; then`,
781
+ ` git apply ${shellQuote(remoteUnstagedPatchPath)}`,
782
+ "fi",
783
+ `if [ -f ${shellQuote(remoteUntrackedPath)} ]; then`,
784
+ ` tar -xzf ${shellQuote(remoteUntrackedPath)} -C .`,
785
+ "fi",
786
+ ].join("\n");
787
+ const execResult = await client.exec(canonical, [
788
+ "/bin/bash",
789
+ "--noprofile",
790
+ "--norc",
791
+ "-e",
792
+ "-u",
793
+ "-o",
794
+ "pipefail",
795
+ "-c",
796
+ remoteCommand,
797
+ ]);
798
+ if (execResult.exitCode !== 0) {
799
+ throw new Error(execResult.stderr || "Remote init failed");
800
+ }
801
+ await updateInitState({ steps: { workdirProvisioned: true } });
802
+ }
803
+ }
804
+ else if (!skipProvision) {
805
+ status.stage("Packaging repo (archive)");
806
+ if (copyWorktree) {
807
+ const workingFiles = await readNullSeparatedPaths(repoRoot, [
808
+ "ls-files",
809
+ "--cached",
810
+ "--others",
811
+ "--exclude-standard",
812
+ ]);
813
+ await createFileListArchive(repoRoot, workingFiles, archivePath, untrackedListPath);
814
+ }
815
+ else {
816
+ await runCommand(repoRoot, "git", [
817
+ "archive",
818
+ "--format=tar.gz",
819
+ "-o",
820
+ archivePath,
821
+ "HEAD",
822
+ ]);
823
+ }
824
+ status.stage("Uploading repo archive");
825
+ await client.writeFile(canonical, remoteArchivePath, await fs.readFile(archivePath));
826
+ if (globalGitConfigMappings.length > 0) {
827
+ status.stage("Uploading git config");
828
+ for (const mapping of globalGitConfigMappings) {
829
+ const data = await fs.readFile(mapping.source);
830
+ await client.writeFile(canonical, mapping.dest, data);
831
+ }
832
+ }
833
+ const backup = `${expandedWorkdir}.bak-${Date.now()}`;
834
+ status.stage("Provisioning workdir");
835
+ const remoteCommand = [
836
+ "set -euo pipefail",
837
+ "unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE",
838
+ `if [ -d ${shellQuote(expandedWorkdir)} ]; then`,
839
+ parsed.force
840
+ ? ` mv ${shellQuote(expandedWorkdir)} ${shellQuote(backup)}`
841
+ : ` echo "Target exists: ${expandedWorkdir}" >&2; exit 1`,
842
+ "fi",
843
+ `mkdir -p ${shellQuote(expandedWorkdir)}`,
844
+ `tar -xzf ${shellQuote(remoteArchivePath)} -C ${shellQuote(expandedWorkdir)}`,
845
+ ].join("\n");
846
+ const execResult = await client.exec(canonical, [
847
+ "/bin/bash",
848
+ "--noprofile",
849
+ "--norc",
850
+ "-e",
851
+ "-u",
852
+ "-o",
853
+ "pipefail",
854
+ "-c",
855
+ remoteCommand,
856
+ ]);
857
+ if (execResult.exitCode !== 0) {
858
+ throw new Error(execResult.stderr || "Remote init failed");
859
+ }
860
+ await updateInitState({ steps: { workdirProvisioned: true } });
861
+ }
862
+ if (!skipSetupUpload) {
863
+ await uploadSetupPlan({
864
+ client,
865
+ canonical,
866
+ localSetupPath: setupPath,
867
+ remoteSetupPath,
868
+ localArtifactsBundlePath: setupArtifacts?.bundlePath ?? null,
869
+ localArtifactsManifestPath: setupArtifacts?.manifestPath ?? null,
870
+ remoteArtifactsBundlePath: setupArtifacts
871
+ ? path.posix.join(expandedWorkdir, ".devbox", "setup-artifacts.tgz")
872
+ : null,
873
+ remoteArtifactsManifestPath: setupArtifacts
874
+ ? path.posix.join(expandedWorkdir, ".devbox", "setup-artifacts.json")
875
+ : null,
876
+ status,
877
+ });
878
+ await updateInitState({ steps: { setupUploaded: true } });
879
+ await updateRegistryProjectStatus(resolveInitStatus(initState?.steps, initState?.complete));
880
+ }
881
+ }
882
+ finally {
883
+ await fs.rm(tempDir, { recursive: true, force: true });
884
+ }
885
+ }
886
+ await ensureWorkdirOwnership({
887
+ client,
888
+ canonical,
889
+ workdir: expandedWorkdir,
890
+ status,
891
+ json: parsed.json === true,
892
+ });
893
+ const skipSshdConfig = shouldResume && initState?.steps.sshdConfigured;
894
+ if (!skipSshdConfig) {
895
+ status.stage("Configuring SSH mount access");
896
+ await ensureLocalMountKey();
897
+ await ensureKnownHostsFile();
898
+ const publicKey = await readLocalMountPublicKey();
899
+ await ensureRemoteMountAccess(client, canonical, publicKey);
900
+ await ensureSshdService(client, canonical);
901
+ await updateInitState({ steps: { sshdConfigured: true } });
902
+ }
903
+ const skipSshAuth = nonInteractive ||
904
+ (shouldResume && initState?.steps.sshAuthConfigured);
905
+ if (!skipSshAuth) {
906
+ status.stage("Configuring git SSH auth");
907
+ const remoteOrigin = await readRemoteOrigin(client, canonical, expandedWorkdir);
908
+ if (!remoteOrigin) {
909
+ if (!parsed.json) {
910
+ status.stop();
911
+ console.warn("Warning: unable to detect remote origin on sprite. Skipping SSH setup.");
912
+ }
913
+ }
914
+ else {
915
+ const remoteInfo = parseGitRemote(remoteOrigin);
916
+ if (!remoteInfo) {
917
+ if (!parsed.json) {
918
+ status.stop();
919
+ console.warn(`Warning: unrecognized git remote format (${remoteOrigin}). Skipping SSH setup.`);
920
+ }
921
+ }
922
+ else {
923
+ let activeOrigin = remoteOrigin;
924
+ if (remoteInfo.protocol === "https") {
925
+ if (!parsed.json) {
926
+ status.stop();
927
+ console.log(`Origin is HTTPS (${remoteOrigin}). SSH auth will not work unless we switch this remote to SSH.`);
928
+ }
929
+ const shouldSwitch = await promptYesNo("Switch origin to SSH for sprite git auth?", false);
930
+ if (!shouldSwitch) {
931
+ if (!parsed.json) {
932
+ console.warn("Skipping SSH auth setup. Configure git credentials for this repo manually before pulling or pushing.");
933
+ }
934
+ activeOrigin = "";
935
+ }
936
+ else {
937
+ status.stage("Switching git remote to SSH");
938
+ await setRemoteOrigin(client, canonical, expandedWorkdir, remoteInfo.sshUrl);
939
+ activeOrigin = remoteInfo.sshUrl;
940
+ }
941
+ }
942
+ if (activeOrigin) {
943
+ status.stage("Generating SSH key");
944
+ const publicKey = await ensureSshKey(client, canonical, `${alias}@devbox`);
945
+ status.stage("Updating SSH config");
946
+ await ensureSshConfig(client, canonical, remoteInfo.host);
947
+ if (!parsed.json) {
948
+ status.stop();
949
+ console.log("");
950
+ console.log(`Add this SSH public key to ${remoteInfo.host}:`);
951
+ console.log(publicKey);
952
+ console.log(`Open: ${remoteInfo.settingsUrl}`);
953
+ const copied = await copyToClipboard(publicKey);
954
+ if (copied) {
955
+ console.log("Copied the SSH public key to your clipboard.");
956
+ }
957
+ else {
958
+ console.log("Could not copy the SSH key automatically.");
959
+ }
960
+ const shouldOpen = await promptToContinue("Press Enter to open the SSH key page in your browser...");
961
+ if (shouldOpen) {
962
+ const opened = openBrowser(remoteInfo.settingsUrl);
963
+ if (!opened) {
964
+ console.log("Unable to open the browser automatically.");
965
+ }
966
+ }
967
+ }
968
+ const added = await promptYesNo("Have you added the SSH key?", false);
969
+ if (!added) {
970
+ if (!parsed.json) {
971
+ console.warn("Skipping SSH verification. Add the key and re-run `dvb init --resume` to verify.");
972
+ }
973
+ }
974
+ else {
975
+ status.stage("Verifying git SSH auth");
976
+ const verified = await verifySshAuth(client, canonical, remoteInfo.host, expandedWorkdir);
977
+ if (!verified) {
978
+ if (!parsed.json) {
979
+ status.stop();
980
+ console.warn("SSH auth verification failed. Confirm the key is added and that the repo access is granted.");
981
+ }
982
+ }
983
+ else {
984
+ await updateInitState({
985
+ steps: { sshAuthConfigured: true },
986
+ });
987
+ }
988
+ }
989
+ }
990
+ }
991
+ }
992
+ }
993
+ if (!shouldResume || !initState?.steps.weztermMuxInstalled) {
994
+ status.stage("Installing WezTerm mux server");
995
+ try {
996
+ await installWeztermMux({ client, spriteName: canonical });
997
+ await updateInitState({ steps: { weztermMuxInstalled: true } });
998
+ }
999
+ catch (error) {
1000
+ logger.warn("wezterm_mux_install_failed", {
1001
+ box: canonical,
1002
+ error: String(error),
1003
+ });
1004
+ if (!parsed.json) {
1005
+ status.stop();
1006
+ const message = error instanceof Error && error.message
1007
+ ? error.message
1008
+ : String(error);
1009
+ console.warn(`Warning: failed to install WezTerm mux server. ${message}`);
1010
+ }
1011
+ }
1012
+ }
1013
+ status.stage("Ensuring WezTerm mux service");
1014
+ try {
1015
+ await ensureWeztermMuxService({ client, spriteName: canonical });
1016
+ await updateInitState({ steps: { weztermMuxServiceEnsured: true } });
1017
+ }
1018
+ catch (error) {
1019
+ logger.warn("wezterm_mux_service_failed", {
1020
+ box: canonical,
1021
+ error: String(error),
1022
+ });
1023
+ if (!parsed.json) {
1024
+ status.stop();
1025
+ const message = error instanceof Error && error.message
1026
+ ? error.message
1027
+ : String(error);
1028
+ console.warn(`Warning: failed to ensure WezTerm mux service. ${message}`);
1029
+ }
1030
+ }
1031
+ if (!skipServicesConfig) {
1032
+ if (!approvedServices) {
1033
+ throw new Error("Missing services plan output.");
1034
+ }
1035
+ status.stage("Writing devbox.toml services");
1036
+ await writeRemoteServicesToml({
1037
+ client,
1038
+ canonical,
1039
+ workdir: expandedWorkdir,
1040
+ services: approvedServices.backgroundServices,
1041
+ });
1042
+ await updateInitState({ steps: { servicesConfigWritten: true } });
1043
+ }
1044
+ status.stage("Registering project");
1045
+ const projectMeta = {
1046
+ fingerprint,
1047
+ canonical,
1048
+ alias,
1049
+ workdir,
1050
+ origin: projectOrigin,
1051
+ createdAt: projectCreatedAt,
1052
+ };
1053
+ await client.writeFile(canonical, `/home/sprite/.devbox/projects/${fingerprint}.json`, Buffer.from(JSON.stringify(projectMeta, null, 2)));
1054
+ status.stage("Writing local metadata");
1055
+ await ensureDevboxToml(repoRoot, repoName, slug);
1056
+ await writeRepoMarker(repoRoot, { fingerprint, canonical, alias });
1057
+ status.stage("Updating Codex config on sprite");
1058
+ try {
1059
+ await writeRemoteCodexConfig(client, canonical, repoName);
1060
+ }
1061
+ catch (error) {
1062
+ logger.warn("codex_config_update_failed", {
1063
+ error: String(error),
1064
+ });
1065
+ if (!parsed.json) {
1066
+ status.stop();
1067
+ console.warn("Warning: failed to update /home/sprite/.codex/config.toml for full-access sandbox settings.");
1068
+ }
1069
+ }
1070
+ try {
1071
+ const gitignore = await fs.readFile(path.join(repoRoot, ".gitignore"), "utf8");
1072
+ if (!gitignore.includes(".devbox")) {
1073
+ console.log("Note: add .devbox/ to .gitignore to keep local marker private.");
1074
+ }
1075
+ }
1076
+ catch {
1077
+ // ignore missing .gitignore
1078
+ }
1079
+ if (!skipCodexApply) {
1080
+ await runRemoteCodexSetup({
1081
+ client,
1082
+ canonical,
1083
+ expandedWorkdir,
1084
+ remoteSetupPath,
1085
+ remoteArtifactsBundlePath,
1086
+ remoteArtifactsManifestPath,
1087
+ socketInfo,
1088
+ status,
1089
+ pathSetup,
1090
+ entrypoints: approvedServices?.appEntrypoints ?? [],
1091
+ });
1092
+ await updateInitState({ steps: { codexApplied: true } });
1093
+ await updateRegistryProjectStatus(resolveInitStatus(initState?.steps, initState?.complete));
1094
+ }
1095
+ await updateInitState({ complete: true });
1096
+ await updateRegistryProjectStatus("complete");
1097
+ status.stop();
1098
+ if (parsed.json) {
1099
+ console.log(JSON.stringify({ ok: true, canonical, alias, workdir, fingerprint }, null, 2));
1100
+ return;
1101
+ }
1102
+ console.log(`devbox initialized: ${alias} -> ${canonical}`);
1103
+ console.log(`workdir: ${workdir}`);
1104
+ console.log("sprites: synced to control plane");
1105
+ };
1106
+ try {
1107
+ await run();
1108
+ }
1109
+ catch (error) {
1110
+ status.fail("Init failed");
1111
+ throw error;
1112
+ }
1113
+ finally {
1114
+ status.stop();
1115
+ }
1116
+ };
1117
+ //# sourceMappingURL=index.js.map