@boxes-dev/dvb 1.0.43 → 1.0.44
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/bin/dvb.cjs +3686 -3661
- package/dist/bin/dvb.cjs.map +1 -1
- package/dist/bin/dvbd.cjs +5 -5
- package/dist/devbox/cli.d.ts.map +1 -1
- package/dist/devbox/cli.js +1 -1
- package/dist/devbox/cli.js.map +1 -1
- package/dist/devbox/commands/init/args.d.ts +0 -1
- package/dist/devbox/commands/init/args.d.ts.map +1 -1
- package/dist/devbox/commands/init/args.js +0 -4
- package/dist/devbox/commands/init/args.js.map +1 -1
- package/dist/devbox/commands/init/finalizeFlow.d.ts +56 -0
- package/dist/devbox/commands/init/finalizeFlow.d.ts.map +1 -0
- package/dist/devbox/commands/init/finalizeFlow.js +601 -0
- package/dist/devbox/commands/init/finalizeFlow.js.map +1 -0
- package/dist/devbox/commands/init/index.d.ts.map +1 -1
- package/dist/devbox/commands/init/index.js +120 -2254
- package/dist/devbox/commands/init/index.js.map +1 -1
- package/dist/devbox/commands/init/provisionFlow.d.ts +34 -0
- package/dist/devbox/commands/init/provisionFlow.d.ts.map +1 -0
- package/dist/devbox/commands/init/provisionFlow.js +319 -0
- package/dist/devbox/commands/init/provisionFlow.js.map +1 -0
- package/dist/devbox/commands/init/session.d.ts +56 -0
- package/dist/devbox/commands/init/session.d.ts.map +1 -0
- package/dist/devbox/commands/init/session.js +150 -0
- package/dist/devbox/commands/init/session.js.map +1 -0
- package/dist/devbox/commands/init/setupPlanFlow.d.ts +28 -0
- package/dist/devbox/commands/init/setupPlanFlow.d.ts.map +1 -0
- package/dist/devbox/commands/init/setupPlanFlow.js +840 -0
- package/dist/devbox/commands/init/setupPlanFlow.js.map +1 -0
- package/dist/devbox/commands/init/statusFlow.d.ts +26 -0
- package/dist/devbox/commands/init/statusFlow.d.ts.map +1 -0
- package/dist/devbox/commands/init/statusFlow.js +152 -0
- package/dist/devbox/commands/init/statusFlow.js.map +1 -0
- package/dist/devbox/completions/index.d.ts.map +1 -1
- package/dist/devbox/completions/index.js +0 -1
- package/dist/devbox/completions/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import os from "node:os";
|
|
4
|
-
import { cancel as clackCancel, confirm as clackConfirm, isCancel,
|
|
4
|
+
import { cancel as clackCancel, confirm as clackConfirm, isCancel, taskLog as clackTaskLog, } from "@clack/prompts";
|
|
5
5
|
import { resolveSocketInfo } from "@boxes-dev/core";
|
|
6
6
|
import { createSecretStore, loadConfig, resolveCodexAuthMode, createSpritesClient, normalizeGitRemoteUrl, resolveSpritesApiUrl, resolveDevboxProjectDir, SpritesApiError, slugify, } from "@boxes-dev/core";
|
|
7
7
|
import { DAEMON_TIMEOUT_MS, ensureDaemonRunning, requestJson, requireDaemonFeatures, } from "../../daemonClient.js";
|
|
@@ -9,20 +9,20 @@ import { ensureSpritesToken } from "../../auth.js";
|
|
|
9
9
|
import { fetchSpriteDaemonRelease, getConvexUrl, issueSpriteDaemonToken, } from "../../controlPlane.js";
|
|
10
10
|
import { logger } from "../../logger.js";
|
|
11
11
|
import { parseInitArgs } from "./args.js";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
12
|
+
import { runInitStatusFlow } from "./statusFlow.js";
|
|
13
|
+
import { createCodexCheckpointRecorder, createInitStateUpdater, prepareInitSessionState, } from "./session.js";
|
|
14
|
+
import { findRepoRoot, ensureRepoProjectId, readRepoOrigin } from "./repo.js";
|
|
15
|
+
import { bootstrapDevbox, ensureSpriteDaemonService, expandHome, ensureWeztermMuxService, installSpriteDaemon, shellQuote, } from "./remote.js";
|
|
15
16
|
import { ensureWeztermMuxInstalled } from "../../../wezterm/ensureMux.js";
|
|
16
|
-
import { readRepoMarker
|
|
17
|
-
import {
|
|
18
|
-
import { INIT_STEP_KEYS, readInitState, writeInitState, } from "./state.js";
|
|
19
|
-
import { ensureSshConfig, ensureSshKey, copyToClipboard, openBrowser, parseGitRemote, readRemoteOrigin, setRemoteOrigin, verifySshAuth, } from "./ssh.js";
|
|
17
|
+
import { readRepoMarker } from "./registry.js";
|
|
18
|
+
import { readInitState, writeInitState } from "./state.js";
|
|
20
19
|
import { ensureKnownHostsFile, ensureLocalMountKey, ensureRemoteMountAccess, ensureSshdService, readLocalMountPublicKey, } from "../mountSsh.js";
|
|
21
|
-
import {
|
|
22
|
-
import { mergeServicesToml, splitShellCommand, } from "../servicesToml.js";
|
|
20
|
+
import { ensureRemoteCodexInstalled } from "./codex/index.js";
|
|
23
21
|
import { checkSpriteExists, destroySpriteAndClearState, } from "../destroyShared.js";
|
|
24
22
|
import { runInitStep } from "./progress.js";
|
|
25
|
-
import {
|
|
23
|
+
import { runSetupPlanFlow } from "./setupPlanFlow.js";
|
|
24
|
+
import { runProvisionFlow } from "./provisionFlow.js";
|
|
25
|
+
import { runFinalizeFlow } from "./finalizeFlow.js";
|
|
26
26
|
const requireDaemonJsonOk = (response, label) => {
|
|
27
27
|
if (response.status >= 200 && response.status < 300) {
|
|
28
28
|
return response.body;
|
|
@@ -59,7 +59,6 @@ const ensurePrivateDir = async (dir) => {
|
|
|
59
59
|
}
|
|
60
60
|
};
|
|
61
61
|
const DEFAULT_INIT_STEP_RETRIES = 3;
|
|
62
|
-
const SETUP_ARTIFACT_REGEN_MAX_ATTEMPTS = 3;
|
|
63
62
|
const INIT_STEP_RETRYABLE_STATUSES = new Set([
|
|
64
63
|
408, 409, 425, 429, 500, 502, 503, 504,
|
|
65
64
|
]);
|
|
@@ -200,27 +199,6 @@ const migrateLegacyRepoDevboxDir = async ({ repoRoot, projectDir, }) => {
|
|
|
200
199
|
}
|
|
201
200
|
}
|
|
202
201
|
};
|
|
203
|
-
const buildServicesTomlUpdates = (services) => {
|
|
204
|
-
const updates = {};
|
|
205
|
-
for (const service of services) {
|
|
206
|
-
const name = service.name.trim();
|
|
207
|
-
if (!name) {
|
|
208
|
-
throw new Error("Service name is required in setup.json services.");
|
|
209
|
-
}
|
|
210
|
-
const parts = splitShellCommand(service.command ?? "");
|
|
211
|
-
const [cmd, ...args] = parts;
|
|
212
|
-
if (!cmd) {
|
|
213
|
-
throw new Error(`Service "${name}" is missing a command.`);
|
|
214
|
-
}
|
|
215
|
-
updates[name] = {
|
|
216
|
-
name,
|
|
217
|
-
cmd,
|
|
218
|
-
...(args.length > 0 ? { args } : {}),
|
|
219
|
-
...(service.httpPort !== null ? { httpPort: service.httpPort } : {}),
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
return updates;
|
|
223
|
-
};
|
|
224
202
|
const extractCheckpointId = (events) => {
|
|
225
203
|
const idPattern = /\bv\d+\b/;
|
|
226
204
|
const matchId = (value) => {
|
|
@@ -247,195 +225,10 @@ const extractCheckpointId = (events) => {
|
|
|
247
225
|
}
|
|
248
226
|
return null;
|
|
249
227
|
};
|
|
250
|
-
const writeRemoteServicesToml = async ({ client, canonical, workdir, services, }) => {
|
|
251
|
-
if (services.length === 0)
|
|
252
|
-
return;
|
|
253
|
-
let baseContent = "";
|
|
254
|
-
try {
|
|
255
|
-
const bytes = await client.readFile(canonical, {
|
|
256
|
-
path: "devbox.toml",
|
|
257
|
-
workingDir: workdir,
|
|
258
|
-
});
|
|
259
|
-
baseContent = Buffer.from(bytes).toString("utf8");
|
|
260
|
-
}
|
|
261
|
-
catch (error) {
|
|
262
|
-
if (!(error instanceof SpritesApiError && error.status === 404)) {
|
|
263
|
-
throw error;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
const updates = buildServicesTomlUpdates(services);
|
|
267
|
-
if (Object.keys(updates).length === 0)
|
|
268
|
-
return;
|
|
269
|
-
const merged = mergeServicesToml(baseContent, updates);
|
|
270
|
-
await client.writeFile(canonical, path.posix.join(workdir.replace(/\/$/, ""), "devbox.toml"), Buffer.from(merged));
|
|
271
|
-
};
|
|
272
|
-
const enableRemoteServices = async ({ client, canonical, services, status, }) => {
|
|
273
|
-
if (services.length === 0)
|
|
274
|
-
return;
|
|
275
|
-
logger.info("init_services_enable_start", {
|
|
276
|
-
box: canonical,
|
|
277
|
-
serviceCount: services.length,
|
|
278
|
-
});
|
|
279
|
-
for (const service of services) {
|
|
280
|
-
const name = service.name.trim();
|
|
281
|
-
if (!name) {
|
|
282
|
-
throw new Error("Service name is required in setup.json services.");
|
|
283
|
-
}
|
|
284
|
-
status?.stage(`Enabling service: ${name}`);
|
|
285
|
-
const parts = splitShellCommand(service.command ?? "");
|
|
286
|
-
const [cmd, ...args] = parts;
|
|
287
|
-
if (!cmd) {
|
|
288
|
-
throw new Error(`Service "${name}" is missing a command.`);
|
|
289
|
-
}
|
|
290
|
-
const input = {
|
|
291
|
-
cmd,
|
|
292
|
-
...(args.length > 0 ? { args } : {}),
|
|
293
|
-
...(service.httpPort !== null ? { httpPort: service.httpPort } : {}),
|
|
294
|
-
};
|
|
295
|
-
await client.createService(canonical, name, input);
|
|
296
|
-
}
|
|
297
|
-
logger.info("init_services_enable_complete", { box: canonical });
|
|
298
|
-
};
|
|
299
228
|
const throwInitCanceled = () => {
|
|
300
229
|
clackCancel("Init canceled.");
|
|
301
230
|
throw new Error("Init canceled.");
|
|
302
231
|
};
|
|
303
|
-
const promptBeforeOpenBrowser = async ({ url, title, consequence, }) => {
|
|
304
|
-
if (!process.stdin.isTTY)
|
|
305
|
-
return false;
|
|
306
|
-
showCopyableUrl(url, title);
|
|
307
|
-
const choice = await clackSelect({
|
|
308
|
-
message: `Open ${title} in your browser?`,
|
|
309
|
-
options: [
|
|
310
|
-
{ value: "open", label: "Open in browser" },
|
|
311
|
-
{ value: "skip", label: "Skip" },
|
|
312
|
-
],
|
|
313
|
-
initialValue: "open",
|
|
314
|
-
});
|
|
315
|
-
if (isCancel(choice)) {
|
|
316
|
-
throwInitCanceled();
|
|
317
|
-
}
|
|
318
|
-
if (choice === "skip") {
|
|
319
|
-
clackLog.warn(consequence);
|
|
320
|
-
return false;
|
|
321
|
-
}
|
|
322
|
-
return true;
|
|
323
|
-
};
|
|
324
|
-
const toPosixPath = (value) => value.split(path.sep).join(path.posix.sep);
|
|
325
|
-
const toRepoRelativePath = (repoRoot, filePath) => {
|
|
326
|
-
const resolved = path.isAbsolute(filePath)
|
|
327
|
-
? filePath
|
|
328
|
-
: path.resolve(repoRoot, filePath);
|
|
329
|
-
const relative = path.relative(repoRoot, resolved) || filePath;
|
|
330
|
-
return toPosixPath(relative);
|
|
331
|
-
};
|
|
332
|
-
const isOutsideRepoPath = (relativePath) => relativePath === ".." || relativePath.startsWith(`..${path.posix.sep}`);
|
|
333
|
-
const ensureWorkdirOwnership = async ({ client, canonical, workdir, status, }) => {
|
|
334
|
-
const checkResult = await client.exec(canonical, [
|
|
335
|
-
"/bin/bash",
|
|
336
|
-
"-lc",
|
|
337
|
-
[
|
|
338
|
-
"set -euo pipefail",
|
|
339
|
-
`dir=${shellQuote(workdir)}`,
|
|
340
|
-
'if [ ! -d "$dir" ]; then',
|
|
341
|
-
' echo "Missing workdir: $dir" >&2',
|
|
342
|
-
" exit 1",
|
|
343
|
-
"fi",
|
|
344
|
-
'stat -c %U "$dir"',
|
|
345
|
-
].join("\n"),
|
|
346
|
-
]);
|
|
347
|
-
if (checkResult.exitCode !== 0) {
|
|
348
|
-
const details = checkResult.stderr || checkResult.stdout || "";
|
|
349
|
-
throw new Error(details
|
|
350
|
-
? `Failed to check workdir ownership: ${details.trim()}`
|
|
351
|
-
: `Failed to check workdir ownership (exit ${checkResult.exitCode})`);
|
|
352
|
-
}
|
|
353
|
-
const owner = checkResult.stdout.trim();
|
|
354
|
-
if (!owner || owner === "sprite")
|
|
355
|
-
return;
|
|
356
|
-
status.stage("Fixing workdir ownership");
|
|
357
|
-
const chownResult = await client.exec(canonical, [
|
|
358
|
-
"/bin/bash",
|
|
359
|
-
"-lc",
|
|
360
|
-
`sudo -n chown -R sprite:sprite ${shellQuote(workdir)}`,
|
|
361
|
-
]);
|
|
362
|
-
if (chownResult.exitCode !== 0) {
|
|
363
|
-
const details = chownResult.stderr || chownResult.stdout || "";
|
|
364
|
-
throw new Error(details
|
|
365
|
-
? `Failed to update workdir ownership: ${details.trim()}`
|
|
366
|
-
: `Failed to update workdir ownership (exit ${chownResult.exitCode})`);
|
|
367
|
-
}
|
|
368
|
-
};
|
|
369
|
-
const ensureGitSafeDirectory = async ({ client, canonical, workdir, status, }) => {
|
|
370
|
-
status.stage("Configuring git safe.directory");
|
|
371
|
-
logger.info("init_git_safe_directory_configure_start", {
|
|
372
|
-
box: canonical,
|
|
373
|
-
workdir,
|
|
374
|
-
});
|
|
375
|
-
const script = [
|
|
376
|
-
"set -euo pipefail",
|
|
377
|
-
`repo=${shellQuote(workdir)}`,
|
|
378
|
-
'if [ ! -d "$repo" ]; then',
|
|
379
|
-
' echo "Missing repo workdir: $repo" >&2',
|
|
380
|
-
" exit 1",
|
|
381
|
-
"fi",
|
|
382
|
-
'if [ ! -d "$repo/.git" ]; then',
|
|
383
|
-
" exit 0",
|
|
384
|
-
"fi",
|
|
385
|
-
"if ! command -v git >/dev/null 2>&1; then",
|
|
386
|
-
" exit 0",
|
|
387
|
-
"fi",
|
|
388
|
-
"",
|
|
389
|
-
"ensure_safe_in_file() {",
|
|
390
|
-
' cfg="$1"',
|
|
391
|
-
' existing="$(git config --file "$cfg" --get-all safe.directory 2>/dev/null || true)"',
|
|
392
|
-
' if printf \'%s\\n\' "$existing" | grep -Fxq "$repo"; then',
|
|
393
|
-
" return 0",
|
|
394
|
-
" fi",
|
|
395
|
-
' git config --file "$cfg" --add safe.directory "$repo"',
|
|
396
|
-
"}",
|
|
397
|
-
"",
|
|
398
|
-
// Ensure the repo is trusted for the sprite user (most devbox flows).
|
|
399
|
-
'ensure_safe_in_file "/home/sprite/.gitconfig"',
|
|
400
|
-
// If we created/modified the file as root, best-effort fix the owner.
|
|
401
|
-
'if [ "$(id -u)" -eq 0 ]; then',
|
|
402
|
-
" chown sprite:sprite /home/sprite/.gitconfig >/dev/null 2>&1 || true",
|
|
403
|
-
"elif command -v sudo >/dev/null 2>&1; then",
|
|
404
|
-
" sudo -n chown sprite:sprite /home/sprite/.gitconfig >/dev/null 2>&1 || true",
|
|
405
|
-
"fi",
|
|
406
|
-
"",
|
|
407
|
-
// Best-effort: also trust the repo for root, in case tools run git as root.
|
|
408
|
-
'if [ "$(id -u)" -eq 0 ]; then',
|
|
409
|
-
' ensure_safe_in_file "/root/.gitconfig"',
|
|
410
|
-
"elif command -v sudo >/dev/null 2>&1; then",
|
|
411
|
-
' root_existing="$(sudo -n git config --file /root/.gitconfig --get-all safe.directory 2>/dev/null || true)"',
|
|
412
|
-
' if ! printf \'%s\\n\' "$root_existing" | grep -Fxq "$repo"; then',
|
|
413
|
-
' sudo -n git config --file /root/.gitconfig --add safe.directory "$repo" >/dev/null 2>&1 || true',
|
|
414
|
-
" fi",
|
|
415
|
-
"fi",
|
|
416
|
-
].join("\n");
|
|
417
|
-
const result = await client.exec(canonical, [
|
|
418
|
-
"/bin/bash",
|
|
419
|
-
"--noprofile",
|
|
420
|
-
"--norc",
|
|
421
|
-
"-e",
|
|
422
|
-
"-u",
|
|
423
|
-
"-o",
|
|
424
|
-
"pipefail",
|
|
425
|
-
"-c",
|
|
426
|
-
script,
|
|
427
|
-
]);
|
|
428
|
-
if (result.exitCode !== 0) {
|
|
429
|
-
const details = result.stderr || result.stdout || "";
|
|
430
|
-
throw new Error(details
|
|
431
|
-
? `Failed to configure git safe.directory: ${details.trim()}`
|
|
432
|
-
: `Failed to configure git safe.directory (exit ${result.exitCode})`);
|
|
433
|
-
}
|
|
434
|
-
logger.info("init_git_safe_directory_configure_complete", {
|
|
435
|
-
box: canonical,
|
|
436
|
-
workdir,
|
|
437
|
-
});
|
|
438
|
-
};
|
|
439
232
|
export const runInit = async (args) => {
|
|
440
233
|
const parsed = parseInitArgs(args);
|
|
441
234
|
const progressEnabled = process.stdout.isTTY && !parsed.json;
|
|
@@ -492,549 +285,62 @@ export const runInit = async (args) => {
|
|
|
492
285
|
const { repoRoot, repoName, slug, localHomeDir, projectId, projectDir, repoMarker, origin, normalizedOrigin, fingerprint, } = detected;
|
|
493
286
|
let initState = detected.initState;
|
|
494
287
|
const initFingerprintMismatch = detected.initFingerprintMismatch;
|
|
495
|
-
if (
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
const existingProject = await requestJson(socketInfo.socketPath, "GET", `/registry/project?fingerprint=${encodeURIComponent(fingerprint)}`, DAEMON_TIMEOUT_MS.registry);
|
|
510
|
-
registryProject =
|
|
511
|
-
requireDaemonJsonOk(existingProject, "Loading registry project (/registry/project)").project ?? null;
|
|
512
|
-
}
|
|
513
|
-
catch (error) {
|
|
514
|
-
daemonError = error instanceof Error ? error.message : String(error);
|
|
515
|
-
}
|
|
516
|
-
const localStatus = initState && !initFingerprintMismatch
|
|
517
|
-
? resolveInitStatus(initState.steps, initState.complete)
|
|
518
|
-
: null;
|
|
519
|
-
const recommendsResume = Boolean(initState && !initFingerprintMismatch && !initState.complete);
|
|
520
|
-
const recommendsInit = !registryProject && (!initState || initFingerprintMismatch);
|
|
521
|
-
const recommendsForce = !recommendsResume &&
|
|
522
|
-
(!initState || initFingerprintMismatch) &&
|
|
523
|
-
Boolean(registryProject?.initStatus &&
|
|
524
|
-
registryProject.initStatus !== "complete");
|
|
525
|
-
const lines = [];
|
|
526
|
-
const markerPath = path.join(projectDir, "box.json");
|
|
527
|
-
const checkpointPath = path.join(projectDir, "init-state.json");
|
|
528
|
-
lines.push("INIT STATUS");
|
|
529
|
-
lines.push("");
|
|
530
|
-
lines.push("Repo");
|
|
531
|
-
lines.push(` root: ${repoRoot}`);
|
|
532
|
-
lines.push(` origin: ${normalizedOrigin ?? origin ?? "(none)"}`);
|
|
533
|
-
lines.push(` projectId (git config devbox.projectId): ${projectId}`);
|
|
534
|
-
lines.push(` local state dir: ${projectDir}`);
|
|
535
|
-
lines.push("");
|
|
536
|
-
lines.push("Local state");
|
|
537
|
-
if (repoMarker?.canonical) {
|
|
538
|
-
lines.push(` box marker (${markerPath}): present (alias: ${repoMarker.alias ?? "(none)"}, box: ${repoMarker.canonical})`);
|
|
539
|
-
}
|
|
540
|
-
else {
|
|
541
|
-
lines.push(` box marker (${markerPath}): missing`);
|
|
542
|
-
}
|
|
543
|
-
if (initState) {
|
|
544
|
-
const completeText = initFingerprintMismatch
|
|
545
|
-
? `${String(Boolean(initState.complete))} (projectId mismatch)`
|
|
546
|
-
: String(Boolean(initState.complete));
|
|
547
|
-
lines.push(` init checkpoint (${checkpointPath}): present (updated: ${initState.updatedAt}, complete: ${completeText})`);
|
|
548
|
-
if (initState.canonical || initState.alias || initState.workdir) {
|
|
549
|
-
lines.push(` box: ${initState.canonical ?? "(unknown)"} (alias: ${initState.alias ?? "(unknown)"})`);
|
|
550
|
-
lines.push(` workdir: ${initState.workdir ?? "(unknown)"}`);
|
|
551
|
-
}
|
|
552
|
-
if (initState.checkpoints?.preCodexSetup ||
|
|
553
|
-
initState.checkpoints?.postCodexSetup) {
|
|
554
|
-
const pre = initState.checkpoints?.preCodexSetup;
|
|
555
|
-
const post = initState.checkpoints?.postCodexSetup;
|
|
556
|
-
lines.push(" snapshots:");
|
|
557
|
-
if (pre) {
|
|
558
|
-
lines.push(` pre-codex-setup: ${pre.id} (created: ${pre.createdAt})`);
|
|
559
|
-
}
|
|
560
|
-
if (post) {
|
|
561
|
-
lines.push(` post-codex-setup: ${post.id} (created: ${post.createdAt})`);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
if (localStatus) {
|
|
565
|
-
lines.push(` inferred init status: ${localStatus}`);
|
|
566
|
-
}
|
|
567
|
-
lines.push(" steps:");
|
|
568
|
-
const steps = initState.steps ?? {};
|
|
569
|
-
for (const key of INIT_STEP_KEYS) {
|
|
570
|
-
lines.push(` ${key}: ${steps[key] ? "yes" : "no"}`);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
else {
|
|
574
|
-
lines.push(` init checkpoint (${checkpointPath}): missing`);
|
|
575
|
-
}
|
|
576
|
-
lines.push("");
|
|
577
|
-
lines.push("Registry");
|
|
578
|
-
lines.push(` daemon socket: ${socketInfo.socketPath}`);
|
|
579
|
-
if (daemonError) {
|
|
580
|
-
lines.push(` daemon: unavailable (${daemonError})`);
|
|
581
|
-
}
|
|
582
|
-
else if (registryProject) {
|
|
583
|
-
lines.push(" project: present");
|
|
584
|
-
lines.push(` box: ${registryProject.canonical}`);
|
|
585
|
-
lines.push(` alias: ${registryProject.alias ?? "(none)"}`);
|
|
586
|
-
lines.push(` initStatus: ${registryProject.initStatus ?? "(none)"}`);
|
|
587
|
-
lines.push(` initUpdatedAt: ${registryProject.initUpdatedAt ?? "(none)"}`);
|
|
588
|
-
if (registryProject.localPaths &&
|
|
589
|
-
registryProject.localPaths.length > 0) {
|
|
590
|
-
lines.push(` localPaths: ${registryProject.localPaths.length} path(s)`);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
else {
|
|
594
|
-
lines.push(" project: not found");
|
|
595
|
-
}
|
|
596
|
-
lines.push("");
|
|
597
|
-
lines.push("Recommended");
|
|
598
|
-
if (recommendsResume) {
|
|
599
|
-
lines.push(" dvb init --resume");
|
|
600
|
-
}
|
|
601
|
-
else if (recommendsInit) {
|
|
602
|
-
lines.push(" dvb init");
|
|
603
|
-
}
|
|
604
|
-
else if (recommendsForce) {
|
|
605
|
-
lines.push(" Resume is not available from this clone (no init checkpoint).");
|
|
606
|
-
lines.push(" If init was started elsewhere, re-run `dvb init --resume` from that repo.");
|
|
607
|
-
lines.push(" Otherwise, restart init with `dvb init --force` (destroys and recreates the existing devbox).");
|
|
608
|
-
}
|
|
609
|
-
else {
|
|
610
|
-
lines.push(" (none)");
|
|
611
|
-
}
|
|
612
|
-
lines.push("");
|
|
613
|
-
if (parsed.json) {
|
|
614
|
-
console.log(JSON.stringify({
|
|
615
|
-
ok: true,
|
|
616
|
-
repo: {
|
|
617
|
-
root: repoRoot,
|
|
618
|
-
origin: normalizedOrigin ?? origin ?? null,
|
|
619
|
-
fingerprint,
|
|
620
|
-
},
|
|
621
|
-
local: {
|
|
622
|
-
marker: repoMarker ?? null,
|
|
623
|
-
initState: initState ?? null,
|
|
624
|
-
initFingerprintMismatch,
|
|
625
|
-
inferredStatus: localStatus,
|
|
626
|
-
},
|
|
627
|
-
registry: {
|
|
628
|
-
socketPath: socketInfo.socketPath,
|
|
629
|
-
error: daemonError,
|
|
630
|
-
project: registryProject,
|
|
631
|
-
},
|
|
632
|
-
recommended: recommendsResume
|
|
633
|
-
? "dvb init --resume"
|
|
634
|
-
: recommendsInit
|
|
635
|
-
? "dvb init"
|
|
636
|
-
: recommendsForce
|
|
637
|
-
? "dvb init --force"
|
|
638
|
-
: null,
|
|
639
|
-
}, null, 2));
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
642
|
-
console.log(lines.join("\n"));
|
|
288
|
+
if (await runInitStatusFlow({
|
|
289
|
+
parsed,
|
|
290
|
+
repoRoot,
|
|
291
|
+
origin,
|
|
292
|
+
normalizedOrigin,
|
|
293
|
+
projectId,
|
|
294
|
+
projectDir,
|
|
295
|
+
fingerprint,
|
|
296
|
+
repoMarker,
|
|
297
|
+
initState,
|
|
298
|
+
initFingerprintMismatch,
|
|
299
|
+
requireDaemonJsonOk,
|
|
300
|
+
resolveInitStatus,
|
|
301
|
+
})) {
|
|
643
302
|
return;
|
|
644
303
|
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
const previousState = initState;
|
|
671
|
-
const freshState = {
|
|
672
|
-
version: 1,
|
|
673
|
-
fingerprint,
|
|
674
|
-
...(previousState?.canonical !== undefined
|
|
675
|
-
? { canonical: previousState.canonical }
|
|
676
|
-
: {}),
|
|
677
|
-
...(previousState?.alias !== undefined
|
|
678
|
-
? { alias: previousState.alias }
|
|
679
|
-
: {}),
|
|
680
|
-
...(previousState?.workdir !== undefined
|
|
681
|
-
? { workdir: previousState.workdir }
|
|
682
|
-
: {}),
|
|
683
|
-
steps: {},
|
|
684
|
-
updatedAt: new Date().toISOString(),
|
|
685
|
-
complete: false,
|
|
686
|
-
};
|
|
687
|
-
initState = freshState;
|
|
688
|
-
await writeInitState(projectDir, freshState);
|
|
689
|
-
}
|
|
690
|
-
const shouldResume = Boolean(wantsResume && initState && !initState.complete);
|
|
691
|
-
const ensureInitState = () => {
|
|
692
|
-
if (!initState || initState.fingerprint !== fingerprint) {
|
|
693
|
-
initState = {
|
|
694
|
-
version: 1,
|
|
695
|
-
fingerprint,
|
|
696
|
-
steps: {},
|
|
697
|
-
updatedAt: new Date().toISOString(),
|
|
698
|
-
};
|
|
699
|
-
}
|
|
700
|
-
return initState;
|
|
701
|
-
};
|
|
702
|
-
const updateInitState = async (update) => {
|
|
703
|
-
const base = ensureInitState();
|
|
704
|
-
initState = {
|
|
705
|
-
...base,
|
|
706
|
-
...update,
|
|
707
|
-
steps: {
|
|
708
|
-
...base.steps,
|
|
709
|
-
...(update.steps ?? {}),
|
|
710
|
-
},
|
|
711
|
-
updatedAt: new Date().toISOString(),
|
|
712
|
-
};
|
|
713
|
-
await writeInitState(projectDir, initState);
|
|
714
|
-
};
|
|
715
|
-
const recordCodexCheckpoint = async ({ client, canonical, phase, }) => {
|
|
716
|
-
const createdAt = new Date().toISOString();
|
|
717
|
-
const label = phase === "preCodexSetup" ? "pre-codex-setup" : "post-codex-setup";
|
|
718
|
-
const comment = `dvb init: ${label} repo=${repoName} fingerprint=${fingerprint} at=${createdAt}`;
|
|
719
|
-
const events = await client.createCheckpoint(canonical, { comment });
|
|
720
|
-
const id = extractCheckpointId(events);
|
|
721
|
-
if (!id) {
|
|
304
|
+
const preparedSession = await prepareInitSessionState({
|
|
305
|
+
parsed,
|
|
306
|
+
initState,
|
|
307
|
+
initFingerprintMismatch,
|
|
308
|
+
projectDir,
|
|
309
|
+
fingerprint,
|
|
310
|
+
});
|
|
311
|
+
initState = preparedSession.initState;
|
|
312
|
+
const { shouldResume, nonInteractive, skipCodexApply, skipCodexCliEnsure } = preparedSession;
|
|
313
|
+
const getInitState = () => initState;
|
|
314
|
+
const { updateInitState } = createInitStateUpdater({
|
|
315
|
+
fingerprint,
|
|
316
|
+
projectDir,
|
|
317
|
+
getInitState,
|
|
318
|
+
setInitState: (next) => {
|
|
319
|
+
initState = next;
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
const recordCodexCheckpoint = createCodexCheckpointRecorder({
|
|
323
|
+
repoName,
|
|
324
|
+
fingerprint,
|
|
325
|
+
getInitState,
|
|
326
|
+
updateInitState,
|
|
327
|
+
extractCheckpointId,
|
|
328
|
+
onCheckpointIdMissing: ({ canonical, phaseLabel }) => {
|
|
722
329
|
logger.warn("init_checkpoint_id_missing", {
|
|
723
330
|
box: canonical,
|
|
724
331
|
fingerprint,
|
|
725
|
-
phase:
|
|
726
|
-
});
|
|
727
|
-
throw new Error("Checkpoint ID missing.");
|
|
728
|
-
}
|
|
729
|
-
const record = { id, comment, createdAt };
|
|
730
|
-
if (initState) {
|
|
731
|
-
await updateInitState({
|
|
732
|
-
checkpoints: { ...(initState.checkpoints ?? {}), [phase]: record },
|
|
332
|
+
phase: phaseLabel,
|
|
733
333
|
});
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
try {
|
|
737
|
-
let meta = {};
|
|
738
|
-
try {
|
|
739
|
-
const raw = await client.readFile(canonical, { path: metaPath });
|
|
740
|
-
const parsed = JSON.parse(Buffer.from(raw).toString("utf8"));
|
|
741
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
742
|
-
meta = parsed;
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
catch (error) {
|
|
746
|
-
if (!(error instanceof SpritesApiError && error.status === 404)) {
|
|
747
|
-
throw error;
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
const existingCheckpoints = meta.checkpoints &&
|
|
751
|
-
typeof meta.checkpoints === "object" &&
|
|
752
|
-
!Array.isArray(meta.checkpoints)
|
|
753
|
-
? meta.checkpoints
|
|
754
|
-
: {};
|
|
755
|
-
const updated = {
|
|
756
|
-
...meta,
|
|
757
|
-
checkpoints: {
|
|
758
|
-
...existingCheckpoints,
|
|
759
|
-
[phase]: record,
|
|
760
|
-
},
|
|
761
|
-
};
|
|
762
|
-
await client.writeFile(canonical, metaPath, Buffer.from(JSON.stringify(updated, null, 2)));
|
|
763
|
-
}
|
|
764
|
-
catch (error) {
|
|
334
|
+
},
|
|
335
|
+
onCheckpointMetaUpdateFailed: ({ canonical, phaseLabel, error }) => {
|
|
765
336
|
logger.warn("init_checkpoint_meta_update_failed", {
|
|
766
337
|
box: canonical,
|
|
767
338
|
fingerprint,
|
|
768
|
-
phase:
|
|
769
|
-
error
|
|
770
|
-
});
|
|
771
|
-
}
|
|
772
|
-
return { id, comment, createdAt };
|
|
773
|
-
};
|
|
774
|
-
if (parsed.codexSetupOnly) {
|
|
775
|
-
if (!repoMarker?.canonical) {
|
|
776
|
-
throw new Error("Repo is not initialized. Run `dvb init` first.");
|
|
777
|
-
}
|
|
778
|
-
const setupDir = projectDir;
|
|
779
|
-
const setupPath = path.join(setupDir, "setup.json");
|
|
780
|
-
let setupPlan;
|
|
781
|
-
try {
|
|
782
|
-
setupPlan = await readSetupPlan(setupPath);
|
|
783
|
-
}
|
|
784
|
-
catch {
|
|
785
|
-
throw new Error(`Missing or invalid setup plan (${setupPath}). Run \`dvb init\` first.`);
|
|
786
|
-
}
|
|
787
|
-
const localArtifactsBundlePath = path.join(setupDir, "setup-artifacts.tgz");
|
|
788
|
-
const localArtifactsManifestPath = path.join(setupDir, "setup-artifacts.json");
|
|
789
|
-
const localArtifactsPartsDescriptorPath = path.join(setupDir, "setup-artifacts.parts.json");
|
|
790
|
-
let artifactsBundlePath = null;
|
|
791
|
-
let artifactsManifestPath = null;
|
|
792
|
-
let artifactsPartsDescriptorPath = null;
|
|
793
|
-
try {
|
|
794
|
-
await fs.access(localArtifactsBundlePath);
|
|
795
|
-
await fs.access(localArtifactsManifestPath);
|
|
796
|
-
artifactsBundlePath = localArtifactsBundlePath;
|
|
797
|
-
artifactsManifestPath = localArtifactsManifestPath;
|
|
798
|
-
try {
|
|
799
|
-
await fs.access(localArtifactsPartsDescriptorPath);
|
|
800
|
-
artifactsPartsDescriptorPath = localArtifactsPartsDescriptorPath;
|
|
801
|
-
}
|
|
802
|
-
catch {
|
|
803
|
-
// split artifacts descriptor is optional
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
catch {
|
|
807
|
-
// artifacts are optional in setup-only flow
|
|
808
|
-
}
|
|
809
|
-
const socketInfo = resolveSocketInfo();
|
|
810
|
-
await runInitStep({
|
|
811
|
-
enabled: progressEnabled,
|
|
812
|
-
title: "Starting dvbd",
|
|
813
|
-
fn: async () => {
|
|
814
|
-
await ensureDaemonRunning(socketInfo.socketPath);
|
|
815
|
-
await requireDaemonFeatures(socketInfo.socketPath, ["ports"]);
|
|
816
|
-
},
|
|
817
|
-
});
|
|
818
|
-
const { config, client, controlPlaneToken } = await runInitStep({
|
|
819
|
-
enabled: progressEnabled,
|
|
820
|
-
title: "Loading devbox config",
|
|
821
|
-
fn: async () => {
|
|
822
|
-
const config = await loadConfig(process.env.HOME ? { homeDir: process.env.HOME } : undefined);
|
|
823
|
-
const store = await createSecretStore(config?.tokenStore, process.env.HOME ? { homeDir: process.env.HOME } : undefined);
|
|
824
|
-
const apiBaseUrl = resolveSpritesApiUrl(config);
|
|
825
|
-
const { token, controlPlaneToken } = await ensureSpritesToken(store, undefined, {
|
|
826
|
-
apiBaseUrl,
|
|
827
|
-
});
|
|
828
|
-
const client = createSpritesClient({
|
|
829
|
-
apiBaseUrl,
|
|
830
|
-
token,
|
|
831
|
-
});
|
|
832
|
-
return { config, client, controlPlaneToken };
|
|
833
|
-
},
|
|
834
|
-
});
|
|
835
|
-
const setupOnlyCodexAuthMode = resolveCodexAuthMode(config);
|
|
836
|
-
const setupOnlyCodexProxyOptions = setupOnlyCodexAuthMode === "proxy"
|
|
837
|
-
? {
|
|
838
|
-
gatewayBaseUrl: resolveSpritesApiUrl(config),
|
|
839
|
-
controlPlaneToken: (controlPlaneToken ?? "").trim(),
|
|
840
|
-
}
|
|
841
|
-
: undefined;
|
|
842
|
-
if (setupOnlyCodexAuthMode === "proxy" &&
|
|
843
|
-
!setupOnlyCodexProxyOptions?.controlPlaneToken) {
|
|
844
|
-
throw new Error("Control plane session required for init Codex proxy mode.");
|
|
845
|
-
}
|
|
846
|
-
const canonical = repoMarker.canonical;
|
|
847
|
-
let expandedWorkdir = expandHome(`~/${slug}`);
|
|
848
|
-
try {
|
|
849
|
-
const metaRaw = await client.readFile(canonical, {
|
|
850
|
-
path: `/home/sprite/.devbox/projects/${fingerprint}.json`,
|
|
339
|
+
phase: phaseLabel,
|
|
340
|
+
error,
|
|
851
341
|
});
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
expandedWorkdir = expandHome(meta.workdir);
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
catch {
|
|
858
|
-
// ignore missing project metadata
|
|
859
|
-
}
|
|
860
|
-
const remoteSetupPath = path.posix.join(expandedWorkdir, ".devbox", "setup.json");
|
|
861
|
-
const remoteArtifactsBundlePath = path.posix.join(expandedWorkdir, ".devbox", "setup-artifacts.tgz");
|
|
862
|
-
const remoteArtifactsManifestPath = path.posix.join(expandedWorkdir, ".devbox", "setup-artifacts.json");
|
|
863
|
-
const remoteArtifactsPartsDescriptorPath = path.posix.join(expandedWorkdir, ".devbox", "setup-artifacts.parts.json");
|
|
864
|
-
const pathSetup = 'export PATH="$(npm bin -g 2>/dev/null):$PATH"';
|
|
865
|
-
await runInitStep({
|
|
866
|
-
enabled: progressEnabled,
|
|
867
|
-
title: "Configuring git safe.directory",
|
|
868
|
-
fn: async ({ status }) => {
|
|
869
|
-
await ensureGitSafeDirectory({
|
|
870
|
-
client,
|
|
871
|
-
canonical,
|
|
872
|
-
workdir: expandedWorkdir,
|
|
873
|
-
status,
|
|
874
|
-
});
|
|
875
|
-
},
|
|
876
|
-
});
|
|
877
|
-
await runInitStep({
|
|
878
|
-
enabled: progressEnabled,
|
|
879
|
-
title: "Uploading setup plan",
|
|
880
|
-
fn: async ({ status }) => {
|
|
881
|
-
await uploadSetupPlan({
|
|
882
|
-
client,
|
|
883
|
-
canonical,
|
|
884
|
-
localSetupPath: setupPath,
|
|
885
|
-
remoteSetupPath,
|
|
886
|
-
localArtifactsBundlePath: artifactsBundlePath,
|
|
887
|
-
localArtifactsManifestPath: artifactsManifestPath,
|
|
888
|
-
localArtifactsPartsDescriptorPath: artifactsPartsDescriptorPath,
|
|
889
|
-
remoteArtifactsBundlePath: artifactsBundlePath
|
|
890
|
-
? remoteArtifactsBundlePath
|
|
891
|
-
: null,
|
|
892
|
-
remoteArtifactsManifestPath: artifactsBundlePath
|
|
893
|
-
? remoteArtifactsManifestPath
|
|
894
|
-
: null,
|
|
895
|
-
remoteArtifactsPartsDescriptorPath: artifactsBundlePath
|
|
896
|
-
? remoteArtifactsPartsDescriptorPath
|
|
897
|
-
: null,
|
|
898
|
-
status,
|
|
899
|
-
});
|
|
900
|
-
},
|
|
901
|
-
});
|
|
902
|
-
await runInitStep({
|
|
903
|
-
enabled: progressEnabled,
|
|
904
|
-
title: "Staging setup artifacts",
|
|
905
|
-
fn: async ({ status }) => {
|
|
906
|
-
status.stage("Copying repo artifacts and staging external files");
|
|
907
|
-
await stageRemoteSetupArtifacts({
|
|
908
|
-
client,
|
|
909
|
-
canonical,
|
|
910
|
-
workdir: expandedWorkdir,
|
|
911
|
-
artifactsBundlePath: remoteArtifactsBundlePath,
|
|
912
|
-
artifactsManifestPath: remoteArtifactsManifestPath,
|
|
913
|
-
});
|
|
914
|
-
},
|
|
915
|
-
});
|
|
916
|
-
await runInitStep({
|
|
917
|
-
enabled: progressEnabled,
|
|
918
|
-
title: "Snapshotting filesystem (pre-setup)",
|
|
919
|
-
fn: async ({ status, fail, ok }) => {
|
|
920
|
-
try {
|
|
921
|
-
const checkpoint = await retryInitStep({
|
|
922
|
-
status,
|
|
923
|
-
title: "Snapshotting filesystem (pre-setup)",
|
|
924
|
-
fn: async () => await recordCodexCheckpoint({
|
|
925
|
-
client,
|
|
926
|
-
canonical,
|
|
927
|
-
phase: "preCodexSetup",
|
|
928
|
-
}),
|
|
929
|
-
});
|
|
930
|
-
if (checkpoint.id) {
|
|
931
|
-
ok(`Snapshot created: ${checkpoint.id}`);
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
catch (error) {
|
|
935
|
-
logger.warn("init_checkpoint_create_failed", {
|
|
936
|
-
box: canonical,
|
|
937
|
-
fingerprint,
|
|
938
|
-
phase: "pre-codex-setup",
|
|
939
|
-
error: error instanceof Error ? error.message : String(error),
|
|
940
|
-
});
|
|
941
|
-
fail("Snapshotting filesystem (pre-setup) (failed)");
|
|
942
|
-
throw error;
|
|
943
|
-
}
|
|
944
|
-
},
|
|
945
|
-
});
|
|
946
|
-
await runInitStep({
|
|
947
|
-
enabled: progressEnabled,
|
|
948
|
-
title: "Enabling devbox services",
|
|
949
|
-
fn: async ({ status }) => {
|
|
950
|
-
await enableRemoteServices({
|
|
951
|
-
client,
|
|
952
|
-
canonical,
|
|
953
|
-
services: setupPlan.services.backgroundServices,
|
|
954
|
-
status,
|
|
955
|
-
});
|
|
956
|
-
},
|
|
957
|
-
});
|
|
958
|
-
await runInitStep({
|
|
959
|
-
enabled: progressEnabled,
|
|
960
|
-
title: "Ensuring Codex CLI",
|
|
961
|
-
fn: async ({ status, fail }) => {
|
|
962
|
-
try {
|
|
963
|
-
await retryInitStep({
|
|
964
|
-
status,
|
|
965
|
-
title: "Ensuring Codex CLI",
|
|
966
|
-
fn: async () => await ensureRemoteCodexInstalled(client, canonical),
|
|
967
|
-
});
|
|
968
|
-
}
|
|
969
|
-
catch (error) {
|
|
970
|
-
logger.warn("codex_cli_ensure_failed", {
|
|
971
|
-
box: canonical,
|
|
972
|
-
error: error instanceof Error ? error.message : String(error),
|
|
973
|
-
});
|
|
974
|
-
fail("Ensuring Codex CLI (failed)");
|
|
975
|
-
throw error;
|
|
976
|
-
}
|
|
977
|
-
},
|
|
978
|
-
});
|
|
979
|
-
await runInitStep({
|
|
980
|
-
enabled: progressEnabled,
|
|
981
|
-
title: "Applying setup plan",
|
|
982
|
-
fn: async ({ status }) => {
|
|
983
|
-
await runRemoteCodexSetup({
|
|
984
|
-
client,
|
|
985
|
-
canonical,
|
|
986
|
-
expandedWorkdir,
|
|
987
|
-
remoteSetupPath,
|
|
988
|
-
remoteArtifactsBundlePath,
|
|
989
|
-
remoteArtifactsManifestPath,
|
|
990
|
-
socketInfo,
|
|
991
|
-
status,
|
|
992
|
-
pathSetup,
|
|
993
|
-
entrypoints: setupPlan.services.appEntrypoints,
|
|
994
|
-
emitCodexOutput: !parsed.json,
|
|
995
|
-
...(setupOnlyCodexProxyOptions
|
|
996
|
-
? { proxyOptions: setupOnlyCodexProxyOptions }
|
|
997
|
-
: {}),
|
|
998
|
-
});
|
|
999
|
-
},
|
|
1000
|
-
});
|
|
1001
|
-
await runInitStep({
|
|
1002
|
-
enabled: progressEnabled,
|
|
1003
|
-
title: "Snapshotting filesystem (post-setup)",
|
|
1004
|
-
fn: async ({ status, fail, ok }) => {
|
|
1005
|
-
try {
|
|
1006
|
-
const checkpoint = await retryInitStep({
|
|
1007
|
-
status,
|
|
1008
|
-
title: "Snapshotting filesystem (post-setup)",
|
|
1009
|
-
fn: async () => await recordCodexCheckpoint({
|
|
1010
|
-
client,
|
|
1011
|
-
canonical,
|
|
1012
|
-
phase: "postCodexSetup",
|
|
1013
|
-
}),
|
|
1014
|
-
});
|
|
1015
|
-
if (checkpoint.id) {
|
|
1016
|
-
ok(`Snapshot created: ${checkpoint.id}`);
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
catch (error) {
|
|
1020
|
-
logger.warn("init_checkpoint_create_failed", {
|
|
1021
|
-
box: canonical,
|
|
1022
|
-
fingerprint,
|
|
1023
|
-
phase: "post-codex-setup",
|
|
1024
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1025
|
-
});
|
|
1026
|
-
fail("Snapshotting filesystem (post-setup) (failed)");
|
|
1027
|
-
throw error;
|
|
1028
|
-
}
|
|
1029
|
-
},
|
|
1030
|
-
});
|
|
1031
|
-
if (parsed.json) {
|
|
1032
|
-
console.log(JSON.stringify({ ok: true }, null, 2));
|
|
1033
|
-
return;
|
|
1034
|
-
}
|
|
1035
|
-
console.log("Codex setup complete.");
|
|
1036
|
-
return;
|
|
1037
|
-
}
|
|
342
|
+
},
|
|
343
|
+
});
|
|
1038
344
|
const socketInfo = resolveSocketInfo();
|
|
1039
345
|
await runInitStep({
|
|
1040
346
|
enabled: progressEnabled,
|
|
@@ -1425,1159 +731,6 @@ export const runInit = async (args) => {
|
|
|
1425
731
|
}
|
|
1426
732
|
},
|
|
1427
733
|
});
|
|
1428
|
-
const setupDir = projectDir;
|
|
1429
|
-
const setupPath = path.join(setupDir, "setup.json");
|
|
1430
|
-
const scansDir = path.join(setupDir, "scans");
|
|
1431
|
-
const logDir = path.join(setupDir, "logs");
|
|
1432
|
-
const setupEnvSecretsScanPath = path.join(scansDir, "setup-env-secrets.json");
|
|
1433
|
-
const setupExternalScanPath = path.join(scansDir, "setup-external.json");
|
|
1434
|
-
const setupExtraArtifactsScanPath = path.join(scansDir, "setup-extra-artifacts.json");
|
|
1435
|
-
const servicesScanPath = path.join(scansDir, "services.json");
|
|
1436
|
-
const scanThreadsPath = path.join(scansDir, "codex-scan-threads.json");
|
|
1437
|
-
let setupArtifacts = null;
|
|
1438
|
-
const nonInteractive = !process.stdin.isTTY || parsed.json;
|
|
1439
|
-
const skipSetupPlan = shouldResume && initState?.steps.setupPlanWritten;
|
|
1440
|
-
const skipServicesConfig = shouldResume && initState?.steps.servicesConfigWritten;
|
|
1441
|
-
const skipServicesEnable = shouldResume && initState?.steps.servicesEnabled;
|
|
1442
|
-
const skipSetupUpload = nonInteractive || (shouldResume && initState?.steps.setupUploaded);
|
|
1443
|
-
const skipCodexApply = nonInteractive || (shouldResume && initState?.steps.codexApplied);
|
|
1444
|
-
const skipCodexCliEnsure = skipCodexApply || (shouldResume && initState?.steps.codexCliEnsured);
|
|
1445
|
-
let approvedPlan = null;
|
|
1446
|
-
const setupTempDir = await fs.mkdtemp(path.join(os.tmpdir(), "devbox-setup-"));
|
|
1447
|
-
try {
|
|
1448
|
-
await fs.mkdir(setupDir, { recursive: true, mode: 0o700 });
|
|
1449
|
-
try {
|
|
1450
|
-
await fs.chmod(setupDir, 0o700);
|
|
1451
|
-
}
|
|
1452
|
-
catch {
|
|
1453
|
-
// best effort on filesystems that do not support chmod
|
|
1454
|
-
}
|
|
1455
|
-
const tryReadSetupPlan = async () => {
|
|
1456
|
-
try {
|
|
1457
|
-
return await readSetupPlan(setupPath);
|
|
1458
|
-
}
|
|
1459
|
-
catch {
|
|
1460
|
-
return null;
|
|
1461
|
-
}
|
|
1462
|
-
};
|
|
1463
|
-
const tryReadServicesPlan = async () => {
|
|
1464
|
-
try {
|
|
1465
|
-
return await readServicesPlan(servicesScanPath);
|
|
1466
|
-
}
|
|
1467
|
-
catch {
|
|
1468
|
-
return null;
|
|
1469
|
-
}
|
|
1470
|
-
};
|
|
1471
|
-
const tryReadEnvSecretsScan = async () => {
|
|
1472
|
-
try {
|
|
1473
|
-
return await readSetupEnvSecretsPlan(setupEnvSecretsScanPath);
|
|
1474
|
-
}
|
|
1475
|
-
catch {
|
|
1476
|
-
return null;
|
|
1477
|
-
}
|
|
1478
|
-
};
|
|
1479
|
-
const tryReadExternalScan = async () => {
|
|
1480
|
-
try {
|
|
1481
|
-
return await readSetupExternalPlan(setupExternalScanPath);
|
|
1482
|
-
}
|
|
1483
|
-
catch {
|
|
1484
|
-
return null;
|
|
1485
|
-
}
|
|
1486
|
-
};
|
|
1487
|
-
const tryReadExtraArtifactsScan = async () => {
|
|
1488
|
-
try {
|
|
1489
|
-
return await readSetupExtraArtifactsPlan(setupExtraArtifactsScanPath);
|
|
1490
|
-
}
|
|
1491
|
-
catch {
|
|
1492
|
-
return null;
|
|
1493
|
-
}
|
|
1494
|
-
};
|
|
1495
|
-
const tryReadScanThreads = async () => {
|
|
1496
|
-
try {
|
|
1497
|
-
const raw = await fs.readFile(scanThreadsPath, "utf8");
|
|
1498
|
-
const parsed = JSON.parse(raw);
|
|
1499
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1500
|
-
return {};
|
|
1501
|
-
}
|
|
1502
|
-
const record = parsed;
|
|
1503
|
-
return {
|
|
1504
|
-
...(typeof record.envSecretsThreadId === "string"
|
|
1505
|
-
? { envSecretsThreadId: record.envSecretsThreadId }
|
|
1506
|
-
: {}),
|
|
1507
|
-
...(typeof record.externalThreadId === "string"
|
|
1508
|
-
? { externalThreadId: record.externalThreadId }
|
|
1509
|
-
: {}),
|
|
1510
|
-
...(typeof record.extraArtifactsThreadId === "string"
|
|
1511
|
-
? { extraArtifactsThreadId: record.extraArtifactsThreadId }
|
|
1512
|
-
: {}),
|
|
1513
|
-
...(typeof record.servicesThreadId === "string"
|
|
1514
|
-
? { servicesThreadId: record.servicesThreadId }
|
|
1515
|
-
: {}),
|
|
1516
|
-
};
|
|
1517
|
-
}
|
|
1518
|
-
catch {
|
|
1519
|
-
return {};
|
|
1520
|
-
}
|
|
1521
|
-
};
|
|
1522
|
-
let scanThreads = shouldResume
|
|
1523
|
-
? await tryReadScanThreads()
|
|
1524
|
-
: {};
|
|
1525
|
-
let scanThreadsDirty = false;
|
|
1526
|
-
const persistScanThreads = async () => {
|
|
1527
|
-
await fs.mkdir(scansDir, { recursive: true });
|
|
1528
|
-
await fs.writeFile(scanThreadsPath, JSON.stringify(scanThreads, null, 2), "utf8");
|
|
1529
|
-
};
|
|
1530
|
-
const flushScanThreads = async () => {
|
|
1531
|
-
if (!scanThreadsDirty)
|
|
1532
|
-
return;
|
|
1533
|
-
await persistScanThreads();
|
|
1534
|
-
scanThreadsDirty = false;
|
|
1535
|
-
};
|
|
1536
|
-
const saveScanThreadId = (key, threadId) => {
|
|
1537
|
-
if (!threadId)
|
|
1538
|
-
return;
|
|
1539
|
-
if (scanThreads[key] === threadId)
|
|
1540
|
-
return;
|
|
1541
|
-
scanThreads = { ...scanThreads, [key]: threadId };
|
|
1542
|
-
scanThreadsDirty = true;
|
|
1543
|
-
};
|
|
1544
|
-
const shouldRetryCodexScan = (scanFullyCompleted, ...arrays) => !scanFullyCompleted && arrays.every((array) => array.length === 0);
|
|
1545
|
-
const runCodexScanWithImmediateRetry = async ({ run, read, outputPath, update, shouldRetry, }) => {
|
|
1546
|
-
try {
|
|
1547
|
-
await run();
|
|
1548
|
-
let out = await read();
|
|
1549
|
-
if (shouldRetry(out)) {
|
|
1550
|
-
update("retrying");
|
|
1551
|
-
await fs.rm(outputPath, { force: true });
|
|
1552
|
-
await run();
|
|
1553
|
-
out = await read();
|
|
1554
|
-
}
|
|
1555
|
-
update("done");
|
|
1556
|
-
return out;
|
|
1557
|
-
}
|
|
1558
|
-
catch (error) {
|
|
1559
|
-
update("failed");
|
|
1560
|
-
throw error;
|
|
1561
|
-
}
|
|
1562
|
-
};
|
|
1563
|
-
let setupPlan = skipSetupPlan || !shouldResume ? null : await tryReadSetupPlan();
|
|
1564
|
-
const needsSetupScan = !skipSetupPlan && !setupPlan;
|
|
1565
|
-
let servicesPlan = !needsSetupScan || !shouldResume ? null : await tryReadServicesPlan();
|
|
1566
|
-
let envSecretsScan = !needsSetupScan || !shouldResume ? null : await tryReadEnvSecretsScan();
|
|
1567
|
-
let externalScan = !needsSetupScan || !shouldResume ? null : await tryReadExternalScan();
|
|
1568
|
-
let extraArtifactsScan = !needsSetupScan || !shouldResume
|
|
1569
|
-
? null
|
|
1570
|
-
: await tryReadExtraArtifactsScan();
|
|
1571
|
-
if (servicesPlan &&
|
|
1572
|
-
shouldRetryCodexScan(servicesPlan.scanFullyCompleted, servicesPlan.appEntrypoints, servicesPlan.backgroundServices)) {
|
|
1573
|
-
servicesPlan = null;
|
|
1574
|
-
}
|
|
1575
|
-
if (envSecretsScan &&
|
|
1576
|
-
shouldRetryCodexScan(envSecretsScan.scanFullyCompleted, envSecretsScan.envFiles, envSecretsScan.secretFiles)) {
|
|
1577
|
-
envSecretsScan = null;
|
|
1578
|
-
}
|
|
1579
|
-
if (externalScan &&
|
|
1580
|
-
shouldRetryCodexScan(externalScan.scanFullyCompleted, externalScan.externalDependencies, externalScan.externalConfigs)) {
|
|
1581
|
-
externalScan = null;
|
|
1582
|
-
}
|
|
1583
|
-
if (extraArtifactsScan &&
|
|
1584
|
-
shouldRetryCodexScan(extraArtifactsScan.scanFullyCompleted, extraArtifactsScan.extraArtifacts)) {
|
|
1585
|
-
extraArtifactsScan = null;
|
|
1586
|
-
}
|
|
1587
|
-
const needsServicesScan = needsSetupScan && !servicesPlan;
|
|
1588
|
-
const needsEnvSecretsScan = needsSetupScan && !envSecretsScan;
|
|
1589
|
-
const needsExternalScan = needsSetupScan && !externalScan;
|
|
1590
|
-
const needsExtraArtifactsScan = needsSetupScan && !extraArtifactsScan;
|
|
1591
|
-
if (needsSetupScan || needsServicesScan) {
|
|
1592
|
-
const runLocalEnvironmentAnalysis = async ({ updateEnvSecrets, updateExternal, updateExtraArtifacts, updateServices, }) => {
|
|
1593
|
-
let envSecretsSchemaPath = null;
|
|
1594
|
-
let externalSchemaPath = null;
|
|
1595
|
-
let extraArtifactsSchemaPath = null;
|
|
1596
|
-
let servicesSchemaPath = null;
|
|
1597
|
-
if (needsEnvSecretsScan) {
|
|
1598
|
-
envSecretsSchemaPath =
|
|
1599
|
-
await writeSetupEnvSecretsSchema(setupTempDir);
|
|
1600
|
-
}
|
|
1601
|
-
if (needsExternalScan) {
|
|
1602
|
-
externalSchemaPath = await writeSetupExternalSchema(setupTempDir);
|
|
1603
|
-
}
|
|
1604
|
-
if (needsExtraArtifactsScan) {
|
|
1605
|
-
extraArtifactsSchemaPath =
|
|
1606
|
-
await writeSetupExtraArtifactsSchema(setupTempDir);
|
|
1607
|
-
}
|
|
1608
|
-
if (needsServicesScan) {
|
|
1609
|
-
servicesSchemaPath = await writeServicesSchema(setupTempDir);
|
|
1610
|
-
}
|
|
1611
|
-
if (needsEnvSecretsScan && !envSecretsSchemaPath) {
|
|
1612
|
-
throw new Error("Env/secrets schema path missing.");
|
|
1613
|
-
}
|
|
1614
|
-
if (needsExternalScan && !externalSchemaPath) {
|
|
1615
|
-
throw new Error("External schema path missing.");
|
|
1616
|
-
}
|
|
1617
|
-
if (needsExtraArtifactsScan && !extraArtifactsSchemaPath) {
|
|
1618
|
-
throw new Error("Extra artifacts schema path missing.");
|
|
1619
|
-
}
|
|
1620
|
-
if (needsServicesScan && !servicesSchemaPath) {
|
|
1621
|
-
throw new Error("Services schema path missing.");
|
|
1622
|
-
}
|
|
1623
|
-
if (needsSetupScan) {
|
|
1624
|
-
await fs.mkdir(scansDir, { recursive: true });
|
|
1625
|
-
}
|
|
1626
|
-
const envSecretsPromise = !needsSetupScan
|
|
1627
|
-
? Promise.resolve(null)
|
|
1628
|
-
: !needsEnvSecretsScan
|
|
1629
|
-
? Promise.resolve(envSecretsScan)
|
|
1630
|
-
: runCodexScanWithImmediateRetry({
|
|
1631
|
-
run: async () => {
|
|
1632
|
-
const threadId = await runLocalSetupEnvSecretsScan({
|
|
1633
|
-
cwd: repoRoot,
|
|
1634
|
-
logDir,
|
|
1635
|
-
schemaPath: envSecretsSchemaPath,
|
|
1636
|
-
outputPath: setupEnvSecretsScanPath,
|
|
1637
|
-
...(initCodexProxyOptions
|
|
1638
|
-
? { proxyOptions: initCodexProxyOptions }
|
|
1639
|
-
: {}),
|
|
1640
|
-
onProgress: updateEnvSecrets,
|
|
1641
|
-
});
|
|
1642
|
-
await saveScanThreadId("envSecretsThreadId", threadId);
|
|
1643
|
-
},
|
|
1644
|
-
read: async () => await readSetupEnvSecretsPlan(setupEnvSecretsScanPath),
|
|
1645
|
-
outputPath: setupEnvSecretsScanPath,
|
|
1646
|
-
update: updateEnvSecrets,
|
|
1647
|
-
shouldRetry: (plan) => shouldRetryCodexScan(plan.scanFullyCompleted, plan.envFiles, plan.secretFiles),
|
|
1648
|
-
});
|
|
1649
|
-
const externalPromise = !needsSetupScan
|
|
1650
|
-
? Promise.resolve(null)
|
|
1651
|
-
: !needsExternalScan
|
|
1652
|
-
? Promise.resolve(externalScan)
|
|
1653
|
-
: runCodexScanWithImmediateRetry({
|
|
1654
|
-
run: async () => {
|
|
1655
|
-
const threadId = await runLocalSetupExternalScan({
|
|
1656
|
-
cwd: repoRoot,
|
|
1657
|
-
logDir,
|
|
1658
|
-
schemaPath: externalSchemaPath,
|
|
1659
|
-
outputPath: setupExternalScanPath,
|
|
1660
|
-
homeDir: localHomeDir,
|
|
1661
|
-
...(initCodexProxyOptions
|
|
1662
|
-
? { proxyOptions: initCodexProxyOptions }
|
|
1663
|
-
: {}),
|
|
1664
|
-
onProgress: updateExternal,
|
|
1665
|
-
});
|
|
1666
|
-
await saveScanThreadId("externalThreadId", threadId);
|
|
1667
|
-
},
|
|
1668
|
-
read: async () => await readSetupExternalPlan(setupExternalScanPath),
|
|
1669
|
-
outputPath: setupExternalScanPath,
|
|
1670
|
-
update: updateExternal,
|
|
1671
|
-
shouldRetry: (plan) => shouldRetryCodexScan(plan.scanFullyCompleted, plan.externalDependencies, plan.externalConfigs),
|
|
1672
|
-
});
|
|
1673
|
-
const extraArtifactsPromise = !needsSetupScan
|
|
1674
|
-
? Promise.resolve(null)
|
|
1675
|
-
: !needsExtraArtifactsScan
|
|
1676
|
-
? Promise.resolve(extraArtifactsScan)
|
|
1677
|
-
: runCodexScanWithImmediateRetry({
|
|
1678
|
-
run: async () => {
|
|
1679
|
-
const threadId = await runLocalSetupExtraArtifactsScan({
|
|
1680
|
-
cwd: repoRoot,
|
|
1681
|
-
logDir,
|
|
1682
|
-
schemaPath: extraArtifactsSchemaPath,
|
|
1683
|
-
outputPath: setupExtraArtifactsScanPath,
|
|
1684
|
-
...(initCodexProxyOptions
|
|
1685
|
-
? { proxyOptions: initCodexProxyOptions }
|
|
1686
|
-
: {}),
|
|
1687
|
-
onProgress: updateExtraArtifacts,
|
|
1688
|
-
});
|
|
1689
|
-
await saveScanThreadId("extraArtifactsThreadId", threadId);
|
|
1690
|
-
},
|
|
1691
|
-
read: async () => await readSetupExtraArtifactsPlan(setupExtraArtifactsScanPath),
|
|
1692
|
-
outputPath: setupExtraArtifactsScanPath,
|
|
1693
|
-
update: updateExtraArtifacts,
|
|
1694
|
-
shouldRetry: (plan) => shouldRetryCodexScan(plan.scanFullyCompleted, plan.extraArtifacts),
|
|
1695
|
-
});
|
|
1696
|
-
const servicesPromise = !needsServicesScan
|
|
1697
|
-
? Promise.resolve(servicesPlan)
|
|
1698
|
-
: runCodexScanWithImmediateRetry({
|
|
1699
|
-
run: async () => {
|
|
1700
|
-
const threadId = await runLocalServicesScan({
|
|
1701
|
-
cwd: repoRoot,
|
|
1702
|
-
logDir,
|
|
1703
|
-
schemaPath: servicesSchemaPath,
|
|
1704
|
-
outputPath: servicesScanPath,
|
|
1705
|
-
homeDir: localHomeDir,
|
|
1706
|
-
...(initCodexProxyOptions
|
|
1707
|
-
? { proxyOptions: initCodexProxyOptions }
|
|
1708
|
-
: {}),
|
|
1709
|
-
onProgress: updateServices,
|
|
1710
|
-
});
|
|
1711
|
-
await saveScanThreadId("servicesThreadId", threadId);
|
|
1712
|
-
},
|
|
1713
|
-
read: async () => await readServicesPlan(servicesScanPath),
|
|
1714
|
-
outputPath: servicesScanPath,
|
|
1715
|
-
update: updateServices,
|
|
1716
|
-
shouldRetry: (plan) => shouldRetryCodexScan(plan.scanFullyCompleted, plan.appEntrypoints, plan.backgroundServices),
|
|
1717
|
-
});
|
|
1718
|
-
const [envSecrets, external, extraArtifacts, services] = await Promise.all([
|
|
1719
|
-
envSecretsPromise,
|
|
1720
|
-
externalPromise,
|
|
1721
|
-
extraArtifactsPromise,
|
|
1722
|
-
servicesPromise,
|
|
1723
|
-
]);
|
|
1724
|
-
await flushScanThreads();
|
|
1725
|
-
if (needsServicesScan) {
|
|
1726
|
-
if (!services) {
|
|
1727
|
-
throw new Error("Services scan missing.");
|
|
1728
|
-
}
|
|
1729
|
-
servicesPlan = services;
|
|
1730
|
-
}
|
|
1731
|
-
if (needsSetupScan) {
|
|
1732
|
-
if (!envSecrets) {
|
|
1733
|
-
throw new Error("Env/secrets scan missing.");
|
|
1734
|
-
}
|
|
1735
|
-
if (!external) {
|
|
1736
|
-
throw new Error("External scan missing.");
|
|
1737
|
-
}
|
|
1738
|
-
if (!extraArtifacts) {
|
|
1739
|
-
throw new Error("Extra artifacts scan missing.");
|
|
1740
|
-
}
|
|
1741
|
-
if (!servicesPlan) {
|
|
1742
|
-
throw new Error("Services scan missing.");
|
|
1743
|
-
}
|
|
1744
|
-
updateEnvSecrets("merging setup plan");
|
|
1745
|
-
const merged = mergeSetupScans({
|
|
1746
|
-
envSecrets,
|
|
1747
|
-
external,
|
|
1748
|
-
extraArtifacts,
|
|
1749
|
-
services: {
|
|
1750
|
-
appEntrypoints: servicesPlan.appEntrypoints,
|
|
1751
|
-
backgroundServices: servicesPlan.backgroundServices,
|
|
1752
|
-
},
|
|
1753
|
-
});
|
|
1754
|
-
await writeSetupPlan(setupPath, merged);
|
|
1755
|
-
setupPlan = merged;
|
|
1756
|
-
}
|
|
1757
|
-
};
|
|
1758
|
-
if (!progressEnabled) {
|
|
1759
|
-
await runLocalEnvironmentAnalysis({
|
|
1760
|
-
updateEnvSecrets: () => { },
|
|
1761
|
-
updateExternal: () => { },
|
|
1762
|
-
updateExtraArtifacts: () => { },
|
|
1763
|
-
updateServices: () => { },
|
|
1764
|
-
});
|
|
1765
|
-
}
|
|
1766
|
-
else {
|
|
1767
|
-
const log = clackTaskLog({
|
|
1768
|
-
title: "Analyzing local environment",
|
|
1769
|
-
limit: 1,
|
|
1770
|
-
spacing: 0,
|
|
1771
|
-
});
|
|
1772
|
-
let active = true;
|
|
1773
|
-
const colorCategory = (label) => {
|
|
1774
|
-
if (!process.stdout.hasColors?.())
|
|
1775
|
-
return label;
|
|
1776
|
-
const undim = "\u001b[22m";
|
|
1777
|
-
const dim = "\u001b[2m";
|
|
1778
|
-
const bold = "\u001b[1m";
|
|
1779
|
-
const teal = "\u001b[36m";
|
|
1780
|
-
const resetColor = "\u001b[39m";
|
|
1781
|
-
return `${undim}${teal}${bold}${label}${resetColor}${undim}${dim}`;
|
|
1782
|
-
};
|
|
1783
|
-
const formatRow = (label, message) => {
|
|
1784
|
-
const normalized = message.replace(/\r?\n/g, " ").trim();
|
|
1785
|
-
return `${colorCategory(label)}: ${normalized}`;
|
|
1786
|
-
};
|
|
1787
|
-
const envSecretsRow = log.group("");
|
|
1788
|
-
const externalRow = log.group("");
|
|
1789
|
-
const extraArtifactsRow = log.group("");
|
|
1790
|
-
const servicesRow = log.group("");
|
|
1791
|
-
const makeUpdater = (row, label) => (message) => {
|
|
1792
|
-
if (!active)
|
|
1793
|
-
return;
|
|
1794
|
-
row.message(formatRow(label, message));
|
|
1795
|
-
};
|
|
1796
|
-
const updateEnvSecrets = makeUpdater(envSecretsRow, "env/secrets");
|
|
1797
|
-
const updateExternal = makeUpdater(externalRow, "external");
|
|
1798
|
-
const updateExtraArtifacts = makeUpdater(extraArtifactsRow, "extra artifacts");
|
|
1799
|
-
const updateServices = makeUpdater(servicesRow, "services");
|
|
1800
|
-
updateEnvSecrets(needsSetupScan
|
|
1801
|
-
? needsEnvSecretsScan
|
|
1802
|
-
? "starting"
|
|
1803
|
-
: "cached"
|
|
1804
|
-
: "skipped");
|
|
1805
|
-
updateExternal(needsSetupScan
|
|
1806
|
-
? needsExternalScan
|
|
1807
|
-
? "starting"
|
|
1808
|
-
: "cached"
|
|
1809
|
-
: "skipped");
|
|
1810
|
-
updateExtraArtifacts(needsSetupScan
|
|
1811
|
-
? needsExtraArtifactsScan
|
|
1812
|
-
? "starting"
|
|
1813
|
-
: "cached"
|
|
1814
|
-
: "skipped");
|
|
1815
|
-
updateServices(needsServicesScan ? "starting" : "cached");
|
|
1816
|
-
try {
|
|
1817
|
-
await runLocalEnvironmentAnalysis({
|
|
1818
|
-
updateEnvSecrets,
|
|
1819
|
-
updateExternal,
|
|
1820
|
-
updateExtraArtifacts,
|
|
1821
|
-
updateServices,
|
|
1822
|
-
});
|
|
1823
|
-
active = false;
|
|
1824
|
-
log.success("Analyzing local environment");
|
|
1825
|
-
}
|
|
1826
|
-
catch (error) {
|
|
1827
|
-
active = false;
|
|
1828
|
-
log.error("Analyzing local environment");
|
|
1829
|
-
throw error;
|
|
1830
|
-
}
|
|
1831
|
-
}
|
|
1832
|
-
}
|
|
1833
|
-
if (!skipSetupPlan && !setupPlan) {
|
|
1834
|
-
setupPlan = await readSetupPlan(setupPath);
|
|
1835
|
-
}
|
|
1836
|
-
if (!skipSetupPlan && !setupPlan) {
|
|
1837
|
-
throw new Error("Setup plan missing.");
|
|
1838
|
-
}
|
|
1839
|
-
const scanStepUpdate = {};
|
|
1840
|
-
if (!skipSetupPlan && setupPlan) {
|
|
1841
|
-
if (!initState?.steps.setupEnvSecretsScanned) {
|
|
1842
|
-
scanStepUpdate.setupEnvSecretsScanned = true;
|
|
1843
|
-
}
|
|
1844
|
-
if (!initState?.steps.setupExternalScanned) {
|
|
1845
|
-
scanStepUpdate.setupExternalScanned = true;
|
|
1846
|
-
}
|
|
1847
|
-
if (!initState?.steps.setupExtraArtifactsScanned) {
|
|
1848
|
-
scanStepUpdate.setupExtraArtifactsScanned = true;
|
|
1849
|
-
}
|
|
1850
|
-
if (!initState?.steps.setupPlanScanned) {
|
|
1851
|
-
scanStepUpdate.setupPlanScanned = true;
|
|
1852
|
-
}
|
|
1853
|
-
if (!initState?.steps.servicesPlanScanned) {
|
|
1854
|
-
scanStepUpdate.servicesPlanScanned = true;
|
|
1855
|
-
}
|
|
1856
|
-
}
|
|
1857
|
-
if (Object.keys(scanStepUpdate).length > 0) {
|
|
1858
|
-
await updateInitState({ steps: scanStepUpdate });
|
|
1859
|
-
}
|
|
1860
|
-
if (skipSetupPlan) {
|
|
1861
|
-
approvedPlan = await readSetupPlan(setupPath);
|
|
1862
|
-
}
|
|
1863
|
-
if (shouldResume && approvedPlan) {
|
|
1864
|
-
const backfillStepUpdate = {};
|
|
1865
|
-
if (!initState?.steps.setupEnvSecretsScanned) {
|
|
1866
|
-
backfillStepUpdate.setupEnvSecretsScanned = true;
|
|
1867
|
-
}
|
|
1868
|
-
if (!initState?.steps.setupExternalScanned) {
|
|
1869
|
-
backfillStepUpdate.setupExternalScanned = true;
|
|
1870
|
-
}
|
|
1871
|
-
if (!initState?.steps.setupExtraArtifactsScanned) {
|
|
1872
|
-
backfillStepUpdate.setupExtraArtifactsScanned = true;
|
|
1873
|
-
}
|
|
1874
|
-
if (!initState?.steps.setupPlanScanned) {
|
|
1875
|
-
backfillStepUpdate.setupPlanScanned = true;
|
|
1876
|
-
}
|
|
1877
|
-
if (!initState?.steps.servicesPlanScanned) {
|
|
1878
|
-
backfillStepUpdate.servicesPlanScanned = true;
|
|
1879
|
-
}
|
|
1880
|
-
if (Object.keys(backfillStepUpdate).length > 0) {
|
|
1881
|
-
await updateInitState({ steps: backfillStepUpdate });
|
|
1882
|
-
}
|
|
1883
|
-
}
|
|
1884
|
-
const formatMissingSetupArtifact = (entry) => {
|
|
1885
|
-
const categoryLabel = entry.category === "envFiles" || entry.category === "secretFiles"
|
|
1886
|
-
? "env/secrets"
|
|
1887
|
-
: entry.category === "externalConfigs"
|
|
1888
|
-
? "external"
|
|
1889
|
-
: "extra artifacts";
|
|
1890
|
-
return `${categoryLabel}: ${entry.path}`;
|
|
1891
|
-
};
|
|
1892
|
-
const buildMissingPathsFeedback = (categoryLabel, entries) => [
|
|
1893
|
-
`Your previous ${categoryLabel} scan output included path(s) that do not exist on disk right now.`,
|
|
1894
|
-
"Fix the result and return corrected JSON. Only include paths that currently exist.",
|
|
1895
|
-
"Missing paths:",
|
|
1896
|
-
...entries.map((entry) => `- ${entry.path} (resolved: ${toRepoRelativePath(repoRoot, entry.resolvedPath)})`),
|
|
1897
|
-
].join("\n");
|
|
1898
|
-
const regenerateSetupPlanForMissingArtifacts = async ({ plan, missingArtifacts, status, }) => {
|
|
1899
|
-
const categories = new Set(missingArtifacts.map((entry) => entry.category));
|
|
1900
|
-
const needsEnvSecrets = categories.has("envFiles") || categories.has("secretFiles");
|
|
1901
|
-
const needsExternal = categories.has("externalConfigs");
|
|
1902
|
-
const needsExtraArtifacts = categories.has("extraArtifacts");
|
|
1903
|
-
let nextPlan = plan;
|
|
1904
|
-
if (needsEnvSecrets) {
|
|
1905
|
-
const envSecretsMissing = missingArtifacts.filter((entry) => entry.category === "envFiles" || entry.category === "secretFiles");
|
|
1906
|
-
const envSecretsSchemaPath = await writeSetupEnvSecretsSchema(setupTempDir);
|
|
1907
|
-
envSecretsScan = await runCodexScanWithImmediateRetry({
|
|
1908
|
-
run: async () => {
|
|
1909
|
-
const threadId = await runLocalSetupEnvSecretsScan({
|
|
1910
|
-
cwd: repoRoot,
|
|
1911
|
-
logDir,
|
|
1912
|
-
schemaPath: envSecretsSchemaPath,
|
|
1913
|
-
outputPath: setupEnvSecretsScanPath,
|
|
1914
|
-
...(scanThreads.envSecretsThreadId
|
|
1915
|
-
? { resumeThreadId: scanThreads.envSecretsThreadId }
|
|
1916
|
-
: {}),
|
|
1917
|
-
retryFeedback: buildMissingPathsFeedback("env/secrets", envSecretsMissing),
|
|
1918
|
-
...(initCodexProxyOptions
|
|
1919
|
-
? { proxyOptions: initCodexProxyOptions }
|
|
1920
|
-
: {}),
|
|
1921
|
-
onProgress: (message) => status.stage(`Regenerating env/secrets scan — ${message}`),
|
|
1922
|
-
});
|
|
1923
|
-
await saveScanThreadId("envSecretsThreadId", threadId);
|
|
1924
|
-
},
|
|
1925
|
-
read: async () => await readSetupEnvSecretsPlan(setupEnvSecretsScanPath),
|
|
1926
|
-
outputPath: setupEnvSecretsScanPath,
|
|
1927
|
-
update: (message) => status.stage(`Regenerating env/secrets scan — ${message}`),
|
|
1928
|
-
shouldRetry: (scan) => shouldRetryCodexScan(scan.scanFullyCompleted, scan.envFiles, scan.secretFiles),
|
|
1929
|
-
});
|
|
1930
|
-
nextPlan = {
|
|
1931
|
-
...nextPlan,
|
|
1932
|
-
envFiles: remapSelectedPathEntries({
|
|
1933
|
-
selected: nextPlan.envFiles,
|
|
1934
|
-
refreshed: envSecretsScan.envFiles,
|
|
1935
|
-
}),
|
|
1936
|
-
secretFiles: remapSelectedPathEntries({
|
|
1937
|
-
selected: nextPlan.secretFiles,
|
|
1938
|
-
refreshed: envSecretsScan.secretFiles,
|
|
1939
|
-
}),
|
|
1940
|
-
};
|
|
1941
|
-
}
|
|
1942
|
-
if (needsExternal) {
|
|
1943
|
-
const externalMissing = missingArtifacts.filter((entry) => entry.category === "externalConfigs");
|
|
1944
|
-
const externalSchemaPath = await writeSetupExternalSchema(setupTempDir);
|
|
1945
|
-
externalScan = await runCodexScanWithImmediateRetry({
|
|
1946
|
-
run: async () => {
|
|
1947
|
-
const threadId = await runLocalSetupExternalScan({
|
|
1948
|
-
cwd: repoRoot,
|
|
1949
|
-
logDir,
|
|
1950
|
-
schemaPath: externalSchemaPath,
|
|
1951
|
-
outputPath: setupExternalScanPath,
|
|
1952
|
-
homeDir: localHomeDir,
|
|
1953
|
-
...(scanThreads.externalThreadId
|
|
1954
|
-
? { resumeThreadId: scanThreads.externalThreadId }
|
|
1955
|
-
: {}),
|
|
1956
|
-
retryFeedback: buildMissingPathsFeedback("external", externalMissing),
|
|
1957
|
-
...(initCodexProxyOptions
|
|
1958
|
-
? { proxyOptions: initCodexProxyOptions }
|
|
1959
|
-
: {}),
|
|
1960
|
-
onProgress: (message) => status.stage(`Regenerating external scan — ${message}`),
|
|
1961
|
-
});
|
|
1962
|
-
await saveScanThreadId("externalThreadId", threadId);
|
|
1963
|
-
},
|
|
1964
|
-
read: async () => await readSetupExternalPlan(setupExternalScanPath),
|
|
1965
|
-
outputPath: setupExternalScanPath,
|
|
1966
|
-
update: (message) => status.stage(`Regenerating external scan — ${message}`),
|
|
1967
|
-
shouldRetry: (scan) => shouldRetryCodexScan(scan.scanFullyCompleted, scan.externalDependencies, scan.externalConfigs),
|
|
1968
|
-
});
|
|
1969
|
-
nextPlan = {
|
|
1970
|
-
...nextPlan,
|
|
1971
|
-
externalConfigs: remapSelectedPathEntries({
|
|
1972
|
-
selected: nextPlan.externalConfigs,
|
|
1973
|
-
refreshed: externalScan.externalConfigs,
|
|
1974
|
-
}),
|
|
1975
|
-
};
|
|
1976
|
-
}
|
|
1977
|
-
if (needsExtraArtifacts) {
|
|
1978
|
-
const extraArtifactsMissing = missingArtifacts.filter((entry) => entry.category === "extraArtifacts");
|
|
1979
|
-
const extraArtifactsSchemaPath = await writeSetupExtraArtifactsSchema(setupTempDir);
|
|
1980
|
-
extraArtifactsScan = await runCodexScanWithImmediateRetry({
|
|
1981
|
-
run: async () => {
|
|
1982
|
-
const threadId = await runLocalSetupExtraArtifactsScan({
|
|
1983
|
-
cwd: repoRoot,
|
|
1984
|
-
logDir,
|
|
1985
|
-
schemaPath: extraArtifactsSchemaPath,
|
|
1986
|
-
outputPath: setupExtraArtifactsScanPath,
|
|
1987
|
-
...(scanThreads.extraArtifactsThreadId
|
|
1988
|
-
? { resumeThreadId: scanThreads.extraArtifactsThreadId }
|
|
1989
|
-
: {}),
|
|
1990
|
-
retryFeedback: buildMissingPathsFeedback("extra artifacts", extraArtifactsMissing),
|
|
1991
|
-
...(initCodexProxyOptions
|
|
1992
|
-
? { proxyOptions: initCodexProxyOptions }
|
|
1993
|
-
: {}),
|
|
1994
|
-
onProgress: (message) => status.stage(`Regenerating extra artifacts scan — ${message}`),
|
|
1995
|
-
});
|
|
1996
|
-
await saveScanThreadId("extraArtifactsThreadId", threadId);
|
|
1997
|
-
},
|
|
1998
|
-
read: async () => await readSetupExtraArtifactsPlan(setupExtraArtifactsScanPath),
|
|
1999
|
-
outputPath: setupExtraArtifactsScanPath,
|
|
2000
|
-
update: (message) => status.stage(`Regenerating extra artifacts scan — ${message}`),
|
|
2001
|
-
shouldRetry: (scan) => shouldRetryCodexScan(scan.scanFullyCompleted, scan.extraArtifacts),
|
|
2002
|
-
});
|
|
2003
|
-
nextPlan = {
|
|
2004
|
-
...nextPlan,
|
|
2005
|
-
extraArtifacts: remapSelectedPathEntries({
|
|
2006
|
-
selected: nextPlan.extraArtifacts,
|
|
2007
|
-
refreshed: extraArtifactsScan.extraArtifacts,
|
|
2008
|
-
}),
|
|
2009
|
-
};
|
|
2010
|
-
}
|
|
2011
|
-
await flushScanThreads();
|
|
2012
|
-
return nextPlan;
|
|
2013
|
-
};
|
|
2014
|
-
if (nonInteractive) {
|
|
2015
|
-
if (setupPlan)
|
|
2016
|
-
approvedPlan = setupPlan;
|
|
2017
|
-
}
|
|
2018
|
-
else if (!skipSetupPlan && setupPlan) {
|
|
2019
|
-
if (!process.stdin.isTTY || parsed.json) {
|
|
2020
|
-
throw new Error("Interactive terminal required to approve setup.");
|
|
2021
|
-
}
|
|
2022
|
-
const statCache = new Map();
|
|
2023
|
-
const readPathInfo = async (candidatePath) => {
|
|
2024
|
-
const cached = statCache.get(candidatePath);
|
|
2025
|
-
if (cached)
|
|
2026
|
-
return cached;
|
|
2027
|
-
const resolved = path.isAbsolute(candidatePath)
|
|
2028
|
-
? candidatePath
|
|
2029
|
-
: path.resolve(repoRoot, candidatePath);
|
|
2030
|
-
const relative = toRepoRelativePath(repoRoot, candidatePath);
|
|
2031
|
-
const outsideRepo = isOutsideRepoPath(relative);
|
|
2032
|
-
let isDirectory = false;
|
|
2033
|
-
try {
|
|
2034
|
-
const stat = await fs.stat(resolved);
|
|
2035
|
-
isDirectory = stat.isDirectory();
|
|
2036
|
-
}
|
|
2037
|
-
catch {
|
|
2038
|
-
isDirectory = false;
|
|
2039
|
-
}
|
|
2040
|
-
const info = { relative, outsideRepo, isDirectory };
|
|
2041
|
-
statCache.set(candidatePath, info);
|
|
2042
|
-
return info;
|
|
2043
|
-
};
|
|
2044
|
-
const buildApprovalSummary = async (plan) => {
|
|
2045
|
-
const lines = [];
|
|
2046
|
-
lines.push("Setup");
|
|
2047
|
-
lines.push(`- .env files: ${plan.envFiles.length}`);
|
|
2048
|
-
for (const entry of plan.envFiles) {
|
|
2049
|
-
lines.push(` - ${toRepoRelativePath(repoRoot, entry.path)}`);
|
|
2050
|
-
}
|
|
2051
|
-
lines.push(`- Secret/config files: ${plan.secretFiles.length}`);
|
|
2052
|
-
for (const entry of plan.secretFiles) {
|
|
2053
|
-
lines.push(` - ${toRepoRelativePath(repoRoot, entry.path)}`);
|
|
2054
|
-
}
|
|
2055
|
-
lines.push(`- Other artifacts: ${plan.extraArtifacts.length}`);
|
|
2056
|
-
for (const entry of plan.extraArtifacts) {
|
|
2057
|
-
lines.push(` - ${toRepoRelativePath(repoRoot, entry.path)}`);
|
|
2058
|
-
}
|
|
2059
|
-
lines.push(`- External dependencies: ${plan.externalDependencies.length}`);
|
|
2060
|
-
for (const entry of plan.externalDependencies) {
|
|
2061
|
-
lines.push(` - ${entry.version ? `${entry.name}@${entry.version}` : entry.name}`);
|
|
2062
|
-
}
|
|
2063
|
-
lines.push(`- External config/secret files: ${plan.externalConfigs.length}`);
|
|
2064
|
-
for (const entry of plan.externalConfigs) {
|
|
2065
|
-
lines.push(` - ${toRepoRelativePath(repoRoot, entry.path)}`);
|
|
2066
|
-
}
|
|
2067
|
-
lines.push("");
|
|
2068
|
-
lines.push("Services");
|
|
2069
|
-
lines.push(`- App entrypoints: ${plan.services.appEntrypoints.length}`);
|
|
2070
|
-
for (const entry of plan.services.appEntrypoints) {
|
|
2071
|
-
lines.push(` - ${entry.command}`);
|
|
2072
|
-
}
|
|
2073
|
-
lines.push(`- Background services: ${plan.services.backgroundServices.length}`);
|
|
2074
|
-
for (const entry of plan.services.backgroundServices) {
|
|
2075
|
-
lines.push(` - ${entry.name}`);
|
|
2076
|
-
}
|
|
2077
|
-
const candidatePaths = new Set();
|
|
2078
|
-
for (const entry of [
|
|
2079
|
-
...plan.envFiles,
|
|
2080
|
-
...plan.secretFiles,
|
|
2081
|
-
...plan.extraArtifacts,
|
|
2082
|
-
...plan.externalConfigs,
|
|
2083
|
-
]) {
|
|
2084
|
-
candidatePaths.add(entry.path);
|
|
2085
|
-
}
|
|
2086
|
-
const outsideRepoPaths = [];
|
|
2087
|
-
const directoryPaths = [];
|
|
2088
|
-
for (const candidatePath of candidatePaths) {
|
|
2089
|
-
const info = await readPathInfo(candidatePath);
|
|
2090
|
-
if (info.outsideRepo)
|
|
2091
|
-
outsideRepoPaths.push(info.relative);
|
|
2092
|
-
if (info.isDirectory)
|
|
2093
|
-
directoryPaths.push(info.relative);
|
|
2094
|
-
}
|
|
2095
|
-
outsideRepoPaths.sort();
|
|
2096
|
-
directoryPaths.sort();
|
|
2097
|
-
const hasRisk = outsideRepoPaths.length > 0 || directoryPaths.length > 0;
|
|
2098
|
-
const warningLines = [];
|
|
2099
|
-
if (outsideRepoPaths.length > 0) {
|
|
2100
|
-
warningLines.push("Outside repo:");
|
|
2101
|
-
for (const entry of outsideRepoPaths) {
|
|
2102
|
-
warningLines.push(`- ${entry}`);
|
|
2103
|
-
}
|
|
2104
|
-
warningLines.push("");
|
|
2105
|
-
}
|
|
2106
|
-
if (directoryPaths.length > 0) {
|
|
2107
|
-
warningLines.push("Directories:");
|
|
2108
|
-
for (const entry of directoryPaths) {
|
|
2109
|
-
warningLines.push(`- ${entry}`);
|
|
2110
|
-
}
|
|
2111
|
-
}
|
|
2112
|
-
return {
|
|
2113
|
-
summary: lines.join("\n"),
|
|
2114
|
-
warning: warningLines.length > 0 ? warningLines.join("\n") : null,
|
|
2115
|
-
hasRisk,
|
|
2116
|
-
};
|
|
2117
|
-
};
|
|
2118
|
-
let draftSetup = setupPlan;
|
|
2119
|
-
while (true) {
|
|
2120
|
-
const nextSetup = await promptForPlanApproval({
|
|
2121
|
-
plan: setupPlan,
|
|
2122
|
-
repoRoot,
|
|
2123
|
-
initialPlan: draftSetup,
|
|
2124
|
-
});
|
|
2125
|
-
const nextServices = await promptForServicesApproval({
|
|
2126
|
-
plan: setupPlan.services,
|
|
2127
|
-
initialSelection: draftSetup?.services ?? null,
|
|
2128
|
-
});
|
|
2129
|
-
const nextPlan = { ...nextSetup, services: nextServices };
|
|
2130
|
-
const { summary, warning, hasRisk } = await buildApprovalSummary(nextPlan);
|
|
2131
|
-
clackNote(summary, "Selected setup requirements");
|
|
2132
|
-
if (warning) {
|
|
2133
|
-
clackNote(warning, "Special attention");
|
|
2134
|
-
}
|
|
2135
|
-
const decision = await clackSelect({
|
|
2136
|
-
message: "Proceed with these selections?",
|
|
2137
|
-
options: [
|
|
2138
|
-
{ value: "proceed", label: "Proceed" },
|
|
2139
|
-
{ value: "edit", label: "Edit selections" },
|
|
2140
|
-
{ value: "cancel", label: "Cancel" },
|
|
2141
|
-
],
|
|
2142
|
-
initialValue: "proceed",
|
|
2143
|
-
});
|
|
2144
|
-
if (isCancel(decision) || decision === "cancel") {
|
|
2145
|
-
throwInitCanceled();
|
|
2146
|
-
}
|
|
2147
|
-
if (decision === "edit") {
|
|
2148
|
-
draftSetup = nextPlan;
|
|
2149
|
-
continue;
|
|
2150
|
-
}
|
|
2151
|
-
if (hasRisk) {
|
|
2152
|
-
const confirmed = await clackConfirm({
|
|
2153
|
-
message: "You selected items outside the repo and/or directories. Continue?",
|
|
2154
|
-
active: "Continue",
|
|
2155
|
-
inactive: "Edit selections",
|
|
2156
|
-
initialValue: true,
|
|
2157
|
-
});
|
|
2158
|
-
if (isCancel(confirmed)) {
|
|
2159
|
-
throwInitCanceled();
|
|
2160
|
-
}
|
|
2161
|
-
if (!confirmed) {
|
|
2162
|
-
draftSetup = nextPlan;
|
|
2163
|
-
continue;
|
|
2164
|
-
}
|
|
2165
|
-
}
|
|
2166
|
-
approvedPlan = nextPlan;
|
|
2167
|
-
break;
|
|
2168
|
-
}
|
|
2169
|
-
}
|
|
2170
|
-
if (!approvedPlan) {
|
|
2171
|
-
throw new Error("Setup plan missing.");
|
|
2172
|
-
}
|
|
2173
|
-
let ensuredPlan = approvedPlan;
|
|
2174
|
-
if (!skipSetupPlan) {
|
|
2175
|
-
await writeSetupPlan(setupPath, ensuredPlan);
|
|
2176
|
-
await updateInitState({ steps: { setupPlanWritten: true } });
|
|
2177
|
-
}
|
|
2178
|
-
if (!skipSetupUpload) {
|
|
2179
|
-
setupArtifacts = await runInitStep({
|
|
2180
|
-
enabled: progressEnabled,
|
|
2181
|
-
title: "Packaging setup artifacts",
|
|
2182
|
-
fn: async ({ status }) => {
|
|
2183
|
-
let planForArtifacts = ensuredPlan;
|
|
2184
|
-
for (let attempt = 1; attempt <= SETUP_ARTIFACT_REGEN_MAX_ATTEMPTS + 1; attempt += 1) {
|
|
2185
|
-
const missingArtifacts = await collectMissingSetupArtifacts({
|
|
2186
|
-
repoRoot,
|
|
2187
|
-
homeDir: localHomeDir,
|
|
2188
|
-
plan: planForArtifacts,
|
|
2189
|
-
});
|
|
2190
|
-
if (missingArtifacts.length === 0) {
|
|
2191
|
-
ensuredPlan = planForArtifacts;
|
|
2192
|
-
approvedPlan = planForArtifacts;
|
|
2193
|
-
return await createSetupArtifacts({
|
|
2194
|
-
repoRoot,
|
|
2195
|
-
plan: planForArtifacts,
|
|
2196
|
-
outputDir: setupDir,
|
|
2197
|
-
tempDir: setupTempDir,
|
|
2198
|
-
homeDir: localHomeDir,
|
|
2199
|
-
});
|
|
2200
|
-
}
|
|
2201
|
-
if (attempt > SETUP_ARTIFACT_REGEN_MAX_ATTEMPTS) {
|
|
2202
|
-
const lines = missingArtifacts
|
|
2203
|
-
.map((entry) => `- ${formatMissingSetupArtifact(entry)}`)
|
|
2204
|
-
.sort((a, b) => a.localeCompare(b));
|
|
2205
|
-
throw new Error([
|
|
2206
|
-
"Setup artifact scan returned paths that do not exist locally.",
|
|
2207
|
-
`Retried regeneration ${SETUP_ARTIFACT_REGEN_MAX_ATTEMPTS} time(s) and still found missing files:`,
|
|
2208
|
-
...lines,
|
|
2209
|
-
].join("\n"));
|
|
2210
|
-
}
|
|
2211
|
-
const needsEnvSecrets = missingArtifacts.some((entry) => entry.category === "envFiles" ||
|
|
2212
|
-
entry.category === "secretFiles");
|
|
2213
|
-
const needsExternal = missingArtifacts.some((entry) => entry.category === "externalConfigs");
|
|
2214
|
-
const needsExtraArtifacts = missingArtifacts.some((entry) => entry.category === "extraArtifacts");
|
|
2215
|
-
const scanLabels = [
|
|
2216
|
-
...(needsEnvSecrets ? ["env/secrets"] : []),
|
|
2217
|
-
...(needsExternal ? ["external"] : []),
|
|
2218
|
-
...(needsExtraArtifacts ? ["extra artifacts"] : []),
|
|
2219
|
-
];
|
|
2220
|
-
status.stage(`Found ${missingArtifacts.length} missing setup artifact path(s); regenerating ${scanLabels.join(", ")} scan(s) (${attempt}/${SETUP_ARTIFACT_REGEN_MAX_ATTEMPTS})`);
|
|
2221
|
-
planForArtifacts = await regenerateSetupPlanForMissingArtifacts({
|
|
2222
|
-
plan: planForArtifacts,
|
|
2223
|
-
missingArtifacts,
|
|
2224
|
-
status,
|
|
2225
|
-
});
|
|
2226
|
-
await writeSetupPlan(setupPath, planForArtifacts);
|
|
2227
|
-
}
|
|
2228
|
-
throw new Error("Unreachable setup artifacts retry path.");
|
|
2229
|
-
},
|
|
2230
|
-
});
|
|
2231
|
-
}
|
|
2232
|
-
}
|
|
2233
|
-
finally {
|
|
2234
|
-
await fs.rm(setupTempDir, { recursive: true, force: true });
|
|
2235
|
-
}
|
|
2236
|
-
if (!approvedPlan) {
|
|
2237
|
-
throw new Error("Setup plan missing.");
|
|
2238
|
-
}
|
|
2239
|
-
const finalApprovedPlan = approvedPlan;
|
|
2240
|
-
const skipProvision = shouldResume && initState?.steps.workdirProvisioned;
|
|
2241
|
-
if (skipProvision && !skipSetupUpload) {
|
|
2242
|
-
await runInitStep({
|
|
2243
|
-
enabled: progressEnabled,
|
|
2244
|
-
title: "Uploading setup plan",
|
|
2245
|
-
fn: async ({ status }) => {
|
|
2246
|
-
await uploadSetupPlan({
|
|
2247
|
-
client,
|
|
2248
|
-
canonical,
|
|
2249
|
-
localSetupPath: setupPath,
|
|
2250
|
-
remoteSetupPath,
|
|
2251
|
-
localArtifactsBundlePath: setupArtifacts?.bundlePath ?? null,
|
|
2252
|
-
localArtifactsManifestPath: setupArtifacts?.manifestPath ?? null,
|
|
2253
|
-
localArtifactsPartsDescriptorPath: setupArtifacts?.partsDescriptorPath ?? null,
|
|
2254
|
-
remoteArtifactsBundlePath: setupArtifacts
|
|
2255
|
-
? path.posix.join(expandedWorkdir, ".devbox", "setup-artifacts.tgz")
|
|
2256
|
-
: null,
|
|
2257
|
-
remoteArtifactsManifestPath: setupArtifacts
|
|
2258
|
-
? path.posix.join(expandedWorkdir, ".devbox", "setup-artifacts.json")
|
|
2259
|
-
: null,
|
|
2260
|
-
remoteArtifactsPartsDescriptorPath: setupArtifacts
|
|
2261
|
-
? remoteArtifactsPartsDescriptorPath
|
|
2262
|
-
: null,
|
|
2263
|
-
status,
|
|
2264
|
-
});
|
|
2265
|
-
},
|
|
2266
|
-
});
|
|
2267
|
-
await updateInitState({ steps: { setupUploaded: true } });
|
|
2268
|
-
await updateRegistryProjectStatus(resolveInitStatus(initState?.steps, initState?.complete));
|
|
2269
|
-
}
|
|
2270
|
-
else if (!skipProvision || !skipSetupUpload) {
|
|
2271
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "devbox-init-"));
|
|
2272
|
-
const bundlePath = path.join(tempDir, "repo.bundle");
|
|
2273
|
-
const gitMetaPath = path.join(tempDir, "git-meta.tgz");
|
|
2274
|
-
const gitMetaListPath = path.join(tempDir, "git-meta.list");
|
|
2275
|
-
const stagedPatchPath = path.join(tempDir, "staged.patch");
|
|
2276
|
-
const unstagedPatchPath = path.join(tempDir, "unstaged.patch");
|
|
2277
|
-
const untrackedPath = path.join(tempDir, "untracked.tgz");
|
|
2278
|
-
const untrackedListPath = path.join(tempDir, "untracked.list");
|
|
2279
|
-
const globalGitConfigSources = await readGlobalGitConfigFiles(repoRoot);
|
|
2280
|
-
const globalGitConfigMappings = mapGlobalGitConfigDestinations(globalGitConfigSources, localHomeDir);
|
|
2281
|
-
try {
|
|
2282
|
-
const remoteBundlePath = "/home/sprite/.devbox/upload.bundle";
|
|
2283
|
-
const remoteGitMetaPath = "/home/sprite/.devbox/git-meta.tgz";
|
|
2284
|
-
const remoteStagedPatchPath = "/home/sprite/.devbox/staged.patch";
|
|
2285
|
-
const remoteUnstagedPatchPath = "/home/sprite/.devbox/unstaged.patch";
|
|
2286
|
-
const remoteUntrackedPath = "/home/sprite/.devbox/untracked.tgz";
|
|
2287
|
-
await runInitStep({
|
|
2288
|
-
enabled: progressEnabled,
|
|
2289
|
-
title: "Preparing remote directories",
|
|
2290
|
-
fn: async ({ status }) => {
|
|
2291
|
-
status.stage("Checking remote git");
|
|
2292
|
-
const gitCheck = await client.exec(canonical, [
|
|
2293
|
-
"/bin/bash",
|
|
2294
|
-
"-lc",
|
|
2295
|
-
"git --version",
|
|
2296
|
-
]);
|
|
2297
|
-
if (gitCheck.exitCode !== 0) {
|
|
2298
|
-
const details = gitCheck.stderr || gitCheck.stdout || "";
|
|
2299
|
-
throw new Error(details
|
|
2300
|
-
? `Remote git unavailable: ${details.trim()}`
|
|
2301
|
-
: "Remote git unavailable");
|
|
2302
|
-
}
|
|
2303
|
-
const remoteDirs = new Set();
|
|
2304
|
-
remoteDirs.add(path.posix.dirname(remoteBundlePath));
|
|
2305
|
-
remoteDirs.add(path.posix.dirname(remoteGitMetaPath));
|
|
2306
|
-
remoteDirs.add(path.posix.dirname(remoteStagedPatchPath));
|
|
2307
|
-
remoteDirs.add(path.posix.dirname(remoteUnstagedPatchPath));
|
|
2308
|
-
remoteDirs.add(path.posix.dirname(remoteUntrackedPath));
|
|
2309
|
-
for (const mapping of globalGitConfigMappings) {
|
|
2310
|
-
remoteDirs.add(path.posix.dirname(mapping.dest));
|
|
2311
|
-
}
|
|
2312
|
-
if (remoteDirs.size > 0) {
|
|
2313
|
-
status.stage("Preparing remote directories");
|
|
2314
|
-
const prepResult = await client.exec(canonical, [
|
|
2315
|
-
"/bin/bash",
|
|
2316
|
-
"-lc",
|
|
2317
|
-
`mkdir -p ${[...remoteDirs].map(shellQuote).join(" ")}`,
|
|
2318
|
-
]);
|
|
2319
|
-
if (prepResult.exitCode !== 0) {
|
|
2320
|
-
throw new Error(prepResult.stderr || "Failed to prepare remote dirs");
|
|
2321
|
-
}
|
|
2322
|
-
}
|
|
2323
|
-
},
|
|
2324
|
-
});
|
|
2325
|
-
const { headState, gitCommonDir, worktreeState, copyWorktree } = await runInitStep({
|
|
2326
|
-
enabled: progressEnabled,
|
|
2327
|
-
title: "Inspecting repo state",
|
|
2328
|
-
fn: async ({ status }) => {
|
|
2329
|
-
const headState = await readHeadState(repoRoot);
|
|
2330
|
-
const resolved = await resolveGitCommonDir(repoRoot);
|
|
2331
|
-
const worktreeState = await readWorktreeState(repoRoot);
|
|
2332
|
-
const hasWorktreeChanges = worktreeState.staged.length > 0 ||
|
|
2333
|
-
worktreeState.unstaged.length > 0 ||
|
|
2334
|
-
worktreeState.untracked.length > 0;
|
|
2335
|
-
let copyWorktree = false;
|
|
2336
|
-
if (hasWorktreeChanges && process.stdin.isTTY && !parsed.json) {
|
|
2337
|
-
status.stop();
|
|
2338
|
-
copyWorktree = await confirmCopyWorktree(worktreeState);
|
|
2339
|
-
status.stage("Inspecting repo state");
|
|
2340
|
-
}
|
|
2341
|
-
else if (hasWorktreeChanges) {
|
|
2342
|
-
copyWorktree = true;
|
|
2343
|
-
}
|
|
2344
|
-
return {
|
|
2345
|
-
headState,
|
|
2346
|
-
gitCommonDir: resolved.commonDir,
|
|
2347
|
-
worktreeState,
|
|
2348
|
-
copyWorktree,
|
|
2349
|
-
};
|
|
2350
|
-
},
|
|
2351
|
-
});
|
|
2352
|
-
const packaged = await runInitStep({
|
|
2353
|
-
enabled: progressEnabled,
|
|
2354
|
-
title: "Packaging repo",
|
|
2355
|
-
fn: async ({ status }) => {
|
|
2356
|
-
let gitMetaCreated = false;
|
|
2357
|
-
let stagedPatchCreated = false;
|
|
2358
|
-
let unstagedPatchCreated = false;
|
|
2359
|
-
let untrackedCreated = false;
|
|
2360
|
-
status.stage("Packaging repo bundle");
|
|
2361
|
-
await runCommand(repoRoot, "git", [
|
|
2362
|
-
"bundle",
|
|
2363
|
-
"create",
|
|
2364
|
-
bundlePath,
|
|
2365
|
-
"--all",
|
|
2366
|
-
]);
|
|
2367
|
-
status.stage("Packaging git metadata");
|
|
2368
|
-
gitMetaCreated = await createGitMetaArchive(gitCommonDir, gitMetaPath, gitMetaListPath);
|
|
2369
|
-
if (copyWorktree) {
|
|
2370
|
-
status.stage("Packaging repo changes");
|
|
2371
|
-
stagedPatchCreated = await writePatch(repoRoot, ["diff", "--binary", "--cached"], stagedPatchPath);
|
|
2372
|
-
unstagedPatchCreated = await writePatch(repoRoot, ["diff", "--binary"], unstagedPatchPath);
|
|
2373
|
-
untrackedCreated = await createFileListArchive(repoRoot, worktreeState.untracked, untrackedPath, untrackedListPath);
|
|
2374
|
-
}
|
|
2375
|
-
return {
|
|
2376
|
-
gitMetaCreated,
|
|
2377
|
-
stagedPatchCreated,
|
|
2378
|
-
unstagedPatchCreated,
|
|
2379
|
-
untrackedCreated,
|
|
2380
|
-
};
|
|
2381
|
-
},
|
|
2382
|
-
});
|
|
2383
|
-
await runInitStep({
|
|
2384
|
-
enabled: progressEnabled,
|
|
2385
|
-
title: "Uploading repo",
|
|
2386
|
-
fn: async ({ status }) => {
|
|
2387
|
-
const uploadItems = [
|
|
2388
|
-
{
|
|
2389
|
-
label: "repo bundle",
|
|
2390
|
-
localPath: bundlePath,
|
|
2391
|
-
remotePath: remoteBundlePath,
|
|
2392
|
-
},
|
|
2393
|
-
];
|
|
2394
|
-
if (packaged.gitMetaCreated) {
|
|
2395
|
-
uploadItems.push({
|
|
2396
|
-
label: "git metadata",
|
|
2397
|
-
localPath: gitMetaPath,
|
|
2398
|
-
remotePath: remoteGitMetaPath,
|
|
2399
|
-
});
|
|
2400
|
-
}
|
|
2401
|
-
if (packaged.stagedPatchCreated) {
|
|
2402
|
-
uploadItems.push({
|
|
2403
|
-
label: "staged changes",
|
|
2404
|
-
localPath: stagedPatchPath,
|
|
2405
|
-
remotePath: remoteStagedPatchPath,
|
|
2406
|
-
});
|
|
2407
|
-
}
|
|
2408
|
-
if (packaged.unstagedPatchCreated) {
|
|
2409
|
-
uploadItems.push({
|
|
2410
|
-
label: "unstaged changes",
|
|
2411
|
-
localPath: unstagedPatchPath,
|
|
2412
|
-
remotePath: remoteUnstagedPatchPath,
|
|
2413
|
-
});
|
|
2414
|
-
}
|
|
2415
|
-
if (packaged.untrackedCreated) {
|
|
2416
|
-
uploadItems.push({
|
|
2417
|
-
label: "untracked files",
|
|
2418
|
-
localPath: untrackedPath,
|
|
2419
|
-
remotePath: remoteUntrackedPath,
|
|
2420
|
-
});
|
|
2421
|
-
}
|
|
2422
|
-
for (const mapping of globalGitConfigMappings) {
|
|
2423
|
-
uploadItems.push({
|
|
2424
|
-
label: `git config (${path.basename(mapping.source)})`,
|
|
2425
|
-
localPath: mapping.source,
|
|
2426
|
-
remotePath: mapping.dest,
|
|
2427
|
-
});
|
|
2428
|
-
}
|
|
2429
|
-
const plannedUploads = await Promise.all(uploadItems.map(async (item) => {
|
|
2430
|
-
const stats = await fs.stat(item.localPath);
|
|
2431
|
-
return { ...item, size: stats.size };
|
|
2432
|
-
}));
|
|
2433
|
-
const totalBytes = plannedUploads.reduce((sum, item) => sum + item.size, 0);
|
|
2434
|
-
let uploadedBytes = 0;
|
|
2435
|
-
const updateProgress = (currentFileBytes, detail) => status.byteProgress({
|
|
2436
|
-
title: "Uploading repo",
|
|
2437
|
-
uploadedBytes: uploadedBytes + currentFileBytes,
|
|
2438
|
-
totalBytes,
|
|
2439
|
-
detail,
|
|
2440
|
-
});
|
|
2441
|
-
for (const [index, upload] of plannedUploads.entries()) {
|
|
2442
|
-
const detail = `${upload.label} (${index + 1}/${plannedUploads.length})`;
|
|
2443
|
-
const fileData = await fs.readFile(upload.localPath);
|
|
2444
|
-
updateProgress(0, detail);
|
|
2445
|
-
await client.writeFile(canonical, upload.remotePath, fileData, {
|
|
2446
|
-
onProgress: (fileUploadedBytes, fileTotalBytes) => {
|
|
2447
|
-
const bounded = Math.min(fileUploadedBytes, Math.max(fileData.length, fileTotalBytes));
|
|
2448
|
-
updateProgress(bounded, detail);
|
|
2449
|
-
},
|
|
2450
|
-
});
|
|
2451
|
-
uploadedBytes += fileData.length;
|
|
2452
|
-
updateProgress(fileData.length, detail);
|
|
2453
|
-
}
|
|
2454
|
-
status.byteProgress({
|
|
2455
|
-
title: "Uploading repo",
|
|
2456
|
-
uploadedBytes: totalBytes,
|
|
2457
|
-
totalBytes,
|
|
2458
|
-
detail: "completed",
|
|
2459
|
-
});
|
|
2460
|
-
},
|
|
2461
|
-
});
|
|
2462
|
-
await runInitStep({
|
|
2463
|
-
enabled: progressEnabled,
|
|
2464
|
-
title: "Provisioning workdir",
|
|
2465
|
-
fn: async () => {
|
|
2466
|
-
const backup = `${expandedWorkdir}.bak-${Date.now()}`;
|
|
2467
|
-
const checkoutCommand = headState.branch
|
|
2468
|
-
? `git checkout -B ${shellQuote(headState.branch)} ${shellQuote(headState.commit)}`
|
|
2469
|
-
: `git checkout --detach ${shellQuote(headState.commit)}`;
|
|
2470
|
-
const remoteCommand = [
|
|
2471
|
-
"set -euo pipefail",
|
|
2472
|
-
"unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE",
|
|
2473
|
-
`if [ -d ${shellQuote(expandedWorkdir)} ]; then`,
|
|
2474
|
-
parsed.force
|
|
2475
|
-
? ` mv ${shellQuote(expandedWorkdir)} ${shellQuote(backup)}`
|
|
2476
|
-
: ` echo "Target exists: ${expandedWorkdir}" >&2; exit 1`,
|
|
2477
|
-
"fi",
|
|
2478
|
-
`mkdir -p ${shellQuote(path.dirname(expandedWorkdir))}`,
|
|
2479
|
-
`git init -b devbox-init ${shellQuote(expandedWorkdir)}`,
|
|
2480
|
-
`cd ${shellQuote(expandedWorkdir)}`,
|
|
2481
|
-
`git fetch ${shellQuote(remoteBundlePath)} 'refs/*:refs/*'`,
|
|
2482
|
-
`if [ -f ${shellQuote(remoteGitMetaPath)} ]; then`,
|
|
2483
|
-
` tar -xzf ${shellQuote(remoteGitMetaPath)} -C .git`,
|
|
2484
|
-
"fi",
|
|
2485
|
-
checkoutCommand,
|
|
2486
|
-
`if [ -f ${shellQuote(remoteStagedPatchPath)} ]; then`,
|
|
2487
|
-
` git apply --index ${shellQuote(remoteStagedPatchPath)}`,
|
|
2488
|
-
"fi",
|
|
2489
|
-
`if [ -f ${shellQuote(remoteUnstagedPatchPath)} ]; then`,
|
|
2490
|
-
` git apply ${shellQuote(remoteUnstagedPatchPath)}`,
|
|
2491
|
-
"fi",
|
|
2492
|
-
`if [ -f ${shellQuote(remoteUntrackedPath)} ]; then`,
|
|
2493
|
-
` tar -xzf ${shellQuote(remoteUntrackedPath)} -C .`,
|
|
2494
|
-
"fi",
|
|
2495
|
-
].join("\n");
|
|
2496
|
-
const execResult = await client.exec(canonical, [
|
|
2497
|
-
"/bin/bash",
|
|
2498
|
-
"--noprofile",
|
|
2499
|
-
"--norc",
|
|
2500
|
-
"-e",
|
|
2501
|
-
"-u",
|
|
2502
|
-
"-o",
|
|
2503
|
-
"pipefail",
|
|
2504
|
-
"-c",
|
|
2505
|
-
remoteCommand,
|
|
2506
|
-
]);
|
|
2507
|
-
if (execResult.exitCode !== 0) {
|
|
2508
|
-
throw new Error(execResult.stderr || "Remote init failed");
|
|
2509
|
-
}
|
|
2510
|
-
await updateInitState({ steps: { workdirProvisioned: true } });
|
|
2511
|
-
},
|
|
2512
|
-
});
|
|
2513
|
-
if (!skipSetupUpload) {
|
|
2514
|
-
await runInitStep({
|
|
2515
|
-
enabled: progressEnabled,
|
|
2516
|
-
title: "Uploading setup plan",
|
|
2517
|
-
fn: async ({ status }) => {
|
|
2518
|
-
await uploadSetupPlan({
|
|
2519
|
-
client,
|
|
2520
|
-
canonical,
|
|
2521
|
-
localSetupPath: setupPath,
|
|
2522
|
-
remoteSetupPath,
|
|
2523
|
-
localArtifactsBundlePath: setupArtifacts?.bundlePath ?? null,
|
|
2524
|
-
localArtifactsManifestPath: setupArtifacts?.manifestPath ?? null,
|
|
2525
|
-
localArtifactsPartsDescriptorPath: setupArtifacts?.partsDescriptorPath ?? null,
|
|
2526
|
-
remoteArtifactsBundlePath: setupArtifacts
|
|
2527
|
-
? path.posix.join(expandedWorkdir, ".devbox", "setup-artifacts.tgz")
|
|
2528
|
-
: null,
|
|
2529
|
-
remoteArtifactsManifestPath: setupArtifacts
|
|
2530
|
-
? path.posix.join(expandedWorkdir, ".devbox", "setup-artifacts.json")
|
|
2531
|
-
: null,
|
|
2532
|
-
remoteArtifactsPartsDescriptorPath: setupArtifacts
|
|
2533
|
-
? remoteArtifactsPartsDescriptorPath
|
|
2534
|
-
: null,
|
|
2535
|
-
status,
|
|
2536
|
-
});
|
|
2537
|
-
},
|
|
2538
|
-
});
|
|
2539
|
-
await updateInitState({ steps: { setupUploaded: true } });
|
|
2540
|
-
await updateRegistryProjectStatus(resolveInitStatus(initState?.steps, initState?.complete));
|
|
2541
|
-
}
|
|
2542
|
-
}
|
|
2543
|
-
finally {
|
|
2544
|
-
await fs.rm(tempDir, { recursive: true, force: true });
|
|
2545
|
-
}
|
|
2546
|
-
}
|
|
2547
|
-
await runInitStep({
|
|
2548
|
-
enabled: progressEnabled,
|
|
2549
|
-
title: "Ensuring workdir ownership",
|
|
2550
|
-
fn: async ({ status }) => {
|
|
2551
|
-
await retryInitStep({
|
|
2552
|
-
status,
|
|
2553
|
-
title: "Ensuring workdir ownership",
|
|
2554
|
-
fn: async () => await ensureWorkdirOwnership({
|
|
2555
|
-
client,
|
|
2556
|
-
canonical,
|
|
2557
|
-
workdir: expandedWorkdir,
|
|
2558
|
-
status,
|
|
2559
|
-
}),
|
|
2560
|
-
});
|
|
2561
|
-
},
|
|
2562
|
-
});
|
|
2563
|
-
const skipGitSafeDirectory = shouldResume && initState?.steps.gitSafeDirectoryConfigured;
|
|
2564
|
-
if (!skipGitSafeDirectory) {
|
|
2565
|
-
await runInitStep({
|
|
2566
|
-
enabled: progressEnabled,
|
|
2567
|
-
title: "Configuring git safe.directory",
|
|
2568
|
-
fn: async ({ status }) => {
|
|
2569
|
-
await ensureGitSafeDirectory({
|
|
2570
|
-
client,
|
|
2571
|
-
canonical,
|
|
2572
|
-
workdir: expandedWorkdir,
|
|
2573
|
-
status,
|
|
2574
|
-
});
|
|
2575
|
-
await updateInitState({
|
|
2576
|
-
steps: { gitSafeDirectoryConfigured: true },
|
|
2577
|
-
});
|
|
2578
|
-
},
|
|
2579
|
-
});
|
|
2580
|
-
}
|
|
2581
734
|
const skipSshdConfig = shouldResume && initState?.steps.sshdConfigured;
|
|
2582
735
|
if (!skipSshdConfig) {
|
|
2583
736
|
await runInitStep({
|
|
@@ -2624,143 +777,6 @@ export const runInit = async (args) => {
|
|
|
2624
777
|
},
|
|
2625
778
|
});
|
|
2626
779
|
}
|
|
2627
|
-
const skipSshAuth = nonInteractive || (shouldResume && initState?.steps.sshAuthConfigured);
|
|
2628
|
-
if (!skipSshAuth) {
|
|
2629
|
-
const { remoteOrigin, remoteInfo } = await runInitStep({
|
|
2630
|
-
enabled: progressEnabled,
|
|
2631
|
-
title: "Checking git remote for SSH auth",
|
|
2632
|
-
fn: async () => {
|
|
2633
|
-
const remoteOrigin = await readRemoteOrigin(client, canonical, expandedWorkdir);
|
|
2634
|
-
const remoteInfo = remoteOrigin ? parseGitRemote(remoteOrigin) : null;
|
|
2635
|
-
return { remoteOrigin, remoteInfo };
|
|
2636
|
-
},
|
|
2637
|
-
});
|
|
2638
|
-
if (!remoteOrigin) {
|
|
2639
|
-
if (!parsed.json) {
|
|
2640
|
-
console.warn("Warning: unable to detect remote origin on sprite. Skipping SSH setup.");
|
|
2641
|
-
}
|
|
2642
|
-
}
|
|
2643
|
-
else if (!remoteInfo) {
|
|
2644
|
-
if (!parsed.json) {
|
|
2645
|
-
console.warn(`Warning: unrecognized git remote format (${remoteOrigin}). Skipping SSH setup.`);
|
|
2646
|
-
}
|
|
2647
|
-
}
|
|
2648
|
-
else {
|
|
2649
|
-
let activeOrigin = remoteOrigin;
|
|
2650
|
-
if (remoteInfo.protocol === "https") {
|
|
2651
|
-
if (!parsed.json) {
|
|
2652
|
-
clackNote([
|
|
2653
|
-
`Origin is HTTPS (${remoteOrigin}).`,
|
|
2654
|
-
"",
|
|
2655
|
-
"This only changes the remote devbox checkout (your local checkout is unchanged).",
|
|
2656
|
-
"SSH key auth on the remote devbox will not work unless this remote uses SSH:",
|
|
2657
|
-
remoteInfo.sshUrl,
|
|
2658
|
-
].join("\n"), "Git remote");
|
|
2659
|
-
}
|
|
2660
|
-
const shouldSwitch = await clackSelect({
|
|
2661
|
-
message: `Use SSH auth for ${remoteInfo.host} on the remote devbox checkout?`,
|
|
2662
|
-
options: [
|
|
2663
|
-
{
|
|
2664
|
-
value: "switch",
|
|
2665
|
-
label: "Use SSH on remote devbox (Recommended)",
|
|
2666
|
-
},
|
|
2667
|
-
{ value: "keep", label: "Keep HTTPS (Skip SSH auth setup)" },
|
|
2668
|
-
{ value: "cancel", label: "Cancel init" },
|
|
2669
|
-
],
|
|
2670
|
-
initialValue: "switch",
|
|
2671
|
-
});
|
|
2672
|
-
if (isCancel(shouldSwitch) || shouldSwitch === "cancel") {
|
|
2673
|
-
throwInitCanceled();
|
|
2674
|
-
}
|
|
2675
|
-
if (shouldSwitch === "keep") {
|
|
2676
|
-
if (!parsed.json) {
|
|
2677
|
-
console.warn("Skipping SSH auth setup. Configure git credentials for this repo manually before pulling or pushing.");
|
|
2678
|
-
}
|
|
2679
|
-
await updateInitState({
|
|
2680
|
-
steps: { sshAuthConfigured: true },
|
|
2681
|
-
});
|
|
2682
|
-
activeOrigin = "";
|
|
2683
|
-
}
|
|
2684
|
-
else {
|
|
2685
|
-
await runInitStep({
|
|
2686
|
-
enabled: progressEnabled,
|
|
2687
|
-
title: "Updating remote devbox checkout to SSH git remote",
|
|
2688
|
-
fn: async () => {
|
|
2689
|
-
await setRemoteOrigin(client, canonical, expandedWorkdir, remoteInfo.sshUrl);
|
|
2690
|
-
},
|
|
2691
|
-
});
|
|
2692
|
-
activeOrigin = remoteInfo.sshUrl;
|
|
2693
|
-
}
|
|
2694
|
-
}
|
|
2695
|
-
if (activeOrigin) {
|
|
2696
|
-
const publicKey = await runInitStep({
|
|
2697
|
-
enabled: progressEnabled,
|
|
2698
|
-
title: "Generating SSH key",
|
|
2699
|
-
fn: async () => await ensureSshKey(client, canonical, `${alias}@devbox`),
|
|
2700
|
-
});
|
|
2701
|
-
await runInitStep({
|
|
2702
|
-
enabled: progressEnabled,
|
|
2703
|
-
title: "Updating SSH config",
|
|
2704
|
-
fn: async () => await ensureSshConfig(client, canonical, remoteInfo.host),
|
|
2705
|
-
});
|
|
2706
|
-
if (!parsed.json) {
|
|
2707
|
-
clackNote(publicKey, `Add this SSH public key to ${remoteInfo.host}`);
|
|
2708
|
-
const copied = await copyToClipboard(publicKey);
|
|
2709
|
-
if (copied) {
|
|
2710
|
-
clackLog.success("Copied SSH public key to clipboard.");
|
|
2711
|
-
}
|
|
2712
|
-
else {
|
|
2713
|
-
clackLog.warn("Could not copy the SSH key automatically.");
|
|
2714
|
-
}
|
|
2715
|
-
const shouldOpen = await promptBeforeOpenBrowser({
|
|
2716
|
-
url: remoteInfo.settingsUrl,
|
|
2717
|
-
title: `${remoteInfo.host} SSH key page`,
|
|
2718
|
-
consequence: [
|
|
2719
|
-
"Skipping browser open.",
|
|
2720
|
-
"You must add the SSH key before git pull/push will work via SSH.",
|
|
2721
|
-
].join(" "),
|
|
2722
|
-
});
|
|
2723
|
-
if (shouldOpen) {
|
|
2724
|
-
const opened = openBrowser(remoteInfo.settingsUrl);
|
|
2725
|
-
if (!opened) {
|
|
2726
|
-
clackLog.warn("Unable to open the browser automatically.");
|
|
2727
|
-
}
|
|
2728
|
-
}
|
|
2729
|
-
}
|
|
2730
|
-
const added = await clackConfirm({
|
|
2731
|
-
message: "Have you added the SSH key?",
|
|
2732
|
-
active: "Yes, verify now",
|
|
2733
|
-
inactive: "Not yet",
|
|
2734
|
-
initialValue: true,
|
|
2735
|
-
});
|
|
2736
|
-
if (isCancel(added)) {
|
|
2737
|
-
throwInitCanceled();
|
|
2738
|
-
}
|
|
2739
|
-
if (!added) {
|
|
2740
|
-
if (!parsed.json) {
|
|
2741
|
-
console.warn("Skipping SSH verification. Add the key and re-run `dvb init --resume` to verify.");
|
|
2742
|
-
}
|
|
2743
|
-
}
|
|
2744
|
-
else {
|
|
2745
|
-
const verified = await runInitStep({
|
|
2746
|
-
enabled: progressEnabled,
|
|
2747
|
-
title: "Verifying git SSH auth",
|
|
2748
|
-
fn: async () => await verifySshAuth(client, canonical, remoteInfo.host, expandedWorkdir),
|
|
2749
|
-
});
|
|
2750
|
-
if (!verified) {
|
|
2751
|
-
if (!parsed.json) {
|
|
2752
|
-
console.warn("SSH auth verification failed. Confirm the key is added and that the repo access is granted.");
|
|
2753
|
-
}
|
|
2754
|
-
}
|
|
2755
|
-
else {
|
|
2756
|
-
await updateInitState({
|
|
2757
|
-
steps: { sshAuthConfigured: true },
|
|
2758
|
-
});
|
|
2759
|
-
}
|
|
2760
|
-
}
|
|
2761
|
-
}
|
|
2762
|
-
}
|
|
2763
|
-
}
|
|
2764
780
|
const weztermMuxPresent = await runInitStep({
|
|
2765
781
|
enabled: progressEnabled,
|
|
2766
782
|
title: "Ensuring WezTerm mux server (optional)",
|
|
@@ -2839,88 +855,6 @@ export const runInit = async (args) => {
|
|
|
2839
855
|
},
|
|
2840
856
|
});
|
|
2841
857
|
}
|
|
2842
|
-
if (!skipServicesConfig) {
|
|
2843
|
-
await runInitStep({
|
|
2844
|
-
enabled: progressEnabled,
|
|
2845
|
-
title: "Writing devbox.toml services",
|
|
2846
|
-
fn: async () => {
|
|
2847
|
-
await writeRemoteServicesToml({
|
|
2848
|
-
client,
|
|
2849
|
-
canonical,
|
|
2850
|
-
workdir: expandedWorkdir,
|
|
2851
|
-
services: finalApprovedPlan.services.backgroundServices,
|
|
2852
|
-
});
|
|
2853
|
-
await updateInitState({ steps: { servicesConfigWritten: true } });
|
|
2854
|
-
},
|
|
2855
|
-
});
|
|
2856
|
-
}
|
|
2857
|
-
const projectMeta = {
|
|
2858
|
-
fingerprint,
|
|
2859
|
-
canonical,
|
|
2860
|
-
alias,
|
|
2861
|
-
workdir,
|
|
2862
|
-
origin: projectOrigin,
|
|
2863
|
-
createdAt: projectCreatedAt,
|
|
2864
|
-
};
|
|
2865
|
-
const projectMetaPath = `/home/sprite/.devbox/projects/${fingerprint}.json`;
|
|
2866
|
-
await runInitStep({
|
|
2867
|
-
enabled: progressEnabled,
|
|
2868
|
-
title: "Registering project",
|
|
2869
|
-
fn: async () => {
|
|
2870
|
-
let existing = {};
|
|
2871
|
-
try {
|
|
2872
|
-
const raw = await client.readFile(canonical, {
|
|
2873
|
-
path: projectMetaPath,
|
|
2874
|
-
});
|
|
2875
|
-
const parsed = JSON.parse(Buffer.from(raw).toString("utf8"));
|
|
2876
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2877
|
-
existing = parsed;
|
|
2878
|
-
}
|
|
2879
|
-
}
|
|
2880
|
-
catch (error) {
|
|
2881
|
-
if (!(error instanceof SpritesApiError && error.status === 404)) {
|
|
2882
|
-
throw error;
|
|
2883
|
-
}
|
|
2884
|
-
}
|
|
2885
|
-
const existingCheckpoints = existing.checkpoints &&
|
|
2886
|
-
typeof existing.checkpoints === "object" &&
|
|
2887
|
-
!Array.isArray(existing.checkpoints)
|
|
2888
|
-
? existing.checkpoints
|
|
2889
|
-
: {};
|
|
2890
|
-
const stateCheckpoints = initState?.checkpoints ?? {};
|
|
2891
|
-
const merged = {
|
|
2892
|
-
...existing,
|
|
2893
|
-
...projectMeta,
|
|
2894
|
-
checkpoints: { ...existingCheckpoints, ...stateCheckpoints },
|
|
2895
|
-
};
|
|
2896
|
-
await client.writeFile(canonical, projectMetaPath, Buffer.from(JSON.stringify(merged, null, 2)));
|
|
2897
|
-
},
|
|
2898
|
-
});
|
|
2899
|
-
await runInitStep({
|
|
2900
|
-
enabled: progressEnabled,
|
|
2901
|
-
title: "Writing local metadata",
|
|
2902
|
-
fn: async () => {
|
|
2903
|
-
await writeRepoMarker(projectDir, { fingerprint, canonical, alias });
|
|
2904
|
-
},
|
|
2905
|
-
});
|
|
2906
|
-
const skipSetupArtifactsStage = skipCodexApply || (shouldResume && initState?.steps.setupArtifactsStaged);
|
|
2907
|
-
if (!skipSetupArtifactsStage) {
|
|
2908
|
-
await runInitStep({
|
|
2909
|
-
enabled: progressEnabled,
|
|
2910
|
-
title: "Staging setup artifacts",
|
|
2911
|
-
fn: async ({ status }) => {
|
|
2912
|
-
status.stage("Copying repo artifacts and staging external files");
|
|
2913
|
-
await stageRemoteSetupArtifacts({
|
|
2914
|
-
client,
|
|
2915
|
-
canonical,
|
|
2916
|
-
workdir: expandedWorkdir,
|
|
2917
|
-
artifactsBundlePath: remoteArtifactsBundlePath,
|
|
2918
|
-
artifactsManifestPath: remoteArtifactsManifestPath,
|
|
2919
|
-
});
|
|
2920
|
-
},
|
|
2921
|
-
});
|
|
2922
|
-
await updateInitState({ steps: { setupArtifactsStaged: true } });
|
|
2923
|
-
}
|
|
2924
858
|
if (!skipCodexCliEnsure) {
|
|
2925
859
|
await runInitStep({
|
|
2926
860
|
enabled: progressEnabled,
|
|
@@ -2946,138 +880,70 @@ export const runInit = async (args) => {
|
|
|
2946
880
|
},
|
|
2947
881
|
});
|
|
2948
882
|
}
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
? { proxyOptions: initCodexProxyOptions }
|
|
3013
|
-
: {}),
|
|
3014
|
-
});
|
|
3015
|
-
},
|
|
3016
|
-
});
|
|
3017
|
-
await updateInitState({ steps: { codexApplied: true } });
|
|
3018
|
-
await updateRegistryProjectStatus(resolveInitStatus(initState?.steps, initState?.complete));
|
|
3019
|
-
await runInitStep({
|
|
3020
|
-
enabled: progressEnabled,
|
|
3021
|
-
title: "Snapshotting filesystem (post-setup)",
|
|
3022
|
-
fn: async ({ status, fail, ok }) => {
|
|
3023
|
-
try {
|
|
3024
|
-
const checkpoint = await retryInitStep({
|
|
3025
|
-
status,
|
|
3026
|
-
title: "Snapshotting filesystem (post-setup)",
|
|
3027
|
-
fn: async () => await recordCodexCheckpoint({
|
|
3028
|
-
client,
|
|
3029
|
-
canonical,
|
|
3030
|
-
phase: "postCodexSetup",
|
|
3031
|
-
}),
|
|
3032
|
-
});
|
|
3033
|
-
if (checkpoint.id) {
|
|
3034
|
-
ok(`Snapshot created: ${checkpoint.id}`);
|
|
3035
|
-
}
|
|
3036
|
-
}
|
|
3037
|
-
catch (error) {
|
|
3038
|
-
logger.warn("init_checkpoint_create_failed", {
|
|
3039
|
-
box: canonical,
|
|
3040
|
-
fingerprint,
|
|
3041
|
-
phase: "post-codex-setup",
|
|
3042
|
-
error: error instanceof Error ? error.message : String(error),
|
|
3043
|
-
});
|
|
3044
|
-
fail("Snapshotting filesystem (post-setup) (failed)");
|
|
3045
|
-
throw error;
|
|
3046
|
-
}
|
|
3047
|
-
},
|
|
3048
|
-
});
|
|
3049
|
-
}
|
|
3050
|
-
const finalStatus = resolveInitStatus(initState?.steps, initState?.complete);
|
|
3051
|
-
const isComplete = finalStatus === "complete";
|
|
3052
|
-
await runInitStep({
|
|
3053
|
-
enabled: progressEnabled,
|
|
3054
|
-
title: "Finalizing init",
|
|
3055
|
-
fn: async () => {
|
|
3056
|
-
await updateInitState({ complete: isComplete });
|
|
3057
|
-
await updateRegistryProjectStatus(finalStatus);
|
|
3058
|
-
},
|
|
883
|
+
const { setupPath, finalApprovedPlan, setupArtifacts, skipServicesConfig, skipServicesEnable, skipSetupUpload, } = await runSetupPlanFlow({
|
|
884
|
+
projectDir,
|
|
885
|
+
repoRoot,
|
|
886
|
+
localHomeDir,
|
|
887
|
+
shouldResume,
|
|
888
|
+
nonInteractive,
|
|
889
|
+
progressEnabled,
|
|
890
|
+
parsed,
|
|
891
|
+
getInitState,
|
|
892
|
+
updateInitState,
|
|
893
|
+
...(initCodexProxyOptions ? { initCodexProxyOptions } : {}),
|
|
894
|
+
throwInitCanceled,
|
|
895
|
+
});
|
|
896
|
+
await runProvisionFlow({
|
|
897
|
+
client,
|
|
898
|
+
canonical,
|
|
899
|
+
expandedWorkdir,
|
|
900
|
+
repoRoot,
|
|
901
|
+
localHomeDir,
|
|
902
|
+
shouldResume,
|
|
903
|
+
parsed,
|
|
904
|
+
progressEnabled,
|
|
905
|
+
setupPath,
|
|
906
|
+
remoteSetupPath,
|
|
907
|
+
setupArtifacts,
|
|
908
|
+
skipSetupUpload,
|
|
909
|
+
remoteArtifactsBundlePath,
|
|
910
|
+
remoteArtifactsManifestPath,
|
|
911
|
+
remoteArtifactsPartsDescriptorPath,
|
|
912
|
+
getInitState,
|
|
913
|
+
updateInitState,
|
|
914
|
+
refreshRegistryProjectStatus: async () => await updateRegistryProjectStatus(resolveInitStatus(initState?.steps, initState?.complete)),
|
|
915
|
+
});
|
|
916
|
+
await runFinalizeFlow({
|
|
917
|
+
client,
|
|
918
|
+
canonical,
|
|
919
|
+
alias,
|
|
920
|
+
workdir,
|
|
921
|
+
projectDir,
|
|
922
|
+
fingerprint,
|
|
923
|
+
projectCreatedAt,
|
|
924
|
+
...(projectOrigin !== undefined ? { projectOrigin } : {}),
|
|
925
|
+
finalApprovedPlan,
|
|
926
|
+
shouldResume,
|
|
927
|
+
nonInteractive,
|
|
928
|
+
progressEnabled,
|
|
929
|
+
parsed,
|
|
930
|
+
skipCodexApply,
|
|
931
|
+
skipServicesConfig,
|
|
932
|
+
skipServicesEnable,
|
|
933
|
+
remoteSetupPath,
|
|
934
|
+
remoteArtifactsBundlePath,
|
|
935
|
+
remoteArtifactsManifestPath,
|
|
936
|
+
socketInfo,
|
|
937
|
+
pathSetup,
|
|
938
|
+
...(initCodexProxyOptions ? { initCodexProxyOptions } : {}),
|
|
939
|
+
throwInitCanceled,
|
|
940
|
+
getInitState,
|
|
941
|
+
updateInitState,
|
|
942
|
+
updateRegistryProjectStatus,
|
|
943
|
+
resolveInitStatus,
|
|
944
|
+
retryInitStep,
|
|
945
|
+
recordCodexCheckpoint,
|
|
3059
946
|
});
|
|
3060
|
-
if (parsed.json) {
|
|
3061
|
-
console.log(JSON.stringify({
|
|
3062
|
-
ok: true,
|
|
3063
|
-
status: finalStatus,
|
|
3064
|
-
canonical,
|
|
3065
|
-
alias,
|
|
3066
|
-
workdir,
|
|
3067
|
-
fingerprint,
|
|
3068
|
-
}, null, 2));
|
|
3069
|
-
return;
|
|
3070
|
-
}
|
|
3071
|
-
if (!isComplete) {
|
|
3072
|
-
console.log(`devbox initialized (setup incomplete): ${alias} -> ${canonical}`);
|
|
3073
|
-
console.log(`workdir: ${workdir}`);
|
|
3074
|
-
console.log("next: run `dvb init --resume` from this repo to finish setup");
|
|
3075
|
-
console.log("sprites: synced to control plane");
|
|
3076
|
-
return;
|
|
3077
|
-
}
|
|
3078
|
-
console.log(`devbox initialized: ${alias} -> ${canonical}`);
|
|
3079
|
-
console.log(`workdir: ${workdir}`);
|
|
3080
|
-
console.log("sprites: synced to control plane");
|
|
3081
947
|
};
|
|
3082
948
|
await run();
|
|
3083
949
|
};
|