@boxes-dev/dvb 0.2.4 → 0.2.5
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 +5224 -1568
- package/dist/bin/dvbd.cjs +73 -2
- package/dist/codex/services-schema.json +4 -0
- package/dist/codex/setup-env-secrets-schema.json +50 -0
- package/dist/codex/setup-external-schema.json +35 -0
- package/dist/codex/setup-extra-artifacts-schema.json +22 -0
- 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/agent.js +2 -2
- package/dist/devbox/commands/agent.js.map +1 -1
- package/dist/devbox/commands/init/args.d.ts +1 -0
- package/dist/devbox/commands/init/args.d.ts.map +1 -1
- package/dist/devbox/commands/init/args.js +4 -0
- package/dist/devbox/commands/init/args.js.map +1 -1
- package/dist/devbox/commands/init/clack.d.ts +22 -0
- package/dist/devbox/commands/init/clack.d.ts.map +1 -0
- package/dist/devbox/commands/init/clack.js +152 -0
- package/dist/devbox/commands/init/clack.js.map +1 -0
- package/dist/devbox/commands/init/codex/artifacts.d.ts.map +1 -1
- package/dist/devbox/commands/init/codex/artifacts.js +2 -1
- package/dist/devbox/commands/init/codex/artifacts.js.map +1 -1
- package/dist/devbox/commands/init/codex/index.d.ts +6 -6
- package/dist/devbox/commands/init/codex/index.d.ts.map +1 -1
- package/dist/devbox/commands/init/codex/index.js +118 -11
- package/dist/devbox/commands/init/codex/index.js.map +1 -1
- package/dist/devbox/commands/init/codex/local.d.ts +23 -4
- package/dist/devbox/commands/init/codex/local.d.ts.map +1 -1
- package/dist/devbox/commands/init/codex/local.js +344 -142
- package/dist/devbox/commands/init/codex/local.js.map +1 -1
- package/dist/devbox/commands/init/codex/plan.d.ts +31 -3
- package/dist/devbox/commands/init/codex/plan.d.ts.map +1 -1
- package/dist/devbox/commands/init/codex/plan.js +132 -6
- package/dist/devbox/commands/init/codex/plan.js.map +1 -1
- package/dist/devbox/commands/init/codex/prompts.d.ts +4 -2
- package/dist/devbox/commands/init/codex/prompts.d.ts.map +1 -1
- package/dist/devbox/commands/init/codex/prompts.js +4 -2
- package/dist/devbox/commands/init/codex/prompts.js.map +1 -1
- package/dist/devbox/commands/init/codex/remote.d.ts +2 -2
- package/dist/devbox/commands/init/codex/remote.d.ts.map +1 -1
- package/dist/devbox/commands/init/codex/remote.js +116 -11
- package/dist/devbox/commands/init/codex/remote.js.map +1 -1
- package/dist/devbox/commands/init/index.d.ts.map +1 -1
- package/dist/devbox/commands/init/index.js +1957 -658
- package/dist/devbox/commands/init/index.js.map +1 -1
- package/dist/devbox/commands/init/packaging.d.ts.map +1 -1
- package/dist/devbox/commands/init/packaging.js +4 -9
- package/dist/devbox/commands/init/packaging.js.map +1 -1
- package/dist/devbox/commands/init/progress.d.ts +17 -0
- package/dist/devbox/commands/init/progress.d.ts.map +1 -0
- package/dist/devbox/commands/init/progress.js +68 -0
- package/dist/devbox/commands/init/progress.js.map +1 -0
- package/dist/devbox/commands/init/registry.d.ts.map +1 -1
- package/dist/devbox/commands/init/registry.js +0 -6
- package/dist/devbox/commands/init/registry.js.map +1 -1
- package/dist/devbox/commands/init/remote.d.ts +13 -2
- package/dist/devbox/commands/init/remote.d.ts.map +1 -1
- package/dist/devbox/commands/init/remote.js +231 -80
- package/dist/devbox/commands/init/remote.js.map +1 -1
- package/dist/devbox/commands/init/repo.d.ts +5 -2
- package/dist/devbox/commands/init/repo.d.ts.map +1 -1
- package/dist/devbox/commands/init/repo.js +28 -12
- package/dist/devbox/commands/init/repo.js.map +1 -1
- package/dist/devbox/commands/init/state.d.ts +22 -1
- package/dist/devbox/commands/init/state.d.ts.map +1 -1
- package/dist/devbox/commands/init/state.js +25 -1
- package/dist/devbox/commands/init/state.js.map +1 -1
- package/dist/devbox/commands/mountSsh.d.ts.map +1 -1
- package/dist/devbox/commands/mountSsh.js +37 -4
- package/dist/devbox/commands/mountSsh.js.map +1 -1
- package/dist/devbox/commands/wezterm.d.ts.map +1 -1
- package/dist/devbox/commands/wezterm.js +18 -132
- package/dist/devbox/commands/wezterm.js.map +1 -1
- package/dist/devbox/completions/index.d.ts.map +1 -1
- package/dist/devbox/completions/index.js +1 -0
- package/dist/devbox/completions/index.js.map +1 -1
- package/dist/prompts/local-scan-env-secrets.md +51 -0
- package/dist/prompts/local-scan-external.md +36 -0
- package/dist/prompts/local-scan-extra-artifacts.md +26 -0
- package/dist/prompts/local-services-scan.md +66 -5
- package/dist/prompts/remote-apply.md +17 -9
- package/dist/wezterm/ensureMux.d.ts +18 -0
- package/dist/wezterm/ensureMux.d.ts.map +1 -0
- package/dist/wezterm/ensureMux.js +74 -0
- package/dist/wezterm/ensureMux.js.map +1 -0
- package/dist/wezterm/installOptions.d.ts +18 -0
- package/dist/wezterm/installOptions.d.ts.map +1 -0
- package/dist/wezterm/installOptions.js +129 -0
- package/dist/wezterm/installOptions.js.map +1 -0
- package/package.json +3 -1
- package/dist/codex/setup-schema.json +0 -176
- package/dist/prompts/local-scan.md +0 -32
|
@@ -2,24 +2,25 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
|
-
import
|
|
5
|
+
import { cancel as clackCancel, confirm as clackConfirm, isCancel, log as clackLog, note as clackNote, select as clackSelect, selectKey as clackSelectKey, taskLog as clackTaskLog, } from "@clack/prompts";
|
|
6
6
|
import { resolveSocketInfo } from "@boxes-dev/core";
|
|
7
7
|
import { createSecretStore, loadConfig, createSpritesClient, fingerprintFromOrigin, fingerprintFromRootCommit, normalizeGitRemoteUrl, resolveSpritesApiUrl, SpritesApiError, slugify, } from "@boxes-dev/core";
|
|
8
8
|
import { DAEMON_TIMEOUT_MS, ensureDaemonRunning, requestJson, requireDaemonFeatures, } from "../../daemonClient.js";
|
|
9
9
|
import { ensureSpritesToken } from "../../auth.js";
|
|
10
10
|
import { fetchSpriteDaemonRelease, getConvexUrl, issueSpriteDaemonToken, } from "../../controlPlane.js";
|
|
11
11
|
import { logger } from "../../logger.js";
|
|
12
|
-
import { createStatusLine } from "../../ui/statusLine.js";
|
|
13
12
|
import { parseInitArgs } from "./args.js";
|
|
14
13
|
import { confirmCopyWorktree, findRepoRoot, mapGlobalGitConfigDestinations, readGlobalGitConfigFiles, readHeadState, readNullSeparatedPaths, readRepoOrigin, readRootCommit, readWorktreeState, resolveGitCommonDir, runCommand, } from "./repo.js";
|
|
15
14
|
import { createFileListArchive, createGitMetaArchive, writePatch } from "./packaging.js";
|
|
16
|
-
import { bootstrapDevbox, ensureSpriteDaemonService, expandHome, ensureWeztermMuxService, installSpriteDaemon,
|
|
15
|
+
import { bootstrapDevbox, ensureSpriteDaemonService, expandHome, ensureWeztermMuxService, installSpriteDaemon, shellQuote, stageRemoteSetupArtifacts, writeRemoteCodexConfig, } from "./remote.js";
|
|
16
|
+
import { ensureWeztermMuxInstalled } from "../../../wezterm/ensureMux.js";
|
|
17
17
|
import { ensureDevboxToml, readRepoMarker, writeRepoMarker } from "./registry.js";
|
|
18
|
-
import { readInitState, writeInitState } from "./state.js";
|
|
18
|
+
import { INIT_STEP_KEYS, readInitState, writeInitState } from "./state.js";
|
|
19
19
|
import { ensureSshConfig, ensureSshKey, copyToClipboard, openBrowser, parseGitRemote, readRemoteOrigin, setRemoteOrigin, verifySshAuth, } from "./ssh.js";
|
|
20
20
|
import { ensureKnownHostsFile, ensureLocalMountKey, ensureRemoteMountAccess, ensureSshdService, readLocalMountPublicKey, } from "../mountSsh.js";
|
|
21
|
-
import { createSetupArtifacts, promptForPlanApproval, promptForServicesApproval, readSetupPlan, readServicesPlan, runLocalServicesScan,
|
|
21
|
+
import { createSetupArtifacts, promptForPlanApproval, promptForServicesApproval, readSetupPlan, readSetupEnvSecretsPlan, readSetupExternalPlan, readSetupExtraArtifactsPlan, readServicesPlan, runLocalServicesScan, runLocalSetupEnvSecretsScan, runLocalSetupExternalScan, runLocalSetupExtraArtifactsScan, runRemoteCodexSetup, uploadSetupPlan, mergeSetupScans, writeSetupPlan, writeSetupEnvSecretsSchema, writeSetupExternalSchema, writeSetupExtraArtifactsSchema, writeServicesPlan, writeServicesSchema, } from "./codex/index.js";
|
|
22
22
|
import { mergeServicesToml, splitShellCommand, } from "../servicesToml.js";
|
|
23
|
+
import { runInitStep } from "./progress.js";
|
|
23
24
|
const resolveInitStatus = (steps, complete) => {
|
|
24
25
|
if (complete || steps?.codexApplied)
|
|
25
26
|
return "complete";
|
|
@@ -54,6 +55,32 @@ const buildServicesTomlUpdates = (services) => {
|
|
|
54
55
|
}
|
|
55
56
|
return updates;
|
|
56
57
|
};
|
|
58
|
+
const extractCheckpointId = (events) => {
|
|
59
|
+
const idPattern = /\bv\d+\b/;
|
|
60
|
+
const matchId = (value) => {
|
|
61
|
+
if (typeof value !== "string")
|
|
62
|
+
return null;
|
|
63
|
+
const match = value.match(idPattern);
|
|
64
|
+
return match?.[0] ?? null;
|
|
65
|
+
};
|
|
66
|
+
for (const event of events) {
|
|
67
|
+
const direct = event.id ??
|
|
68
|
+
event.checkpoint_id ??
|
|
69
|
+
event.checkpointId ??
|
|
70
|
+
event.checkpointID ??
|
|
71
|
+
event.checkpoint;
|
|
72
|
+
if (typeof direct === "string" && /^v\d+$/.test(direct)) {
|
|
73
|
+
return direct;
|
|
74
|
+
}
|
|
75
|
+
const candidates = [event.data, event.message, event.raw, event.error];
|
|
76
|
+
for (const candidate of candidates) {
|
|
77
|
+
const extracted = matchId(candidate);
|
|
78
|
+
if (extracted)
|
|
79
|
+
return extracted;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
};
|
|
57
84
|
const writeRemoteServicesToml = async ({ client, canonical, workdir, services, }) => {
|
|
58
85
|
if (services.length === 0)
|
|
59
86
|
return;
|
|
@@ -76,34 +103,67 @@ const writeRemoteServicesToml = async ({ client, canonical, workdir, services, }
|
|
|
76
103
|
const merged = mergeServicesToml(baseContent, updates);
|
|
77
104
|
await client.writeFile(canonical, path.posix.join(workdir.replace(/\/$/, ""), "devbox.toml"), Buffer.from(merged));
|
|
78
105
|
};
|
|
79
|
-
const
|
|
80
|
-
if (
|
|
81
|
-
return
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
input: process.stdin,
|
|
86
|
-
output: process.stdout,
|
|
106
|
+
const enableRemoteServices = async ({ client, canonical, services, status, }) => {
|
|
107
|
+
if (services.length === 0)
|
|
108
|
+
return;
|
|
109
|
+
logger.info("init_services_enable_start", {
|
|
110
|
+
box: canonical,
|
|
111
|
+
serviceCount: services.length,
|
|
87
112
|
});
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
113
|
+
for (const service of services) {
|
|
114
|
+
const name = service.name.trim();
|
|
115
|
+
if (!name) {
|
|
116
|
+
throw new Error("Service name is required in services.json.");
|
|
117
|
+
}
|
|
118
|
+
status?.stage(`Enabling service: ${name}`);
|
|
119
|
+
const parts = splitShellCommand(service.command ?? "");
|
|
120
|
+
const [cmd, ...args] = parts;
|
|
121
|
+
if (!cmd) {
|
|
122
|
+
throw new Error(`Service "${name}" is missing a command.`);
|
|
123
|
+
}
|
|
124
|
+
const input = {
|
|
125
|
+
cmd,
|
|
126
|
+
...(args.length > 0 ? { args } : {}),
|
|
127
|
+
...(service.httpPort !== null ? { httpPort: service.httpPort } : {}),
|
|
128
|
+
};
|
|
129
|
+
await client.createService(canonical, name, input);
|
|
130
|
+
}
|
|
131
|
+
logger.info("init_services_enable_complete", { box: canonical });
|
|
132
|
+
};
|
|
133
|
+
const throwInitCanceled = () => {
|
|
134
|
+
clackCancel("Init canceled.");
|
|
135
|
+
throw new Error("Init canceled.");
|
|
94
136
|
};
|
|
95
|
-
const
|
|
96
|
-
if (!process.stdin.isTTY)
|
|
137
|
+
const promptBeforeOpenBrowser = async ({ url, title, consequence, }) => {
|
|
138
|
+
if (!process.stdin.isTTY)
|
|
97
139
|
return false;
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
140
|
+
clackNote(url, title);
|
|
141
|
+
const choice = await clackSelectKey({
|
|
142
|
+
message: `Open ${title} in your browser?`,
|
|
143
|
+
options: [
|
|
144
|
+
{ value: "o", label: "Open in browser" },
|
|
145
|
+
{ value: "s", label: "Skip" },
|
|
146
|
+
],
|
|
147
|
+
initialValue: "o",
|
|
102
148
|
});
|
|
103
|
-
|
|
104
|
-
|
|
149
|
+
if (isCancel(choice)) {
|
|
150
|
+
throwInitCanceled();
|
|
151
|
+
}
|
|
152
|
+
if (choice === "s") {
|
|
153
|
+
clackLog.warn(consequence);
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
105
156
|
return true;
|
|
106
157
|
};
|
|
158
|
+
const toPosixPath = (value) => value.split(path.sep).join(path.posix.sep);
|
|
159
|
+
const toRepoRelativePath = (repoRoot, filePath) => {
|
|
160
|
+
const resolved = path.isAbsolute(filePath)
|
|
161
|
+
? filePath
|
|
162
|
+
: path.resolve(repoRoot, filePath);
|
|
163
|
+
const relative = path.relative(repoRoot, resolved) || filePath;
|
|
164
|
+
return toPosixPath(relative);
|
|
165
|
+
};
|
|
166
|
+
const isOutsideRepoPath = (relativePath) => relativePath === ".." || relativePath.startsWith(`..${path.posix.sep}`);
|
|
107
167
|
const ensureWorkdirOwnership = async ({ client, canonical, workdir, status, json, }) => {
|
|
108
168
|
const checkResult = await client.exec(canonical, [
|
|
109
169
|
"/bin/bash",
|
|
@@ -133,26 +193,273 @@ const ensureWorkdirOwnership = async ({ client, canonical, workdir, status, json
|
|
|
133
193
|
}
|
|
134
194
|
}
|
|
135
195
|
};
|
|
196
|
+
const ensureGitSafeDirectory = async ({ client, canonical, workdir, status, }) => {
|
|
197
|
+
status.stage("Configuring git safe.directory");
|
|
198
|
+
logger.info("init_git_safe_directory_configure_start", {
|
|
199
|
+
box: canonical,
|
|
200
|
+
workdir,
|
|
201
|
+
});
|
|
202
|
+
const script = [
|
|
203
|
+
"set -euo pipefail",
|
|
204
|
+
`repo=${shellQuote(workdir)}`,
|
|
205
|
+
'if [ ! -d "$repo" ]; then',
|
|
206
|
+
' echo "Missing repo workdir: $repo" >&2',
|
|
207
|
+
" exit 1",
|
|
208
|
+
"fi",
|
|
209
|
+
'if [ ! -d "$repo/.git" ]; then',
|
|
210
|
+
" exit 0",
|
|
211
|
+
"fi",
|
|
212
|
+
"if ! command -v git >/dev/null 2>&1; then",
|
|
213
|
+
" exit 0",
|
|
214
|
+
"fi",
|
|
215
|
+
"",
|
|
216
|
+
'ensure_safe_in_file() {',
|
|
217
|
+
' cfg="$1"',
|
|
218
|
+
' existing="$(git config --file "$cfg" --get-all safe.directory 2>/dev/null || true)"',
|
|
219
|
+
' if printf \'%s\\n\' "$existing" | grep -Fxq "$repo"; then',
|
|
220
|
+
" return 0",
|
|
221
|
+
" fi",
|
|
222
|
+
' git config --file "$cfg" --add safe.directory "$repo"',
|
|
223
|
+
"}",
|
|
224
|
+
"",
|
|
225
|
+
// Ensure the repo is trusted for the sprite user (most devbox flows).
|
|
226
|
+
'ensure_safe_in_file "/home/sprite/.gitconfig"',
|
|
227
|
+
// If we created/modified the file as root, best-effort fix the owner.
|
|
228
|
+
'if [ "$(id -u)" -eq 0 ]; then',
|
|
229
|
+
" chown sprite:sprite /home/sprite/.gitconfig >/dev/null 2>&1 || true",
|
|
230
|
+
"elif command -v sudo >/dev/null 2>&1; then",
|
|
231
|
+
" sudo -n chown sprite:sprite /home/sprite/.gitconfig >/dev/null 2>&1 || true",
|
|
232
|
+
"fi",
|
|
233
|
+
"",
|
|
234
|
+
// Best-effort: also trust the repo for root, in case tools run git as root.
|
|
235
|
+
'if [ "$(id -u)" -eq 0 ]; then',
|
|
236
|
+
' ensure_safe_in_file "/root/.gitconfig"',
|
|
237
|
+
"elif command -v sudo >/dev/null 2>&1; then",
|
|
238
|
+
' root_existing="$(sudo -n git config --file /root/.gitconfig --get-all safe.directory 2>/dev/null || true)"',
|
|
239
|
+
' if ! printf \'%s\\n\' "$root_existing" | grep -Fxq "$repo"; then',
|
|
240
|
+
' sudo -n git config --file /root/.gitconfig --add safe.directory "$repo" >/dev/null 2>&1 || true',
|
|
241
|
+
" fi",
|
|
242
|
+
"fi",
|
|
243
|
+
].join("\n");
|
|
244
|
+
const result = await client.exec(canonical, [
|
|
245
|
+
"/bin/bash",
|
|
246
|
+
"--noprofile",
|
|
247
|
+
"--norc",
|
|
248
|
+
"-e",
|
|
249
|
+
"-u",
|
|
250
|
+
"-o",
|
|
251
|
+
"pipefail",
|
|
252
|
+
"-c",
|
|
253
|
+
script,
|
|
254
|
+
]);
|
|
255
|
+
if (result.exitCode !== 0) {
|
|
256
|
+
const details = result.stderr || result.stdout || "";
|
|
257
|
+
throw new Error(details
|
|
258
|
+
? `Failed to configure git safe.directory: ${details.trim()}`
|
|
259
|
+
: `Failed to configure git safe.directory (exit ${result.exitCode})`);
|
|
260
|
+
}
|
|
261
|
+
logger.info("init_git_safe_directory_configure_complete", {
|
|
262
|
+
box: canonical,
|
|
263
|
+
workdir,
|
|
264
|
+
});
|
|
265
|
+
};
|
|
136
266
|
export const runInit = async (args) => {
|
|
137
267
|
const parsed = parseInitArgs(args);
|
|
138
|
-
const
|
|
139
|
-
const status = createStatusLine({ enabled: statusEnabled });
|
|
268
|
+
const progressEnabled = process.stdout.isTTY && !parsed.json;
|
|
140
269
|
const run = async () => {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
270
|
+
const detected = await runInitStep({
|
|
271
|
+
enabled: progressEnabled,
|
|
272
|
+
title: "Detecting repository",
|
|
273
|
+
fn: async () => {
|
|
274
|
+
const cwd = process.cwd();
|
|
275
|
+
const repoRoot = await findRepoRoot(cwd);
|
|
276
|
+
const repoName = path.basename(repoRoot);
|
|
277
|
+
const slug = slugify(repoName);
|
|
278
|
+
const localHomeDir = process.env.HOME ?? os.homedir();
|
|
279
|
+
// Recoverability contract:
|
|
280
|
+
// - Every init step must be idempotent and/or checkpointed in init-state.json.
|
|
281
|
+
// - `complete` must only be true when the setup is actually usable without
|
|
282
|
+
// additional `dvb init --resume` work.
|
|
283
|
+
// See: apps/cli/docs/INIT_RECOVERABILITY.md
|
|
284
|
+
const repoMarker = await readRepoMarker(repoRoot);
|
|
285
|
+
const origin = await readRepoOrigin(repoRoot);
|
|
286
|
+
const normalizedOrigin = origin ? normalizeGitRemoteUrl(origin) : null;
|
|
287
|
+
const fingerprint = origin
|
|
288
|
+
? fingerprintFromOrigin(origin)
|
|
289
|
+
: repoMarker?.fingerprint ??
|
|
290
|
+
fingerprintFromRootCommit(await readRootCommit(repoRoot), randomUUID());
|
|
291
|
+
let initState = await readInitState(repoRoot);
|
|
292
|
+
const initFingerprintMismatch = Boolean(initState?.fingerprint) &&
|
|
293
|
+
initState?.fingerprint !== fingerprint;
|
|
294
|
+
if (!initFingerprintMismatch &&
|
|
295
|
+
initState?.complete &&
|
|
296
|
+
!initState.steps.codexApplied) {
|
|
297
|
+
// Older versions could mark init "complete" even though Codex setup had not
|
|
298
|
+
// been applied yet. Treat this as incomplete so `dvb init --resume` can
|
|
299
|
+
// recover.
|
|
300
|
+
initState = { ...initState, complete: false };
|
|
301
|
+
await writeInitState(repoRoot, initState);
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
repoRoot,
|
|
305
|
+
repoName,
|
|
306
|
+
slug,
|
|
307
|
+
localHomeDir,
|
|
308
|
+
repoMarker,
|
|
309
|
+
origin,
|
|
310
|
+
normalizedOrigin,
|
|
311
|
+
fingerprint,
|
|
312
|
+
initState,
|
|
313
|
+
initFingerprintMismatch,
|
|
314
|
+
};
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
const { repoRoot, repoName, slug, localHomeDir, repoMarker, origin, normalizedOrigin, fingerprint, } = detected;
|
|
318
|
+
let initState = detected.initState;
|
|
319
|
+
const initFingerprintMismatch = detected.initFingerprintMismatch;
|
|
320
|
+
if (parsed.status) {
|
|
321
|
+
if (parsed.resume ||
|
|
322
|
+
parsed.force ||
|
|
323
|
+
parsed.codexSetupOnly ||
|
|
324
|
+
parsed.alias ||
|
|
325
|
+
parsed.name) {
|
|
326
|
+
throw new Error("`dvb init --status` cannot be combined with other init flags (except --json).");
|
|
327
|
+
}
|
|
328
|
+
const socketInfo = resolveSocketInfo();
|
|
329
|
+
let daemonError = null;
|
|
330
|
+
let registryProject = null;
|
|
331
|
+
try {
|
|
332
|
+
await ensureDaemonRunning(socketInfo.socketPath);
|
|
333
|
+
await requireDaemonFeatures(socketInfo.socketPath, ["registry"]);
|
|
334
|
+
const existingProject = await requestJson(socketInfo.socketPath, "GET", `/registry/project?fingerprint=${encodeURIComponent(fingerprint)}`, DAEMON_TIMEOUT_MS.registry);
|
|
335
|
+
registryProject = existingProject.body.project ?? null;
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
daemonError = error instanceof Error ? error.message : String(error);
|
|
339
|
+
}
|
|
340
|
+
const localStatus = initState && !initFingerprintMismatch
|
|
341
|
+
? resolveInitStatus(initState.steps, initState.complete)
|
|
342
|
+
: null;
|
|
343
|
+
const recommendsResume = Boolean(initState && !initFingerprintMismatch && !initState.complete);
|
|
344
|
+
const recommendsInit = !registryProject && (!initState || initFingerprintMismatch);
|
|
345
|
+
const recommendsForce = !recommendsResume &&
|
|
346
|
+
(!initState || initFingerprintMismatch) &&
|
|
347
|
+
Boolean(registryProject?.initStatus && registryProject.initStatus !== "complete");
|
|
348
|
+
const lines = [];
|
|
349
|
+
lines.push("INIT STATUS");
|
|
350
|
+
lines.push("");
|
|
351
|
+
lines.push("Repo");
|
|
352
|
+
lines.push(` root: ${repoRoot}`);
|
|
353
|
+
lines.push(` origin: ${normalizedOrigin ?? origin ?? "(none)"}`);
|
|
354
|
+
lines.push(` fingerprint: ${fingerprint}`);
|
|
355
|
+
lines.push("");
|
|
356
|
+
lines.push("Local state");
|
|
357
|
+
if (repoMarker?.canonical) {
|
|
358
|
+
lines.push(` box marker (.devbox/box.json): present (alias: ${repoMarker.alias ?? "(none)"}, box: ${repoMarker.canonical})`);
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
lines.push(" box marker (.devbox/box.json): missing");
|
|
362
|
+
}
|
|
363
|
+
if (initState) {
|
|
364
|
+
const completeText = initFingerprintMismatch
|
|
365
|
+
? `${String(Boolean(initState.complete))} (fingerprint mismatch)`
|
|
366
|
+
: String(Boolean(initState.complete));
|
|
367
|
+
lines.push(` init checkpoint (.devbox/init-state.json): present (updated: ${initState.updatedAt}, complete: ${completeText})`);
|
|
368
|
+
if (initState.canonical || initState.alias || initState.workdir) {
|
|
369
|
+
lines.push(` box: ${initState.canonical ?? "(unknown)"} (alias: ${initState.alias ?? "(unknown)"})`);
|
|
370
|
+
lines.push(` workdir: ${initState.workdir ?? "(unknown)"}`);
|
|
371
|
+
}
|
|
372
|
+
if (initState.checkpoints?.preCodexSetup || initState.checkpoints?.postCodexSetup) {
|
|
373
|
+
const pre = initState.checkpoints?.preCodexSetup;
|
|
374
|
+
const post = initState.checkpoints?.postCodexSetup;
|
|
375
|
+
lines.push(" snapshots:");
|
|
376
|
+
if (pre) {
|
|
377
|
+
lines.push(` pre-codex-setup: ${pre.id} (created: ${pre.createdAt})`);
|
|
378
|
+
}
|
|
379
|
+
if (post) {
|
|
380
|
+
lines.push(` post-codex-setup: ${post.id} (created: ${post.createdAt})`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (localStatus) {
|
|
384
|
+
lines.push(` inferred init status: ${localStatus}`);
|
|
385
|
+
}
|
|
386
|
+
lines.push(" steps:");
|
|
387
|
+
const steps = initState.steps ?? {};
|
|
388
|
+
for (const key of INIT_STEP_KEYS) {
|
|
389
|
+
lines.push(` ${key}: ${steps[key] ? "yes" : "no"}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
lines.push(" init checkpoint (.devbox/init-state.json): missing");
|
|
394
|
+
}
|
|
395
|
+
lines.push("");
|
|
396
|
+
lines.push("Registry");
|
|
397
|
+
lines.push(` daemon socket: ${socketInfo.socketPath}`);
|
|
398
|
+
if (daemonError) {
|
|
399
|
+
lines.push(` daemon: unavailable (${daemonError})`);
|
|
400
|
+
}
|
|
401
|
+
else if (registryProject) {
|
|
402
|
+
lines.push(" project: present");
|
|
403
|
+
lines.push(` box: ${registryProject.canonical}`);
|
|
404
|
+
lines.push(` alias: ${registryProject.alias ?? "(none)"}`);
|
|
405
|
+
lines.push(` initStatus: ${registryProject.initStatus ?? "(none)"}`);
|
|
406
|
+
lines.push(` initUpdatedAt: ${registryProject.initUpdatedAt ?? "(none)"}`);
|
|
407
|
+
if (registryProject.localPaths && registryProject.localPaths.length > 0) {
|
|
408
|
+
lines.push(` localPaths: ${registryProject.localPaths.length} path(s)`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
lines.push(" project: not found");
|
|
413
|
+
}
|
|
414
|
+
lines.push("");
|
|
415
|
+
lines.push("Recommended");
|
|
416
|
+
if (recommendsResume) {
|
|
417
|
+
lines.push(" dvb init --resume");
|
|
418
|
+
}
|
|
419
|
+
else if (recommendsInit) {
|
|
420
|
+
lines.push(" dvb init");
|
|
421
|
+
}
|
|
422
|
+
else if (recommendsForce) {
|
|
423
|
+
lines.push(" Resume is not available from this clone (no init checkpoint).");
|
|
424
|
+
lines.push(" If init was started elsewhere, re-run `dvb init --resume` from that repo.");
|
|
425
|
+
lines.push(" Otherwise, restart init with `dvb init --force` (re-provisions the remote workdir).");
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
lines.push(" (none)");
|
|
429
|
+
}
|
|
430
|
+
lines.push("");
|
|
431
|
+
if (parsed.json) {
|
|
432
|
+
console.log(JSON.stringify({
|
|
433
|
+
ok: true,
|
|
434
|
+
repo: {
|
|
435
|
+
root: repoRoot,
|
|
436
|
+
origin: normalizedOrigin ?? origin ?? null,
|
|
437
|
+
fingerprint,
|
|
438
|
+
},
|
|
439
|
+
local: {
|
|
440
|
+
marker: repoMarker ?? null,
|
|
441
|
+
initState: initState ?? null,
|
|
442
|
+
initFingerprintMismatch,
|
|
443
|
+
inferredStatus: localStatus,
|
|
444
|
+
},
|
|
445
|
+
registry: {
|
|
446
|
+
socketPath: socketInfo.socketPath,
|
|
447
|
+
error: daemonError,
|
|
448
|
+
project: registryProject,
|
|
449
|
+
},
|
|
450
|
+
recommended: recommendsResume
|
|
451
|
+
? "dvb init --resume"
|
|
452
|
+
: recommendsInit
|
|
453
|
+
? "dvb init"
|
|
454
|
+
: recommendsForce
|
|
455
|
+
? "dvb init --force"
|
|
456
|
+
: null,
|
|
457
|
+
}, null, 2));
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
console.log(lines.join("\n"));
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
156
463
|
if (parsed.resume) {
|
|
157
464
|
if (initFingerprintMismatch) {
|
|
158
465
|
throw new Error("Init state does not match this repo. Remove .devbox/init-state.json and run `dvb init` again.");
|
|
@@ -203,6 +510,63 @@ export const runInit = async (args) => {
|
|
|
203
510
|
};
|
|
204
511
|
await writeInitState(repoRoot, initState);
|
|
205
512
|
};
|
|
513
|
+
const recordCodexCheckpoint = async ({ client, canonical, phase, }) => {
|
|
514
|
+
const createdAt = new Date().toISOString();
|
|
515
|
+
const label = phase === "preCodexSetup" ? "pre-codex-setup" : "post-codex-setup";
|
|
516
|
+
const comment = `dvb init: ${label} repo=${repoName} fingerprint=${fingerprint} at=${createdAt}`;
|
|
517
|
+
const events = await client.createCheckpoint(canonical, { comment });
|
|
518
|
+
const id = extractCheckpointId(events);
|
|
519
|
+
if (!id) {
|
|
520
|
+
logger.warn("init_checkpoint_id_missing", {
|
|
521
|
+
box: canonical,
|
|
522
|
+
fingerprint,
|
|
523
|
+
phase: label,
|
|
524
|
+
});
|
|
525
|
+
return { id: null, comment, createdAt };
|
|
526
|
+
}
|
|
527
|
+
const record = { id, comment, createdAt };
|
|
528
|
+
if (initState) {
|
|
529
|
+
await updateInitState({
|
|
530
|
+
checkpoints: { ...(initState.checkpoints ?? {}), [phase]: record },
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
const metaPath = `/home/sprite/.devbox/projects/${fingerprint}.json`;
|
|
534
|
+
try {
|
|
535
|
+
let meta = {};
|
|
536
|
+
try {
|
|
537
|
+
const raw = await client.readFile(canonical, { path: metaPath });
|
|
538
|
+
const parsed = JSON.parse(Buffer.from(raw).toString("utf8"));
|
|
539
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
540
|
+
meta = parsed;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
if (!(error instanceof SpritesApiError && error.status === 404)) {
|
|
545
|
+
throw error;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
const existingCheckpoints = meta.checkpoints && typeof meta.checkpoints === "object" && !Array.isArray(meta.checkpoints)
|
|
549
|
+
? meta.checkpoints
|
|
550
|
+
: {};
|
|
551
|
+
const updated = {
|
|
552
|
+
...meta,
|
|
553
|
+
checkpoints: {
|
|
554
|
+
...existingCheckpoints,
|
|
555
|
+
[phase]: record,
|
|
556
|
+
},
|
|
557
|
+
};
|
|
558
|
+
await client.writeFile(canonical, metaPath, Buffer.from(JSON.stringify(updated, null, 2)));
|
|
559
|
+
}
|
|
560
|
+
catch (error) {
|
|
561
|
+
logger.warn("init_checkpoint_meta_update_failed", {
|
|
562
|
+
box: canonical,
|
|
563
|
+
fingerprint,
|
|
564
|
+
phase: label,
|
|
565
|
+
error: error instanceof Error ? error.message : String(error),
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
return { id, comment, createdAt };
|
|
569
|
+
};
|
|
206
570
|
if (parsed.codexSetupOnly) {
|
|
207
571
|
if (!repoMarker?.canonical) {
|
|
208
572
|
throw new Error("Repo is not initialized. Run `dvb init` first.");
|
|
@@ -229,21 +593,32 @@ export const runInit = async (args) => {
|
|
|
229
593
|
catch {
|
|
230
594
|
// artifacts are optional in setup-only flow
|
|
231
595
|
}
|
|
232
|
-
status.stage("Starting dvbd");
|
|
233
596
|
const socketInfo = resolveSocketInfo();
|
|
234
|
-
await
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
597
|
+
await runInitStep({
|
|
598
|
+
enabled: progressEnabled,
|
|
599
|
+
title: "Starting dvbd",
|
|
600
|
+
fn: async () => {
|
|
601
|
+
await ensureDaemonRunning(socketInfo.socketPath);
|
|
602
|
+
await requireDaemonFeatures(socketInfo.socketPath, ["ports"]);
|
|
603
|
+
},
|
|
604
|
+
});
|
|
605
|
+
const { config, client } = await runInitStep({
|
|
606
|
+
enabled: progressEnabled,
|
|
607
|
+
title: "Loading devbox config",
|
|
608
|
+
fn: async () => {
|
|
609
|
+
const config = await loadConfig(process.env.HOME ? { homeDir: process.env.HOME } : undefined);
|
|
610
|
+
const store = await createSecretStore(config?.tokenStore, process.env.HOME ? { homeDir: process.env.HOME } : undefined);
|
|
611
|
+
const token = await store.getToken();
|
|
612
|
+
if (!token) {
|
|
613
|
+
throw new Error("Sprites token missing. Run `dvb setup` first.");
|
|
614
|
+
}
|
|
615
|
+
const apiBaseUrl = resolveSpritesApiUrl(config);
|
|
616
|
+
const client = createSpritesClient({
|
|
617
|
+
apiBaseUrl,
|
|
618
|
+
token,
|
|
619
|
+
});
|
|
620
|
+
return { config, client };
|
|
621
|
+
},
|
|
247
622
|
});
|
|
248
623
|
const canonical = repoMarker.canonical;
|
|
249
624
|
let expandedWorkdir = expandHome(`~/${slug}`);
|
|
@@ -270,34 +645,139 @@ export const runInit = async (args) => {
|
|
|
270
645
|
catch {
|
|
271
646
|
servicesPlan = null;
|
|
272
647
|
}
|
|
273
|
-
await
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
648
|
+
await runInitStep({
|
|
649
|
+
enabled: progressEnabled,
|
|
650
|
+
title: "Configuring git safe.directory",
|
|
651
|
+
fn: async ({ status }) => {
|
|
652
|
+
await ensureGitSafeDirectory({
|
|
653
|
+
client,
|
|
654
|
+
canonical,
|
|
655
|
+
workdir: expandedWorkdir,
|
|
656
|
+
status,
|
|
657
|
+
});
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
await runInitStep({
|
|
661
|
+
enabled: progressEnabled,
|
|
662
|
+
title: "Uploading setup plan",
|
|
663
|
+
fn: async ({ status }) => {
|
|
664
|
+
await uploadSetupPlan({
|
|
665
|
+
client,
|
|
666
|
+
canonical,
|
|
667
|
+
localSetupPath: setupPath,
|
|
668
|
+
remoteSetupPath,
|
|
669
|
+
localArtifactsBundlePath: artifactsBundlePath,
|
|
670
|
+
localArtifactsManifestPath: artifactsManifestPath,
|
|
671
|
+
remoteArtifactsBundlePath: artifactsBundlePath
|
|
672
|
+
? remoteArtifactsBundlePath
|
|
673
|
+
: null,
|
|
674
|
+
remoteArtifactsManifestPath: artifactsBundlePath
|
|
675
|
+
? remoteArtifactsManifestPath
|
|
676
|
+
: null,
|
|
677
|
+
status,
|
|
678
|
+
});
|
|
679
|
+
},
|
|
287
680
|
});
|
|
288
|
-
await
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
681
|
+
await runInitStep({
|
|
682
|
+
enabled: progressEnabled,
|
|
683
|
+
title: "Staging setup artifacts",
|
|
684
|
+
fn: async ({ status }) => {
|
|
685
|
+
status.stage("Copying repo artifacts and staging external files");
|
|
686
|
+
await stageRemoteSetupArtifacts({
|
|
687
|
+
client,
|
|
688
|
+
canonical,
|
|
689
|
+
workdir: expandedWorkdir,
|
|
690
|
+
artifactsBundlePath: remoteArtifactsBundlePath,
|
|
691
|
+
artifactsManifestPath: remoteArtifactsManifestPath,
|
|
692
|
+
});
|
|
693
|
+
},
|
|
694
|
+
});
|
|
695
|
+
await runInitStep({
|
|
696
|
+
enabled: progressEnabled,
|
|
697
|
+
title: "Snapshotting filesystem (pre-setup) (optional)",
|
|
698
|
+
fn: async ({ fail, ok }) => {
|
|
699
|
+
try {
|
|
700
|
+
const checkpoint = await recordCodexCheckpoint({
|
|
701
|
+
client,
|
|
702
|
+
canonical,
|
|
703
|
+
phase: "preCodexSetup",
|
|
704
|
+
});
|
|
705
|
+
if (checkpoint.id) {
|
|
706
|
+
ok(`Snapshot created: ${checkpoint.id}`);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
catch (error) {
|
|
710
|
+
logger.warn("init_checkpoint_create_failed", {
|
|
711
|
+
box: canonical,
|
|
712
|
+
fingerprint,
|
|
713
|
+
phase: "pre-codex-setup",
|
|
714
|
+
error: error instanceof Error ? error.message : String(error),
|
|
715
|
+
});
|
|
716
|
+
fail("Snapshotting filesystem (pre-setup) (optional) (failed)");
|
|
717
|
+
if (!parsed.json) {
|
|
718
|
+
console.warn("Warning: failed to create pre-setup filesystem snapshot. Continuing without it.");
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
},
|
|
722
|
+
});
|
|
723
|
+
await runInitStep({
|
|
724
|
+
enabled: progressEnabled,
|
|
725
|
+
title: "Enabling devbox services",
|
|
726
|
+
fn: async ({ status }) => {
|
|
727
|
+
await enableRemoteServices({
|
|
728
|
+
client,
|
|
729
|
+
canonical,
|
|
730
|
+
services: servicesPlan?.backgroundServices ?? [],
|
|
731
|
+
status,
|
|
732
|
+
});
|
|
733
|
+
},
|
|
734
|
+
});
|
|
735
|
+
await runInitStep({
|
|
736
|
+
enabled: progressEnabled,
|
|
737
|
+
title: "Applying setup plan",
|
|
738
|
+
fn: async ({ status }) => {
|
|
739
|
+
await runRemoteCodexSetup({
|
|
740
|
+
client,
|
|
741
|
+
canonical,
|
|
742
|
+
expandedWorkdir,
|
|
743
|
+
remoteSetupPath,
|
|
744
|
+
remoteArtifactsBundlePath,
|
|
745
|
+
remoteArtifactsManifestPath,
|
|
746
|
+
socketInfo,
|
|
747
|
+
status,
|
|
748
|
+
pathSetup,
|
|
749
|
+
entrypoints: servicesPlan?.appEntrypoints ?? [],
|
|
750
|
+
});
|
|
751
|
+
},
|
|
752
|
+
});
|
|
753
|
+
await runInitStep({
|
|
754
|
+
enabled: progressEnabled,
|
|
755
|
+
title: "Snapshotting filesystem (post-setup) (optional)",
|
|
756
|
+
fn: async ({ fail, ok }) => {
|
|
757
|
+
try {
|
|
758
|
+
const checkpoint = await recordCodexCheckpoint({
|
|
759
|
+
client,
|
|
760
|
+
canonical,
|
|
761
|
+
phase: "postCodexSetup",
|
|
762
|
+
});
|
|
763
|
+
if (checkpoint.id) {
|
|
764
|
+
ok(`Snapshot created: ${checkpoint.id}`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
catch (error) {
|
|
768
|
+
logger.warn("init_checkpoint_create_failed", {
|
|
769
|
+
box: canonical,
|
|
770
|
+
fingerprint,
|
|
771
|
+
phase: "post-codex-setup",
|
|
772
|
+
error: error instanceof Error ? error.message : String(error),
|
|
773
|
+
});
|
|
774
|
+
fail("Snapshotting filesystem (post-setup) (optional) (failed)");
|
|
775
|
+
if (!parsed.json) {
|
|
776
|
+
console.warn("Warning: failed to create post-setup filesystem snapshot. Continuing without it.");
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
},
|
|
299
780
|
});
|
|
300
|
-
status.stop();
|
|
301
781
|
if (parsed.json) {
|
|
302
782
|
console.log(JSON.stringify({ ok: true }, null, 2));
|
|
303
783
|
return;
|
|
@@ -305,13 +785,23 @@ export const runInit = async (args) => {
|
|
|
305
785
|
console.log("Codex setup complete.");
|
|
306
786
|
return;
|
|
307
787
|
}
|
|
308
|
-
status.stage("Starting dvbd");
|
|
309
788
|
const socketInfo = resolveSocketInfo();
|
|
310
|
-
await
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
789
|
+
await runInitStep({
|
|
790
|
+
enabled: progressEnabled,
|
|
791
|
+
title: "Starting dvbd",
|
|
792
|
+
fn: async () => {
|
|
793
|
+
await ensureDaemonRunning(socketInfo.socketPath);
|
|
794
|
+
await requireDaemonFeatures(socketInfo.socketPath, ["registry"]);
|
|
795
|
+
},
|
|
796
|
+
});
|
|
797
|
+
const existingEntry = await runInitStep({
|
|
798
|
+
enabled: progressEnabled,
|
|
799
|
+
title: "Checking sprites",
|
|
800
|
+
fn: async () => {
|
|
801
|
+
const existingProject = await requestJson(socketInfo.socketPath, "GET", `/registry/project?fingerprint=${encodeURIComponent(fingerprint)}`, DAEMON_TIMEOUT_MS.registry);
|
|
802
|
+
return existingProject.body.project ?? null;
|
|
803
|
+
},
|
|
804
|
+
});
|
|
315
805
|
if (existingEntry && !parsed.force) {
|
|
316
806
|
if (shouldResume) {
|
|
317
807
|
if (initState?.canonical &&
|
|
@@ -335,14 +825,20 @@ export const runInit = async (args) => {
|
|
|
335
825
|
const remoteArtifactsBundlePath = path.posix.join(expandedWorkdir, ".devbox", "setup-artifacts.tgz");
|
|
336
826
|
const remoteArtifactsManifestPath = path.posix.join(expandedWorkdir, ".devbox", "setup-artifacts.json");
|
|
337
827
|
const pathSetup = 'export PATH="$(npm bin -g 2>/dev/null):$PATH"';
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
828
|
+
const { config, client, controlPlaneToken } = await runInitStep({
|
|
829
|
+
enabled: progressEnabled,
|
|
830
|
+
title: "Loading devbox config",
|
|
831
|
+
fn: async ({ status }) => {
|
|
832
|
+
const config = await loadConfig(process.env.HOME ? { homeDir: process.env.HOME } : undefined);
|
|
833
|
+
const store = await createSecretStore(config?.tokenStore, process.env.HOME ? { homeDir: process.env.HOME } : undefined);
|
|
834
|
+
const { token, controlPlaneToken } = await ensureSpritesToken(store, (message) => status.stage(message));
|
|
835
|
+
const apiBaseUrl = resolveSpritesApiUrl(config);
|
|
836
|
+
const client = createSpritesClient({
|
|
837
|
+
apiBaseUrl,
|
|
838
|
+
token,
|
|
839
|
+
});
|
|
840
|
+
return { config, client, controlPlaneToken };
|
|
841
|
+
},
|
|
346
842
|
});
|
|
347
843
|
const username = os.userInfo().username;
|
|
348
844
|
let canonical = (shouldResume && initState?.canonical ? initState.canonical : null) ??
|
|
@@ -368,25 +864,32 @@ export const runInit = async (args) => {
|
|
|
368
864
|
};
|
|
369
865
|
const skipCreate = shouldResume && initState?.steps.spritesCreated && initState.canonical;
|
|
370
866
|
if (!skipCreate) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
867
|
+
canonical = await runInitStep({
|
|
868
|
+
enabled: progressEnabled,
|
|
869
|
+
title: "Creating devbox",
|
|
870
|
+
fn: async ({ status }) => {
|
|
871
|
+
let nextCanonical = canonical;
|
|
872
|
+
const createResult = await createSprite(nextCanonical);
|
|
873
|
+
if (createResult === "exists" && !parsed.force) {
|
|
874
|
+
if (canonicalHint) {
|
|
875
|
+
throw new Error(`Sprite already exists: ${nextCanonical}`);
|
|
876
|
+
}
|
|
877
|
+
const suffix = fingerprint.slice(0, 6);
|
|
878
|
+
nextCanonical = `${nextCanonical}-${suffix}`;
|
|
879
|
+
status.stage("Resolving devbox name");
|
|
880
|
+
const second = await createSprite(nextCanonical);
|
|
881
|
+
if (second === "exists") {
|
|
882
|
+
throw new Error(`Sprite already exists: ${nextCanonical}`);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
await updateInitState({
|
|
886
|
+
canonical: nextCanonical,
|
|
887
|
+
alias,
|
|
888
|
+
workdir,
|
|
889
|
+
steps: { spritesCreated: true },
|
|
890
|
+
});
|
|
891
|
+
return nextCanonical;
|
|
892
|
+
},
|
|
390
893
|
});
|
|
391
894
|
}
|
|
392
895
|
const aliasLookup = await requestJson(socketInfo.socketPath, "GET", `/registry/alias?alias=${encodeURIComponent(alias)}`, DAEMON_TIMEOUT_MS.registry);
|
|
@@ -422,109 +925,134 @@ export const runInit = async (args) => {
|
|
|
422
925
|
});
|
|
423
926
|
}
|
|
424
927
|
};
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
928
|
+
await runInitStep({
|
|
929
|
+
enabled: progressEnabled,
|
|
930
|
+
title: "Bootstrapping devbox",
|
|
931
|
+
fn: async ({ fail }) => {
|
|
932
|
+
try {
|
|
933
|
+
await bootstrapDevbox(client, canonical);
|
|
934
|
+
}
|
|
935
|
+
catch (error) {
|
|
936
|
+
logger.warn("devbox_bootstrap_failed", {
|
|
937
|
+
box: canonical,
|
|
938
|
+
error: String(error),
|
|
939
|
+
});
|
|
940
|
+
fail("Bootstrapping devbox (failed)");
|
|
941
|
+
if (!parsed.json) {
|
|
942
|
+
const message = error instanceof Error && error.message
|
|
943
|
+
? error.message
|
|
944
|
+
: String(error);
|
|
945
|
+
console.warn(`Warning: devbox bootstrap failed. ${message}`);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
},
|
|
949
|
+
});
|
|
442
950
|
const initialStatus = resolveInitStatus(initState?.steps, initState?.complete);
|
|
443
951
|
if (!shouldResume || !initState?.steps.registrySynced) {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
952
|
+
await runInitStep({
|
|
953
|
+
enabled: progressEnabled,
|
|
954
|
+
title: "Syncing sprites",
|
|
955
|
+
fn: async () => {
|
|
956
|
+
await requestJson(socketInfo.socketPath, "POST", "/registry/upsert", DAEMON_TIMEOUT_MS.registry, {
|
|
957
|
+
project: buildProjectEntry(initialStatus),
|
|
958
|
+
box: {
|
|
959
|
+
canonical,
|
|
960
|
+
org: config?.org,
|
|
961
|
+
createdAt: new Date().toISOString(),
|
|
962
|
+
},
|
|
963
|
+
alias: { alias, canonical },
|
|
964
|
+
});
|
|
965
|
+
await updateInitState({
|
|
966
|
+
canonical,
|
|
967
|
+
alias,
|
|
968
|
+
workdir,
|
|
969
|
+
steps: { registrySynced: true },
|
|
970
|
+
});
|
|
451
971
|
},
|
|
452
|
-
alias: { alias, canonical },
|
|
453
|
-
});
|
|
454
|
-
await updateInitState({
|
|
455
|
-
canonical,
|
|
456
|
-
alias,
|
|
457
|
-
workdir,
|
|
458
|
-
steps: { registrySynced: true },
|
|
459
972
|
});
|
|
460
973
|
}
|
|
461
974
|
if (!shouldResume || !initState?.steps.daemonInstalled) {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
await updateInitState({ steps: { daemonServiceEnsured: true } });
|
|
506
|
-
}
|
|
507
|
-
catch (error) {
|
|
508
|
-
logger.warn("sprite_daemon_service_failed", {
|
|
509
|
-
box: canonical,
|
|
510
|
-
error: String(error),
|
|
975
|
+
await runInitStep({
|
|
976
|
+
enabled: progressEnabled,
|
|
977
|
+
title: "Installing sprite daemon",
|
|
978
|
+
fn: async ({ fail }) => {
|
|
979
|
+
try {
|
|
980
|
+
const convexUrl = getConvexUrl();
|
|
981
|
+
if (!controlPlaneToken) {
|
|
982
|
+
throw new Error("Control plane session required to install daemon.");
|
|
983
|
+
}
|
|
984
|
+
if (!convexUrl) {
|
|
985
|
+
throw new Error("Convex URL unavailable.");
|
|
986
|
+
}
|
|
987
|
+
const release = await fetchSpriteDaemonRelease(controlPlaneToken);
|
|
988
|
+
if (!release) {
|
|
989
|
+
throw new Error("No sprite daemon release available.");
|
|
990
|
+
}
|
|
991
|
+
const heartbeatToken = await issueSpriteDaemonToken(controlPlaneToken, canonical);
|
|
992
|
+
if (!heartbeatToken) {
|
|
993
|
+
throw new Error("Daemon token unavailable.");
|
|
994
|
+
}
|
|
995
|
+
await installSpriteDaemon({
|
|
996
|
+
client,
|
|
997
|
+
spriteName: canonical,
|
|
998
|
+
release,
|
|
999
|
+
convexUrl,
|
|
1000
|
+
heartbeatToken,
|
|
1001
|
+
});
|
|
1002
|
+
await updateInitState({ steps: { daemonInstalled: true } });
|
|
1003
|
+
}
|
|
1004
|
+
catch (error) {
|
|
1005
|
+
logger.warn("sprite_daemon_install_failed", {
|
|
1006
|
+
box: canonical,
|
|
1007
|
+
error: String(error),
|
|
1008
|
+
});
|
|
1009
|
+
fail("Installing sprite daemon (failed)");
|
|
1010
|
+
if (!parsed.json) {
|
|
1011
|
+
const message = error instanceof Error && error.message
|
|
1012
|
+
? error.message
|
|
1013
|
+
: String(error);
|
|
1014
|
+
console.warn(`Warning: failed to install sprite daemon. ${message}`);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
},
|
|
511
1018
|
});
|
|
512
|
-
if (!parsed.json) {
|
|
513
|
-
status.stop();
|
|
514
|
-
const message = error instanceof Error && error.message
|
|
515
|
-
? error.message
|
|
516
|
-
: String(error);
|
|
517
|
-
console.warn(`Warning: failed to ensure sprite daemon service. ${message}`);
|
|
518
|
-
}
|
|
519
1019
|
}
|
|
1020
|
+
await runInitStep({
|
|
1021
|
+
enabled: progressEnabled,
|
|
1022
|
+
title: "Ensuring sprite daemon service",
|
|
1023
|
+
fn: async ({ fail }) => {
|
|
1024
|
+
try {
|
|
1025
|
+
await ensureSpriteDaemonService({ client, spriteName: canonical });
|
|
1026
|
+
await updateInitState({ steps: { daemonServiceEnsured: true } });
|
|
1027
|
+
}
|
|
1028
|
+
catch (error) {
|
|
1029
|
+
logger.warn("sprite_daemon_service_failed", {
|
|
1030
|
+
box: canonical,
|
|
1031
|
+
error: String(error),
|
|
1032
|
+
});
|
|
1033
|
+
fail("Ensuring sprite daemon service (failed)");
|
|
1034
|
+
if (!parsed.json) {
|
|
1035
|
+
const message = error instanceof Error && error.message
|
|
1036
|
+
? error.message
|
|
1037
|
+
: String(error);
|
|
1038
|
+
console.warn(`Warning: failed to ensure sprite daemon service. ${message}`);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
},
|
|
1042
|
+
});
|
|
520
1043
|
const setupDir = path.join(repoRoot, ".devbox");
|
|
521
1044
|
const setupPath = path.join(setupDir, "setup.json");
|
|
522
1045
|
const servicesPath = path.join(setupDir, "services.json");
|
|
1046
|
+
const scansDir = path.join(setupDir, "scans");
|
|
1047
|
+
const setupEnvSecretsScanPath = path.join(scansDir, "setup-env-secrets.json");
|
|
1048
|
+
const setupExternalScanPath = path.join(scansDir, "setup-external.json");
|
|
1049
|
+
const setupExtraArtifactsScanPath = path.join(scansDir, "setup-extra-artifacts.json");
|
|
523
1050
|
let setupArtifacts = null;
|
|
524
1051
|
const nonInteractive = !process.stdin.isTTY || parsed.json;
|
|
525
1052
|
const skipSetupPlan = shouldResume && initState?.steps.setupPlanWritten;
|
|
526
1053
|
const skipServicesPlan = shouldResume && initState?.steps.servicesPlanWritten;
|
|
527
1054
|
const skipServicesConfig = shouldResume && initState?.steps.servicesConfigWritten;
|
|
1055
|
+
const skipServicesEnable = shouldResume && initState?.steps.servicesEnabled;
|
|
528
1056
|
const skipSetupUpload = nonInteractive || (shouldResume && initState?.steps.setupUploaded);
|
|
529
1057
|
const skipCodexApply = nonInteractive || (shouldResume && initState?.steps.codexApplied);
|
|
530
1058
|
let approvedPlan = null;
|
|
@@ -532,334 +1060,567 @@ export const runInit = async (args) => {
|
|
|
532
1060
|
const setupTempDir = await fs.mkdtemp(path.join(os.tmpdir(), "devbox-setup-"));
|
|
533
1061
|
try {
|
|
534
1062
|
await fs.mkdir(setupDir, { recursive: true });
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
if (!skipSetupPlan) {
|
|
539
|
-
setupSchemaPath = await writeSetupSchema(setupTempDir);
|
|
1063
|
+
const tryReadSetupPlan = async () => {
|
|
1064
|
+
try {
|
|
1065
|
+
return await readSetupPlan(setupPath);
|
|
540
1066
|
}
|
|
541
|
-
|
|
542
|
-
|
|
1067
|
+
catch {
|
|
1068
|
+
return null;
|
|
543
1069
|
}
|
|
544
|
-
|
|
545
|
-
|
|
1070
|
+
};
|
|
1071
|
+
const tryReadServicesPlan = async () => {
|
|
1072
|
+
try {
|
|
1073
|
+
return await readServicesPlan(servicesPath);
|
|
546
1074
|
}
|
|
547
|
-
|
|
548
|
-
|
|
1075
|
+
catch {
|
|
1076
|
+
return null;
|
|
549
1077
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
: runLocalSetupScan({
|
|
555
|
-
cwd: repoRoot,
|
|
556
|
-
schemaPath: setupSchemaPath,
|
|
557
|
-
outputPath: setupPath,
|
|
558
|
-
homeDir: localHomeDir,
|
|
559
|
-
onProgress: (message) => {
|
|
560
|
-
const base = "Analyzing local environment — setup";
|
|
561
|
-
status.stage(`${base} — ${message}`);
|
|
562
|
-
},
|
|
563
|
-
}),
|
|
564
|
-
skipServicesPlan
|
|
565
|
-
? Promise.resolve()
|
|
566
|
-
: runLocalServicesScan({
|
|
567
|
-
cwd: repoRoot,
|
|
568
|
-
schemaPath: servicesSchemaPath,
|
|
569
|
-
outputPath: servicesPath,
|
|
570
|
-
homeDir: localHomeDir,
|
|
571
|
-
onProgress: (message) => {
|
|
572
|
-
const base = "Analyzing local environment — services";
|
|
573
|
-
status.stage(`${base} — ${message}`);
|
|
574
|
-
},
|
|
575
|
-
}),
|
|
576
|
-
]);
|
|
577
|
-
}
|
|
578
|
-
if (!skipSetupPlan) {
|
|
579
|
-
const setupPlan = await readSetupPlan(setupPath);
|
|
580
|
-
if (nonInteractive) {
|
|
581
|
-
approvedPlan = setupPlan;
|
|
1078
|
+
};
|
|
1079
|
+
const tryReadEnvSecretsScan = async () => {
|
|
1080
|
+
try {
|
|
1081
|
+
return await readSetupEnvSecretsPlan(setupEnvSecretsScanPath);
|
|
582
1082
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
throw new Error("Interactive terminal required to approve setup.");
|
|
586
|
-
}
|
|
587
|
-
status.stage("Reviewing setup findings");
|
|
588
|
-
status.stop();
|
|
589
|
-
approvedPlan = await promptForPlanApproval(setupPlan);
|
|
1083
|
+
catch {
|
|
1084
|
+
return null;
|
|
590
1085
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
approvedPlan = await readSetupPlan(setupPath);
|
|
596
|
-
}
|
|
597
|
-
if (!skipServicesPlan) {
|
|
598
|
-
const servicesPlan = await readServicesPlan(servicesPath);
|
|
599
|
-
if (nonInteractive) {
|
|
600
|
-
approvedServices = servicesPlan;
|
|
1086
|
+
};
|
|
1087
|
+
const tryReadExternalScan = async () => {
|
|
1088
|
+
try {
|
|
1089
|
+
return await readSetupExternalPlan(setupExternalScanPath);
|
|
601
1090
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
throw new Error("Interactive terminal required to approve services.");
|
|
605
|
-
}
|
|
606
|
-
status.stage("Reviewing run services findings");
|
|
607
|
-
status.stop();
|
|
608
|
-
approvedServices = await promptForServicesApproval(servicesPlan);
|
|
1091
|
+
catch {
|
|
1092
|
+
return null;
|
|
609
1093
|
}
|
|
610
|
-
|
|
611
|
-
|
|
1094
|
+
};
|
|
1095
|
+
const tryReadExtraArtifactsScan = async () => {
|
|
1096
|
+
try {
|
|
1097
|
+
return await readSetupExtraArtifactsPlan(setupExtraArtifactsScanPath);
|
|
1098
|
+
}
|
|
1099
|
+
catch {
|
|
1100
|
+
return null;
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
const shouldRetryCodexScan = (scanFullyCompleted, ...arrays) => !scanFullyCompleted && arrays.every((array) => array.length === 0);
|
|
1104
|
+
let setupPlan = skipSetupPlan || !shouldResume ? null : await tryReadSetupPlan();
|
|
1105
|
+
let servicesPlan = skipServicesPlan || !shouldResume ? null : await tryReadServicesPlan();
|
|
1106
|
+
const needsSetupScan = !skipSetupPlan && !setupPlan;
|
|
1107
|
+
let envSecretsScan = !needsSetupScan || !shouldResume ? null : await tryReadEnvSecretsScan();
|
|
1108
|
+
let externalScan = !needsSetupScan || !shouldResume ? null : await tryReadExternalScan();
|
|
1109
|
+
let extraArtifactsScan = !needsSetupScan || !shouldResume
|
|
1110
|
+
? null
|
|
1111
|
+
: await tryReadExtraArtifactsScan();
|
|
1112
|
+
if (servicesPlan &&
|
|
1113
|
+
shouldRetryCodexScan(servicesPlan.scanFullyCompleted, servicesPlan.appEntrypoints, servicesPlan.backgroundServices)) {
|
|
1114
|
+
servicesPlan = null;
|
|
612
1115
|
}
|
|
613
|
-
|
|
614
|
-
|
|
1116
|
+
if (envSecretsScan &&
|
|
1117
|
+
shouldRetryCodexScan(envSecretsScan.scanFullyCompleted, envSecretsScan.envFiles, envSecretsScan.secretFiles)) {
|
|
1118
|
+
envSecretsScan = null;
|
|
615
1119
|
}
|
|
616
|
-
if (
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
repoRoot,
|
|
620
|
-
plan: approvedPlan,
|
|
621
|
-
outputDir: setupDir,
|
|
622
|
-
tempDir: setupTempDir,
|
|
623
|
-
homeDir: localHomeDir,
|
|
624
|
-
});
|
|
1120
|
+
if (externalScan &&
|
|
1121
|
+
shouldRetryCodexScan(externalScan.scanFullyCompleted, externalScan.externalDependencies, externalScan.externalConfigs)) {
|
|
1122
|
+
externalScan = null;
|
|
625
1123
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
}
|
|
630
|
-
const skipProvision = shouldResume && initState?.steps.workdirProvisioned;
|
|
631
|
-
if (!skipProvision || !skipSetupUpload) {
|
|
632
|
-
if (!skipProvision) {
|
|
633
|
-
status.stage("Packaging repo");
|
|
1124
|
+
if (extraArtifactsScan &&
|
|
1125
|
+
shouldRetryCodexScan(extraArtifactsScan.scanFullyCompleted, extraArtifactsScan.extraArtifacts)) {
|
|
1126
|
+
extraArtifactsScan = null;
|
|
634
1127
|
}
|
|
635
|
-
const
|
|
636
|
-
const
|
|
637
|
-
const
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
1128
|
+
const needsServicesScan = !skipServicesPlan && !servicesPlan;
|
|
1129
|
+
const needsEnvSecretsScan = needsSetupScan && !envSecretsScan;
|
|
1130
|
+
const needsExternalScan = needsSetupScan && !externalScan;
|
|
1131
|
+
const needsExtraArtifactsScan = needsSetupScan && !extraArtifactsScan;
|
|
1132
|
+
if (needsSetupScan || needsServicesScan) {
|
|
1133
|
+
const runLocalEnvironmentAnalysis = async ({ updateEnvSecrets, updateExternal, updateExtraArtifacts, updateServices, }) => {
|
|
1134
|
+
let envSecretsSchemaPath = null;
|
|
1135
|
+
let externalSchemaPath = null;
|
|
1136
|
+
let extraArtifactsSchemaPath = null;
|
|
1137
|
+
let servicesSchemaPath = null;
|
|
1138
|
+
if (needsEnvSecretsScan) {
|
|
1139
|
+
envSecretsSchemaPath =
|
|
1140
|
+
await writeSetupEnvSecretsSchema(setupTempDir);
|
|
1141
|
+
}
|
|
1142
|
+
if (needsExternalScan) {
|
|
1143
|
+
externalSchemaPath = await writeSetupExternalSchema(setupTempDir);
|
|
1144
|
+
}
|
|
1145
|
+
if (needsExtraArtifactsScan) {
|
|
1146
|
+
extraArtifactsSchemaPath =
|
|
1147
|
+
await writeSetupExtraArtifactsSchema(setupTempDir);
|
|
1148
|
+
}
|
|
1149
|
+
if (needsServicesScan) {
|
|
1150
|
+
servicesSchemaPath = await writeServicesSchema(setupTempDir);
|
|
1151
|
+
}
|
|
1152
|
+
if (needsEnvSecretsScan && !envSecretsSchemaPath) {
|
|
1153
|
+
throw new Error("Env/secrets schema path missing.");
|
|
1154
|
+
}
|
|
1155
|
+
if (needsExternalScan && !externalSchemaPath) {
|
|
1156
|
+
throw new Error("External schema path missing.");
|
|
1157
|
+
}
|
|
1158
|
+
if (needsExtraArtifactsScan && !extraArtifactsSchemaPath) {
|
|
1159
|
+
throw new Error("Extra artifacts schema path missing.");
|
|
1160
|
+
}
|
|
1161
|
+
if (needsServicesScan && !servicesSchemaPath) {
|
|
1162
|
+
throw new Error("Services schema path missing.");
|
|
1163
|
+
}
|
|
1164
|
+
if (needsSetupScan) {
|
|
1165
|
+
await fs.mkdir(scansDir, { recursive: true });
|
|
1166
|
+
}
|
|
1167
|
+
const runCodexScanWithImmediateRetry = async ({ run, read, outputPath, update, shouldRetry, }) => {
|
|
1168
|
+
try {
|
|
1169
|
+
await run();
|
|
1170
|
+
let out = await read();
|
|
1171
|
+
if (shouldRetry(out)) {
|
|
1172
|
+
update("retrying");
|
|
1173
|
+
await fs.rm(outputPath, { force: true });
|
|
1174
|
+
await run();
|
|
1175
|
+
out = await read();
|
|
1176
|
+
}
|
|
1177
|
+
update("done");
|
|
1178
|
+
return out;
|
|
1179
|
+
}
|
|
1180
|
+
catch (error) {
|
|
1181
|
+
update("failed");
|
|
1182
|
+
throw error;
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
const envSecretsPromise = !needsSetupScan
|
|
1186
|
+
? Promise.resolve(null)
|
|
1187
|
+
: !needsEnvSecretsScan
|
|
1188
|
+
? Promise.resolve(envSecretsScan)
|
|
1189
|
+
: runCodexScanWithImmediateRetry({
|
|
1190
|
+
run: async () => await runLocalSetupEnvSecretsScan({
|
|
1191
|
+
cwd: repoRoot,
|
|
1192
|
+
schemaPath: envSecretsSchemaPath,
|
|
1193
|
+
outputPath: setupEnvSecretsScanPath,
|
|
1194
|
+
onProgress: updateEnvSecrets,
|
|
1195
|
+
}),
|
|
1196
|
+
read: async () => await readSetupEnvSecretsPlan(setupEnvSecretsScanPath),
|
|
1197
|
+
outputPath: setupEnvSecretsScanPath,
|
|
1198
|
+
update: updateEnvSecrets,
|
|
1199
|
+
shouldRetry: (plan) => shouldRetryCodexScan(plan.scanFullyCompleted, plan.envFiles, plan.secretFiles),
|
|
1200
|
+
});
|
|
1201
|
+
const externalPromise = !needsSetupScan
|
|
1202
|
+
? Promise.resolve(null)
|
|
1203
|
+
: !needsExternalScan
|
|
1204
|
+
? Promise.resolve(externalScan)
|
|
1205
|
+
: runCodexScanWithImmediateRetry({
|
|
1206
|
+
run: async () => await runLocalSetupExternalScan({
|
|
1207
|
+
cwd: repoRoot,
|
|
1208
|
+
schemaPath: externalSchemaPath,
|
|
1209
|
+
outputPath: setupExternalScanPath,
|
|
1210
|
+
homeDir: localHomeDir,
|
|
1211
|
+
onProgress: updateExternal,
|
|
1212
|
+
}),
|
|
1213
|
+
read: async () => await readSetupExternalPlan(setupExternalScanPath),
|
|
1214
|
+
outputPath: setupExternalScanPath,
|
|
1215
|
+
update: updateExternal,
|
|
1216
|
+
shouldRetry: (plan) => shouldRetryCodexScan(plan.scanFullyCompleted, plan.externalDependencies, plan.externalConfigs),
|
|
1217
|
+
});
|
|
1218
|
+
const extraArtifactsPromise = !needsSetupScan
|
|
1219
|
+
? Promise.resolve(null)
|
|
1220
|
+
: !needsExtraArtifactsScan
|
|
1221
|
+
? Promise.resolve(extraArtifactsScan)
|
|
1222
|
+
: runCodexScanWithImmediateRetry({
|
|
1223
|
+
run: async () => await runLocalSetupExtraArtifactsScan({
|
|
1224
|
+
cwd: repoRoot,
|
|
1225
|
+
schemaPath: extraArtifactsSchemaPath,
|
|
1226
|
+
outputPath: setupExtraArtifactsScanPath,
|
|
1227
|
+
onProgress: updateExtraArtifacts,
|
|
1228
|
+
}),
|
|
1229
|
+
read: async () => await readSetupExtraArtifactsPlan(setupExtraArtifactsScanPath),
|
|
1230
|
+
outputPath: setupExtraArtifactsScanPath,
|
|
1231
|
+
update: updateExtraArtifacts,
|
|
1232
|
+
shouldRetry: (plan) => shouldRetryCodexScan(plan.scanFullyCompleted, plan.extraArtifacts),
|
|
1233
|
+
});
|
|
1234
|
+
const servicesPromise = !needsServicesScan
|
|
1235
|
+
? Promise.resolve(servicesPlan)
|
|
1236
|
+
: runCodexScanWithImmediateRetry({
|
|
1237
|
+
run: async () => await runLocalServicesScan({
|
|
1238
|
+
cwd: repoRoot,
|
|
1239
|
+
schemaPath: servicesSchemaPath,
|
|
1240
|
+
outputPath: servicesPath,
|
|
1241
|
+
homeDir: localHomeDir,
|
|
1242
|
+
onProgress: updateServices,
|
|
1243
|
+
}),
|
|
1244
|
+
read: async () => await readServicesPlan(servicesPath),
|
|
1245
|
+
outputPath: servicesPath,
|
|
1246
|
+
update: updateServices,
|
|
1247
|
+
shouldRetry: (plan) => shouldRetryCodexScan(plan.scanFullyCompleted, plan.appEntrypoints, plan.backgroundServices),
|
|
1248
|
+
});
|
|
1249
|
+
const [envSecrets, external, extraArtifacts, services] = await Promise.all([
|
|
1250
|
+
envSecretsPromise,
|
|
1251
|
+
externalPromise,
|
|
1252
|
+
extraArtifactsPromise,
|
|
1253
|
+
servicesPromise,
|
|
676
1254
|
]);
|
|
677
|
-
if (
|
|
678
|
-
|
|
1255
|
+
if (needsServicesScan) {
|
|
1256
|
+
if (!services) {
|
|
1257
|
+
throw new Error("Services plan missing.");
|
|
1258
|
+
}
|
|
1259
|
+
servicesPlan = services;
|
|
1260
|
+
}
|
|
1261
|
+
if (needsSetupScan) {
|
|
1262
|
+
if (!envSecrets) {
|
|
1263
|
+
throw new Error("Env/secrets scan missing.");
|
|
1264
|
+
}
|
|
1265
|
+
if (!external) {
|
|
1266
|
+
throw new Error("External scan missing.");
|
|
1267
|
+
}
|
|
1268
|
+
if (!extraArtifacts) {
|
|
1269
|
+
throw new Error("Extra artifacts scan missing.");
|
|
1270
|
+
}
|
|
1271
|
+
updateEnvSecrets("merging setup plan");
|
|
1272
|
+
const merged = mergeSetupScans({
|
|
1273
|
+
envSecrets,
|
|
1274
|
+
external,
|
|
1275
|
+
extraArtifacts,
|
|
1276
|
+
});
|
|
1277
|
+
await writeSetupPlan(setupPath, merged);
|
|
1278
|
+
setupPlan = merged;
|
|
679
1279
|
}
|
|
1280
|
+
};
|
|
1281
|
+
if (!progressEnabled) {
|
|
1282
|
+
await runLocalEnvironmentAnalysis({
|
|
1283
|
+
updateEnvSecrets: () => { },
|
|
1284
|
+
updateExternal: () => { },
|
|
1285
|
+
updateExtraArtifacts: () => { },
|
|
1286
|
+
updateServices: () => { },
|
|
1287
|
+
});
|
|
680
1288
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
|
|
1289
|
+
else {
|
|
1290
|
+
const log = clackTaskLog({
|
|
1291
|
+
title: "Analyzing local environment",
|
|
1292
|
+
limit: 1,
|
|
1293
|
+
spacing: 0,
|
|
1294
|
+
});
|
|
1295
|
+
let active = true;
|
|
1296
|
+
const colorCategory = (label) => {
|
|
1297
|
+
if (!process.stdout.hasColors?.())
|
|
1298
|
+
return label;
|
|
1299
|
+
const undim = "\u001b[22m";
|
|
1300
|
+
const dim = "\u001b[2m";
|
|
1301
|
+
const bold = "\u001b[1m";
|
|
1302
|
+
const teal = "\u001b[36m";
|
|
1303
|
+
const resetColor = "\u001b[39m";
|
|
1304
|
+
return `${undim}${teal}${bold}${label}${resetColor}${undim}${dim}`;
|
|
1305
|
+
};
|
|
1306
|
+
const formatRow = (label, message) => {
|
|
1307
|
+
const normalized = message.replace(/\r?\n/g, " ").trim();
|
|
1308
|
+
return `${colorCategory(label)}: ${normalized}`;
|
|
1309
|
+
};
|
|
1310
|
+
const envSecretsRow = log.group("");
|
|
1311
|
+
const externalRow = log.group("");
|
|
1312
|
+
const extraArtifactsRow = log.group("");
|
|
1313
|
+
const servicesRow = log.group("");
|
|
1314
|
+
const makeUpdater = (row, label) => (message) => {
|
|
1315
|
+
if (!active)
|
|
1316
|
+
return;
|
|
1317
|
+
row.message(formatRow(label, message));
|
|
1318
|
+
};
|
|
1319
|
+
const updateEnvSecrets = makeUpdater(envSecretsRow, "env/secrets");
|
|
1320
|
+
const updateExternal = makeUpdater(externalRow, "external");
|
|
1321
|
+
const updateExtraArtifacts = makeUpdater(extraArtifactsRow, "extra artifacts");
|
|
1322
|
+
const updateServices = makeUpdater(servicesRow, "services");
|
|
1323
|
+
updateEnvSecrets(needsSetupScan ? (needsEnvSecretsScan ? "starting" : "cached") : "skipped");
|
|
1324
|
+
updateExternal(needsSetupScan ? (needsExternalScan ? "starting" : "cached") : "skipped");
|
|
1325
|
+
updateExtraArtifacts(needsSetupScan
|
|
1326
|
+
? needsExtraArtifactsScan
|
|
1327
|
+
? "starting"
|
|
1328
|
+
: "cached"
|
|
1329
|
+
: "skipped");
|
|
1330
|
+
updateServices(skipServicesPlan ? "skipped" : needsServicesScan ? "starting" : "cached");
|
|
1331
|
+
try {
|
|
1332
|
+
await runLocalEnvironmentAnalysis({
|
|
1333
|
+
updateEnvSecrets,
|
|
1334
|
+
updateExternal,
|
|
1335
|
+
updateExtraArtifacts,
|
|
1336
|
+
updateServices,
|
|
1337
|
+
});
|
|
1338
|
+
active = false;
|
|
1339
|
+
log.success("Analyzing local environment");
|
|
1340
|
+
}
|
|
1341
|
+
catch (error) {
|
|
1342
|
+
active = false;
|
|
1343
|
+
log.error("Analyzing local environment");
|
|
1344
|
+
throw error;
|
|
700
1345
|
}
|
|
701
1346
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
1347
|
+
}
|
|
1348
|
+
if (!skipSetupPlan && !setupPlan) {
|
|
1349
|
+
setupPlan = await readSetupPlan(setupPath);
|
|
1350
|
+
}
|
|
1351
|
+
if (!skipServicesPlan && !servicesPlan) {
|
|
1352
|
+
servicesPlan = await readServicesPlan(servicesPath);
|
|
1353
|
+
}
|
|
1354
|
+
if (!skipSetupPlan && !setupPlan) {
|
|
1355
|
+
throw new Error("Setup plan missing.");
|
|
1356
|
+
}
|
|
1357
|
+
if (!skipServicesPlan && !servicesPlan) {
|
|
1358
|
+
throw new Error("Services plan missing.");
|
|
1359
|
+
}
|
|
1360
|
+
const scanStepUpdate = {};
|
|
1361
|
+
if (!skipSetupPlan && setupPlan) {
|
|
1362
|
+
if (!initState?.steps.setupEnvSecretsScanned) {
|
|
1363
|
+
scanStepUpdate.setupEnvSecretsScanned = true;
|
|
1364
|
+
}
|
|
1365
|
+
if (!initState?.steps.setupExternalScanned) {
|
|
1366
|
+
scanStepUpdate.setupExternalScanned = true;
|
|
1367
|
+
}
|
|
1368
|
+
if (!initState?.steps.setupExtraArtifactsScanned) {
|
|
1369
|
+
scanStepUpdate.setupExtraArtifactsScanned = true;
|
|
1370
|
+
}
|
|
1371
|
+
if (!initState?.steps.setupPlanScanned) {
|
|
1372
|
+
scanStepUpdate.setupPlanScanned = true;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
if (!skipServicesPlan &&
|
|
1376
|
+
servicesPlan &&
|
|
1377
|
+
!initState?.steps.servicesPlanScanned) {
|
|
1378
|
+
scanStepUpdate.servicesPlanScanned = true;
|
|
1379
|
+
}
|
|
1380
|
+
if (Object.keys(scanStepUpdate).length > 0) {
|
|
1381
|
+
await updateInitState({ steps: scanStepUpdate });
|
|
1382
|
+
}
|
|
1383
|
+
if (skipSetupPlan) {
|
|
1384
|
+
approvedPlan = await readSetupPlan(setupPath);
|
|
1385
|
+
}
|
|
1386
|
+
if (skipServicesPlan) {
|
|
1387
|
+
approvedServices = await readServicesPlan(servicesPath);
|
|
1388
|
+
}
|
|
1389
|
+
if (shouldResume && (approvedPlan || approvedServices)) {
|
|
1390
|
+
const backfillStepUpdate = {};
|
|
1391
|
+
if (approvedPlan) {
|
|
1392
|
+
if (!initState?.steps.setupEnvSecretsScanned) {
|
|
1393
|
+
backfillStepUpdate.setupEnvSecretsScanned = true;
|
|
715
1394
|
}
|
|
716
|
-
if (!
|
|
717
|
-
|
|
718
|
-
gitMetaCreated = await createGitMetaArchive(gitCommonDir, gitMetaPath, gitMetaListPath);
|
|
1395
|
+
if (!initState?.steps.setupExternalScanned) {
|
|
1396
|
+
backfillStepUpdate.setupExternalScanned = true;
|
|
719
1397
|
}
|
|
720
|
-
if (
|
|
721
|
-
|
|
722
|
-
stagedPatchCreated = await writePatch(repoRoot, ["diff", "--binary", "--cached"], stagedPatchPath);
|
|
723
|
-
unstagedPatchCreated = await writePatch(repoRoot, ["diff", "--binary"], unstagedPatchPath);
|
|
724
|
-
untrackedCreated = await createFileListArchive(repoRoot, worktreeState.untracked, untrackedPath, untrackedListPath);
|
|
1398
|
+
if (!initState?.steps.setupExtraArtifactsScanned) {
|
|
1399
|
+
backfillStepUpdate.setupExtraArtifactsScanned = true;
|
|
725
1400
|
}
|
|
726
|
-
if (!
|
|
727
|
-
|
|
728
|
-
const bundleData = await fs.readFile(bundlePath);
|
|
729
|
-
await client.writeFile(canonical, remoteBundlePath, bundleData);
|
|
1401
|
+
if (!initState?.steps.setupPlanScanned) {
|
|
1402
|
+
backfillStepUpdate.setupPlanScanned = true;
|
|
730
1403
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
1404
|
+
}
|
|
1405
|
+
if (approvedServices && !initState?.steps.servicesPlanScanned) {
|
|
1406
|
+
backfillStepUpdate.servicesPlanScanned = true;
|
|
1407
|
+
}
|
|
1408
|
+
if (Object.keys(backfillStepUpdate).length > 0) {
|
|
1409
|
+
await updateInitState({ steps: backfillStepUpdate });
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
if (nonInteractive) {
|
|
1413
|
+
if (setupPlan)
|
|
1414
|
+
approvedPlan = setupPlan;
|
|
1415
|
+
if (servicesPlan)
|
|
1416
|
+
approvedServices = servicesPlan;
|
|
1417
|
+
}
|
|
1418
|
+
else if (setupPlan || servicesPlan) {
|
|
1419
|
+
if (!process.stdin.isTTY || parsed.json) {
|
|
1420
|
+
throw new Error("Interactive terminal required to approve setup.");
|
|
1421
|
+
}
|
|
1422
|
+
const statCache = new Map();
|
|
1423
|
+
const readPathInfo = async (candidatePath) => {
|
|
1424
|
+
const cached = statCache.get(candidatePath);
|
|
1425
|
+
if (cached)
|
|
1426
|
+
return cached;
|
|
1427
|
+
const resolved = path.isAbsolute(candidatePath)
|
|
1428
|
+
? candidatePath
|
|
1429
|
+
: path.resolve(repoRoot, candidatePath);
|
|
1430
|
+
const relative = toRepoRelativePath(repoRoot, candidatePath);
|
|
1431
|
+
const outsideRepo = isOutsideRepoPath(relative);
|
|
1432
|
+
let isDirectory = false;
|
|
1433
|
+
try {
|
|
1434
|
+
const stat = await fs.stat(resolved);
|
|
1435
|
+
isDirectory = stat.isDirectory();
|
|
735
1436
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
await client.writeFile(canonical, remoteStagedPatchPath, await fs.readFile(stagedPatchPath));
|
|
1437
|
+
catch {
|
|
1438
|
+
isDirectory = false;
|
|
739
1439
|
}
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
1440
|
+
const info = { relative, outsideRepo, isDirectory };
|
|
1441
|
+
statCache.set(candidatePath, info);
|
|
1442
|
+
return info;
|
|
1443
|
+
};
|
|
1444
|
+
const buildApprovalSummary = async ({ setup, services, }) => {
|
|
1445
|
+
const lines = [];
|
|
1446
|
+
lines.push("Setup");
|
|
1447
|
+
lines.push(`- .env files: ${setup.envFiles.length}`);
|
|
1448
|
+
for (const entry of setup.envFiles) {
|
|
1449
|
+
lines.push(` - ${toRepoRelativePath(repoRoot, entry.path)}`);
|
|
743
1450
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
1451
|
+
lines.push(`- Secret/config files: ${setup.secretFiles.length}`);
|
|
1452
|
+
for (const entry of setup.secretFiles) {
|
|
1453
|
+
lines.push(` - ${toRepoRelativePath(repoRoot, entry.path)}`);
|
|
747
1454
|
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
1455
|
+
lines.push(`- Other artifacts: ${setup.extraArtifacts.length}`);
|
|
1456
|
+
for (const entry of setup.extraArtifacts) {
|
|
1457
|
+
lines.push(` - ${toRepoRelativePath(repoRoot, entry.path)}`);
|
|
1458
|
+
}
|
|
1459
|
+
lines.push(`- External dependencies: ${setup.externalDependencies.length}`);
|
|
1460
|
+
for (const entry of setup.externalDependencies) {
|
|
1461
|
+
lines.push(` - ${entry.version ? `${entry.name}@${entry.version}` : entry.name}`);
|
|
1462
|
+
}
|
|
1463
|
+
lines.push(`- External config/secret files: ${setup.externalConfigs.length}`);
|
|
1464
|
+
for (const entry of setup.externalConfigs) {
|
|
1465
|
+
lines.push(` - ${toRepoRelativePath(repoRoot, entry.path)}`);
|
|
1466
|
+
}
|
|
1467
|
+
lines.push("");
|
|
1468
|
+
lines.push("Services");
|
|
1469
|
+
lines.push(`- App entrypoints: ${services.appEntrypoints.length}`);
|
|
1470
|
+
for (const entry of services.appEntrypoints) {
|
|
1471
|
+
lines.push(` - ${entry.command}`);
|
|
1472
|
+
}
|
|
1473
|
+
lines.push(`- Background services: ${services.backgroundServices.length}`);
|
|
1474
|
+
for (const entry of services.backgroundServices) {
|
|
1475
|
+
lines.push(` - ${entry.name}`);
|
|
1476
|
+
}
|
|
1477
|
+
const candidatePaths = new Set();
|
|
1478
|
+
for (const entry of [
|
|
1479
|
+
...setup.envFiles,
|
|
1480
|
+
...setup.secretFiles,
|
|
1481
|
+
...setup.extraArtifacts,
|
|
1482
|
+
...setup.externalConfigs,
|
|
1483
|
+
]) {
|
|
1484
|
+
candidatePaths.add(entry.path);
|
|
1485
|
+
}
|
|
1486
|
+
const outsideRepoPaths = [];
|
|
1487
|
+
const directoryPaths = [];
|
|
1488
|
+
for (const candidatePath of candidatePaths) {
|
|
1489
|
+
const info = await readPathInfo(candidatePath);
|
|
1490
|
+
if (info.outsideRepo)
|
|
1491
|
+
outsideRepoPaths.push(info.relative);
|
|
1492
|
+
if (info.isDirectory)
|
|
1493
|
+
directoryPaths.push(info.relative);
|
|
1494
|
+
}
|
|
1495
|
+
outsideRepoPaths.sort();
|
|
1496
|
+
directoryPaths.sort();
|
|
1497
|
+
const hasRisk = outsideRepoPaths.length > 0 || directoryPaths.length > 0;
|
|
1498
|
+
const warningLines = [];
|
|
1499
|
+
if (outsideRepoPaths.length > 0) {
|
|
1500
|
+
warningLines.push("Outside repo:");
|
|
1501
|
+
for (const entry of outsideRepoPaths) {
|
|
1502
|
+
warningLines.push(`- ${entry}`);
|
|
753
1503
|
}
|
|
1504
|
+
warningLines.push("");
|
|
754
1505
|
}
|
|
755
|
-
if (
|
|
756
|
-
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
: `git checkout --detach ${shellQuote(headState?.commit ?? "")}`;
|
|
760
|
-
status.stage("Provisioning workdir");
|
|
761
|
-
const remoteCommand = [
|
|
762
|
-
"set -euo pipefail",
|
|
763
|
-
"unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE",
|
|
764
|
-
`if [ -d ${shellQuote(expandedWorkdir)} ]; then`,
|
|
765
|
-
parsed.force
|
|
766
|
-
? ` mv ${shellQuote(expandedWorkdir)} ${shellQuote(backup)}`
|
|
767
|
-
: ` echo "Target exists: ${expandedWorkdir}" >&2; exit 1`,
|
|
768
|
-
"fi",
|
|
769
|
-
`mkdir -p ${shellQuote(path.dirname(expandedWorkdir))}`,
|
|
770
|
-
`git init -b devbox-init ${shellQuote(expandedWorkdir)}`,
|
|
771
|
-
`cd ${shellQuote(expandedWorkdir)}`,
|
|
772
|
-
`git fetch ${shellQuote(remoteBundlePath)} 'refs/*:refs/*'`,
|
|
773
|
-
`if [ -f ${shellQuote(remoteGitMetaPath)} ]; then`,
|
|
774
|
-
` tar -xzf ${shellQuote(remoteGitMetaPath)} -C .git`,
|
|
775
|
-
"fi",
|
|
776
|
-
checkoutCommand,
|
|
777
|
-
`if [ -f ${shellQuote(remoteStagedPatchPath)} ]; then`,
|
|
778
|
-
` git apply --index ${shellQuote(remoteStagedPatchPath)}`,
|
|
779
|
-
"fi",
|
|
780
|
-
`if [ -f ${shellQuote(remoteUnstagedPatchPath)} ]; then`,
|
|
781
|
-
` git apply ${shellQuote(remoteUnstagedPatchPath)}`,
|
|
782
|
-
"fi",
|
|
783
|
-
`if [ -f ${shellQuote(remoteUntrackedPath)} ]; then`,
|
|
784
|
-
` tar -xzf ${shellQuote(remoteUntrackedPath)} -C .`,
|
|
785
|
-
"fi",
|
|
786
|
-
].join("\n");
|
|
787
|
-
const execResult = await client.exec(canonical, [
|
|
788
|
-
"/bin/bash",
|
|
789
|
-
"--noprofile",
|
|
790
|
-
"--norc",
|
|
791
|
-
"-e",
|
|
792
|
-
"-u",
|
|
793
|
-
"-o",
|
|
794
|
-
"pipefail",
|
|
795
|
-
"-c",
|
|
796
|
-
remoteCommand,
|
|
797
|
-
]);
|
|
798
|
-
if (execResult.exitCode !== 0) {
|
|
799
|
-
throw new Error(execResult.stderr || "Remote init failed");
|
|
1506
|
+
if (directoryPaths.length > 0) {
|
|
1507
|
+
warningLines.push("Directories:");
|
|
1508
|
+
for (const entry of directoryPaths) {
|
|
1509
|
+
warningLines.push(`- ${entry}`);
|
|
800
1510
|
}
|
|
801
|
-
await updateInitState({ steps: { workdirProvisioned: true } });
|
|
802
1511
|
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
1512
|
+
return {
|
|
1513
|
+
summary: lines.join("\n"),
|
|
1514
|
+
warning: warningLines.length > 0 ? warningLines.join("\n") : null,
|
|
1515
|
+
hasRisk,
|
|
1516
|
+
};
|
|
1517
|
+
};
|
|
1518
|
+
let draftSetup = setupPlan;
|
|
1519
|
+
let draftServices = servicesPlan;
|
|
1520
|
+
while (true) {
|
|
1521
|
+
const nextSetup = skipSetupPlan
|
|
1522
|
+
? approvedPlan
|
|
1523
|
+
: await promptForPlanApproval({
|
|
1524
|
+
plan: setupPlan,
|
|
1525
|
+
repoRoot,
|
|
1526
|
+
initialPlan: draftSetup,
|
|
1527
|
+
});
|
|
1528
|
+
const nextServices = skipServicesPlan
|
|
1529
|
+
? approvedServices
|
|
1530
|
+
: await promptForServicesApproval({
|
|
1531
|
+
plan: servicesPlan,
|
|
1532
|
+
initialPlan: draftServices,
|
|
1533
|
+
});
|
|
1534
|
+
if (!nextSetup) {
|
|
1535
|
+
throw new Error("Setup plan missing.");
|
|
814
1536
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
"archive",
|
|
818
|
-
"--format=tar.gz",
|
|
819
|
-
"-o",
|
|
820
|
-
archivePath,
|
|
821
|
-
"HEAD",
|
|
822
|
-
]);
|
|
1537
|
+
if (!nextServices) {
|
|
1538
|
+
throw new Error("Services plan missing.");
|
|
823
1539
|
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
}
|
|
1540
|
+
const { summary, warning, hasRisk } = await buildApprovalSummary({
|
|
1541
|
+
setup: nextSetup,
|
|
1542
|
+
services: nextServices,
|
|
1543
|
+
});
|
|
1544
|
+
clackNote(summary, "Selected setup requirements");
|
|
1545
|
+
if (warning) {
|
|
1546
|
+
clackNote(warning, "Special attention");
|
|
832
1547
|
}
|
|
833
|
-
const
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
"--norc",
|
|
850
|
-
"-e",
|
|
851
|
-
"-u",
|
|
852
|
-
"-o",
|
|
853
|
-
"pipefail",
|
|
854
|
-
"-c",
|
|
855
|
-
remoteCommand,
|
|
856
|
-
]);
|
|
857
|
-
if (execResult.exitCode !== 0) {
|
|
858
|
-
throw new Error(execResult.stderr || "Remote init failed");
|
|
1548
|
+
const decision = await clackSelect({
|
|
1549
|
+
message: "Proceed with these selections?",
|
|
1550
|
+
options: [
|
|
1551
|
+
{ value: "proceed", label: "Proceed" },
|
|
1552
|
+
{ value: "edit", label: "Edit selections" },
|
|
1553
|
+
{ value: "cancel", label: "Cancel" },
|
|
1554
|
+
],
|
|
1555
|
+
initialValue: "proceed",
|
|
1556
|
+
});
|
|
1557
|
+
if (isCancel(decision) || decision === "cancel") {
|
|
1558
|
+
throwInitCanceled();
|
|
1559
|
+
}
|
|
1560
|
+
if (decision === "edit") {
|
|
1561
|
+
draftSetup = nextSetup;
|
|
1562
|
+
draftServices = nextServices;
|
|
1563
|
+
continue;
|
|
859
1564
|
}
|
|
860
|
-
|
|
1565
|
+
if (hasRisk) {
|
|
1566
|
+
const confirmed = await clackConfirm({
|
|
1567
|
+
message: "You selected items outside the repo and/or directories. Continue?",
|
|
1568
|
+
active: "Continue",
|
|
1569
|
+
inactive: "Edit selections",
|
|
1570
|
+
initialValue: true,
|
|
1571
|
+
});
|
|
1572
|
+
if (isCancel(confirmed)) {
|
|
1573
|
+
throwInitCanceled();
|
|
1574
|
+
}
|
|
1575
|
+
if (!confirmed) {
|
|
1576
|
+
draftSetup = nextSetup;
|
|
1577
|
+
draftServices = nextServices;
|
|
1578
|
+
continue;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
approvedPlan = nextSetup;
|
|
1582
|
+
approvedServices = nextServices;
|
|
1583
|
+
break;
|
|
861
1584
|
}
|
|
862
|
-
|
|
1585
|
+
}
|
|
1586
|
+
if (!approvedPlan) {
|
|
1587
|
+
throw new Error("Setup plan missing.");
|
|
1588
|
+
}
|
|
1589
|
+
if (!approvedServices) {
|
|
1590
|
+
throw new Error("Services plan missing.");
|
|
1591
|
+
}
|
|
1592
|
+
const ensuredPlan = approvedPlan;
|
|
1593
|
+
if (!skipSetupPlan) {
|
|
1594
|
+
await writeSetupPlan(setupPath, ensuredPlan);
|
|
1595
|
+
await updateInitState({ steps: { setupPlanWritten: true } });
|
|
1596
|
+
}
|
|
1597
|
+
if (!skipServicesPlan) {
|
|
1598
|
+
await writeServicesPlan(servicesPath, approvedServices);
|
|
1599
|
+
await updateInitState({ steps: { servicesPlanWritten: true } });
|
|
1600
|
+
}
|
|
1601
|
+
if (!skipSetupUpload) {
|
|
1602
|
+
setupArtifacts = await runInitStep({
|
|
1603
|
+
enabled: progressEnabled,
|
|
1604
|
+
title: "Packaging setup artifacts",
|
|
1605
|
+
fn: async () => await createSetupArtifacts({
|
|
1606
|
+
repoRoot,
|
|
1607
|
+
plan: ensuredPlan,
|
|
1608
|
+
outputDir: setupDir,
|
|
1609
|
+
tempDir: setupTempDir,
|
|
1610
|
+
homeDir: localHomeDir,
|
|
1611
|
+
}),
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
finally {
|
|
1616
|
+
await fs.rm(setupTempDir, { recursive: true, force: true });
|
|
1617
|
+
}
|
|
1618
|
+
const skipProvision = shouldResume && initState?.steps.workdirProvisioned;
|
|
1619
|
+
if (skipProvision && !skipSetupUpload) {
|
|
1620
|
+
await runInitStep({
|
|
1621
|
+
enabled: progressEnabled,
|
|
1622
|
+
title: "Uploading setup plan",
|
|
1623
|
+
fn: async ({ status }) => {
|
|
863
1624
|
await uploadSetupPlan({
|
|
864
1625
|
client,
|
|
865
1626
|
canonical,
|
|
@@ -875,6 +1636,287 @@ export const runInit = async (args) => {
|
|
|
875
1636
|
: null,
|
|
876
1637
|
status,
|
|
877
1638
|
});
|
|
1639
|
+
},
|
|
1640
|
+
});
|
|
1641
|
+
await updateInitState({ steps: { setupUploaded: true } });
|
|
1642
|
+
await updateRegistryProjectStatus(resolveInitStatus(initState?.steps, initState?.complete));
|
|
1643
|
+
}
|
|
1644
|
+
else if (!skipProvision || !skipSetupUpload) {
|
|
1645
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "devbox-init-"));
|
|
1646
|
+
const bundlePath = path.join(tempDir, "repo.bundle");
|
|
1647
|
+
const archivePath = path.join(tempDir, "repo.tgz");
|
|
1648
|
+
const gitMetaPath = path.join(tempDir, "git-meta.tgz");
|
|
1649
|
+
const gitMetaListPath = path.join(tempDir, "git-meta.list");
|
|
1650
|
+
const stagedPatchPath = path.join(tempDir, "staged.patch");
|
|
1651
|
+
const unstagedPatchPath = path.join(tempDir, "unstaged.patch");
|
|
1652
|
+
const untrackedPath = path.join(tempDir, "untracked.tgz");
|
|
1653
|
+
const untrackedListPath = path.join(tempDir, "untracked.list");
|
|
1654
|
+
const globalGitConfigSources = await readGlobalGitConfigFiles(repoRoot);
|
|
1655
|
+
const globalGitConfigMappings = mapGlobalGitConfigDestinations(globalGitConfigSources, localHomeDir);
|
|
1656
|
+
try {
|
|
1657
|
+
const remoteBundlePath = "/home/sprite/.devbox/upload.bundle";
|
|
1658
|
+
const remoteArchivePath = "/home/sprite/.devbox/upload.tgz";
|
|
1659
|
+
const remoteGitMetaPath = "/home/sprite/.devbox/git-meta.tgz";
|
|
1660
|
+
const remoteStagedPatchPath = "/home/sprite/.devbox/staged.patch";
|
|
1661
|
+
const remoteUnstagedPatchPath = "/home/sprite/.devbox/unstaged.patch";
|
|
1662
|
+
const remoteUntrackedPath = "/home/sprite/.devbox/untracked.tgz";
|
|
1663
|
+
const remoteHasGit = await runInitStep({
|
|
1664
|
+
enabled: progressEnabled,
|
|
1665
|
+
title: "Preparing remote directories",
|
|
1666
|
+
fn: async ({ status }) => {
|
|
1667
|
+
status.stage("Checking remote git");
|
|
1668
|
+
const gitCheck = await client.exec(canonical, [
|
|
1669
|
+
"/bin/bash",
|
|
1670
|
+
"-lc",
|
|
1671
|
+
"git --version",
|
|
1672
|
+
]);
|
|
1673
|
+
const remoteHasGit = gitCheck.exitCode === 0;
|
|
1674
|
+
const remoteDirs = new Set();
|
|
1675
|
+
remoteDirs.add(path.posix.dirname(remoteBundlePath));
|
|
1676
|
+
remoteDirs.add(path.posix.dirname(remoteArchivePath));
|
|
1677
|
+
remoteDirs.add(path.posix.dirname(remoteGitMetaPath));
|
|
1678
|
+
remoteDirs.add(path.posix.dirname(remoteStagedPatchPath));
|
|
1679
|
+
remoteDirs.add(path.posix.dirname(remoteUnstagedPatchPath));
|
|
1680
|
+
remoteDirs.add(path.posix.dirname(remoteUntrackedPath));
|
|
1681
|
+
for (const mapping of globalGitConfigMappings) {
|
|
1682
|
+
remoteDirs.add(path.posix.dirname(mapping.dest));
|
|
1683
|
+
}
|
|
1684
|
+
if (remoteDirs.size > 0) {
|
|
1685
|
+
status.stage("Preparing remote directories");
|
|
1686
|
+
const prepResult = await client.exec(canonical, [
|
|
1687
|
+
"/bin/bash",
|
|
1688
|
+
"-lc",
|
|
1689
|
+
`mkdir -p ${[...remoteDirs].map(shellQuote).join(" ")}`,
|
|
1690
|
+
]);
|
|
1691
|
+
if (prepResult.exitCode !== 0) {
|
|
1692
|
+
throw new Error(prepResult.stderr || "Failed to prepare remote dirs");
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
return remoteHasGit;
|
|
1696
|
+
},
|
|
1697
|
+
});
|
|
1698
|
+
const { headState, gitCommonDir, worktreeState, copyWorktree } = await runInitStep({
|
|
1699
|
+
enabled: progressEnabled,
|
|
1700
|
+
title: "Inspecting repo state",
|
|
1701
|
+
fn: async ({ status }) => {
|
|
1702
|
+
const headState = await readHeadState(repoRoot);
|
|
1703
|
+
const resolved = await resolveGitCommonDir(repoRoot);
|
|
1704
|
+
const worktreeState = await readWorktreeState(repoRoot);
|
|
1705
|
+
const hasWorktreeChanges = worktreeState.staged.length > 0 ||
|
|
1706
|
+
worktreeState.unstaged.length > 0 ||
|
|
1707
|
+
worktreeState.untracked.length > 0;
|
|
1708
|
+
let copyWorktree = false;
|
|
1709
|
+
if (hasWorktreeChanges && process.stdin.isTTY && !parsed.json) {
|
|
1710
|
+
status.stop();
|
|
1711
|
+
copyWorktree = await confirmCopyWorktree(worktreeState);
|
|
1712
|
+
status.stage("Inspecting repo state");
|
|
1713
|
+
}
|
|
1714
|
+
else if (hasWorktreeChanges) {
|
|
1715
|
+
copyWorktree = true;
|
|
1716
|
+
}
|
|
1717
|
+
return {
|
|
1718
|
+
headState,
|
|
1719
|
+
gitCommonDir: resolved.commonDir,
|
|
1720
|
+
worktreeState,
|
|
1721
|
+
copyWorktree,
|
|
1722
|
+
};
|
|
1723
|
+
},
|
|
1724
|
+
});
|
|
1725
|
+
const packaged = await runInitStep({
|
|
1726
|
+
enabled: progressEnabled,
|
|
1727
|
+
title: "Packaging repo",
|
|
1728
|
+
fn: async ({ status }) => {
|
|
1729
|
+
let gitMetaCreated = false;
|
|
1730
|
+
let stagedPatchCreated = false;
|
|
1731
|
+
let unstagedPatchCreated = false;
|
|
1732
|
+
let untrackedCreated = false;
|
|
1733
|
+
if (remoteHasGit) {
|
|
1734
|
+
status.stage("Packaging repo bundle");
|
|
1735
|
+
await runCommand(repoRoot, "git", [
|
|
1736
|
+
"bundle",
|
|
1737
|
+
"create",
|
|
1738
|
+
bundlePath,
|
|
1739
|
+
"--all",
|
|
1740
|
+
]);
|
|
1741
|
+
status.stage("Packaging git metadata");
|
|
1742
|
+
gitMetaCreated = await createGitMetaArchive(gitCommonDir, gitMetaPath, gitMetaListPath);
|
|
1743
|
+
if (copyWorktree) {
|
|
1744
|
+
status.stage("Packaging repo changes");
|
|
1745
|
+
stagedPatchCreated = await writePatch(repoRoot, ["diff", "--binary", "--cached"], stagedPatchPath);
|
|
1746
|
+
unstagedPatchCreated = await writePatch(repoRoot, ["diff", "--binary"], unstagedPatchPath);
|
|
1747
|
+
untrackedCreated = await createFileListArchive(repoRoot, worktreeState.untracked, untrackedPath, untrackedListPath);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
else {
|
|
1751
|
+
status.stage("Packaging repo archive");
|
|
1752
|
+
if (copyWorktree) {
|
|
1753
|
+
const workingFiles = await readNullSeparatedPaths(repoRoot, [
|
|
1754
|
+
"ls-files",
|
|
1755
|
+
"--cached",
|
|
1756
|
+
"--others",
|
|
1757
|
+
"--exclude-standard",
|
|
1758
|
+
]);
|
|
1759
|
+
await createFileListArchive(repoRoot, workingFiles, archivePath, untrackedListPath);
|
|
1760
|
+
}
|
|
1761
|
+
else {
|
|
1762
|
+
await runCommand(repoRoot, "git", [
|
|
1763
|
+
"archive",
|
|
1764
|
+
"--format=tar.gz",
|
|
1765
|
+
"-o",
|
|
1766
|
+
archivePath,
|
|
1767
|
+
"HEAD",
|
|
1768
|
+
]);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
return {
|
|
1772
|
+
gitMetaCreated,
|
|
1773
|
+
stagedPatchCreated,
|
|
1774
|
+
unstagedPatchCreated,
|
|
1775
|
+
untrackedCreated,
|
|
1776
|
+
};
|
|
1777
|
+
},
|
|
1778
|
+
});
|
|
1779
|
+
await runInitStep({
|
|
1780
|
+
enabled: progressEnabled,
|
|
1781
|
+
title: "Uploading repo",
|
|
1782
|
+
fn: async ({ status }) => {
|
|
1783
|
+
if (remoteHasGit) {
|
|
1784
|
+
status.stage("Uploading repo bundle");
|
|
1785
|
+
const bundleData = await fs.readFile(bundlePath);
|
|
1786
|
+
await client.writeFile(canonical, remoteBundlePath, bundleData);
|
|
1787
|
+
if (packaged.gitMetaCreated) {
|
|
1788
|
+
status.stage("Uploading git metadata");
|
|
1789
|
+
const gitMetaData = await fs.readFile(gitMetaPath);
|
|
1790
|
+
await client.writeFile(canonical, remoteGitMetaPath, gitMetaData);
|
|
1791
|
+
}
|
|
1792
|
+
if (packaged.stagedPatchCreated) {
|
|
1793
|
+
status.stage("Uploading staged changes");
|
|
1794
|
+
await client.writeFile(canonical, remoteStagedPatchPath, await fs.readFile(stagedPatchPath));
|
|
1795
|
+
}
|
|
1796
|
+
if (packaged.unstagedPatchCreated) {
|
|
1797
|
+
status.stage("Uploading unstaged changes");
|
|
1798
|
+
await client.writeFile(canonical, remoteUnstagedPatchPath, await fs.readFile(unstagedPatchPath));
|
|
1799
|
+
}
|
|
1800
|
+
if (packaged.untrackedCreated) {
|
|
1801
|
+
status.stage("Uploading untracked files");
|
|
1802
|
+
await client.writeFile(canonical, remoteUntrackedPath, await fs.readFile(untrackedPath));
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
else {
|
|
1806
|
+
status.stage("Uploading repo archive");
|
|
1807
|
+
await client.writeFile(canonical, remoteArchivePath, await fs.readFile(archivePath));
|
|
1808
|
+
}
|
|
1809
|
+
if (globalGitConfigMappings.length > 0) {
|
|
1810
|
+
status.stage("Uploading git config");
|
|
1811
|
+
for (const mapping of globalGitConfigMappings) {
|
|
1812
|
+
const data = await fs.readFile(mapping.source);
|
|
1813
|
+
await client.writeFile(canonical, mapping.dest, data);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
},
|
|
1817
|
+
});
|
|
1818
|
+
await runInitStep({
|
|
1819
|
+
enabled: progressEnabled,
|
|
1820
|
+
title: "Provisioning workdir",
|
|
1821
|
+
fn: async () => {
|
|
1822
|
+
const backup = `${expandedWorkdir}.bak-${Date.now()}`;
|
|
1823
|
+
if (remoteHasGit) {
|
|
1824
|
+
const checkoutCommand = headState.branch
|
|
1825
|
+
? `git checkout -B ${shellQuote(headState.branch)} ${shellQuote(headState.commit)}`
|
|
1826
|
+
: `git checkout --detach ${shellQuote(headState.commit)}`;
|
|
1827
|
+
const remoteCommand = [
|
|
1828
|
+
"set -euo pipefail",
|
|
1829
|
+
"unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE",
|
|
1830
|
+
`if [ -d ${shellQuote(expandedWorkdir)} ]; then`,
|
|
1831
|
+
parsed.force
|
|
1832
|
+
? ` mv ${shellQuote(expandedWorkdir)} ${shellQuote(backup)}`
|
|
1833
|
+
: ` echo "Target exists: ${expandedWorkdir}" >&2; exit 1`,
|
|
1834
|
+
"fi",
|
|
1835
|
+
`mkdir -p ${shellQuote(path.dirname(expandedWorkdir))}`,
|
|
1836
|
+
`git init -b devbox-init ${shellQuote(expandedWorkdir)}`,
|
|
1837
|
+
`cd ${shellQuote(expandedWorkdir)}`,
|
|
1838
|
+
`git fetch ${shellQuote(remoteBundlePath)} 'refs/*:refs/*'`,
|
|
1839
|
+
`if [ -f ${shellQuote(remoteGitMetaPath)} ]; then`,
|
|
1840
|
+
` tar -xzf ${shellQuote(remoteGitMetaPath)} -C .git`,
|
|
1841
|
+
"fi",
|
|
1842
|
+
checkoutCommand,
|
|
1843
|
+
`if [ -f ${shellQuote(remoteStagedPatchPath)} ]; then`,
|
|
1844
|
+
` git apply --index ${shellQuote(remoteStagedPatchPath)}`,
|
|
1845
|
+
"fi",
|
|
1846
|
+
`if [ -f ${shellQuote(remoteUnstagedPatchPath)} ]; then`,
|
|
1847
|
+
` git apply ${shellQuote(remoteUnstagedPatchPath)}`,
|
|
1848
|
+
"fi",
|
|
1849
|
+
`if [ -f ${shellQuote(remoteUntrackedPath)} ]; then`,
|
|
1850
|
+
` tar -xzf ${shellQuote(remoteUntrackedPath)} -C .`,
|
|
1851
|
+
"fi",
|
|
1852
|
+
].join("\n");
|
|
1853
|
+
const execResult = await client.exec(canonical, [
|
|
1854
|
+
"/bin/bash",
|
|
1855
|
+
"--noprofile",
|
|
1856
|
+
"--norc",
|
|
1857
|
+
"-e",
|
|
1858
|
+
"-u",
|
|
1859
|
+
"-o",
|
|
1860
|
+
"pipefail",
|
|
1861
|
+
"-c",
|
|
1862
|
+
remoteCommand,
|
|
1863
|
+
]);
|
|
1864
|
+
if (execResult.exitCode !== 0) {
|
|
1865
|
+
throw new Error(execResult.stderr || "Remote init failed");
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
else {
|
|
1869
|
+
const remoteCommand = [
|
|
1870
|
+
"set -euo pipefail",
|
|
1871
|
+
"unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE",
|
|
1872
|
+
`if [ -d ${shellQuote(expandedWorkdir)} ]; then`,
|
|
1873
|
+
parsed.force
|
|
1874
|
+
? ` mv ${shellQuote(expandedWorkdir)} ${shellQuote(backup)}`
|
|
1875
|
+
: ` echo "Target exists: ${expandedWorkdir}" >&2; exit 1`,
|
|
1876
|
+
"fi",
|
|
1877
|
+
`mkdir -p ${shellQuote(expandedWorkdir)}`,
|
|
1878
|
+
`tar -xzf ${shellQuote(remoteArchivePath)} -C ${shellQuote(expandedWorkdir)}`,
|
|
1879
|
+
].join("\n");
|
|
1880
|
+
const execResult = await client.exec(canonical, [
|
|
1881
|
+
"/bin/bash",
|
|
1882
|
+
"--noprofile",
|
|
1883
|
+
"--norc",
|
|
1884
|
+
"-e",
|
|
1885
|
+
"-u",
|
|
1886
|
+
"-o",
|
|
1887
|
+
"pipefail",
|
|
1888
|
+
"-c",
|
|
1889
|
+
remoteCommand,
|
|
1890
|
+
]);
|
|
1891
|
+
if (execResult.exitCode !== 0) {
|
|
1892
|
+
throw new Error(execResult.stderr || "Remote init failed");
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
await updateInitState({ steps: { workdirProvisioned: true } });
|
|
1896
|
+
},
|
|
1897
|
+
});
|
|
1898
|
+
if (!skipSetupUpload) {
|
|
1899
|
+
await runInitStep({
|
|
1900
|
+
enabled: progressEnabled,
|
|
1901
|
+
title: "Uploading setup plan",
|
|
1902
|
+
fn: async ({ status }) => {
|
|
1903
|
+
await uploadSetupPlan({
|
|
1904
|
+
client,
|
|
1905
|
+
canonical,
|
|
1906
|
+
localSetupPath: setupPath,
|
|
1907
|
+
remoteSetupPath,
|
|
1908
|
+
localArtifactsBundlePath: setupArtifacts?.bundlePath ?? null,
|
|
1909
|
+
localArtifactsManifestPath: setupArtifacts?.manifestPath ?? null,
|
|
1910
|
+
remoteArtifactsBundlePath: setupArtifacts
|
|
1911
|
+
? path.posix.join(expandedWorkdir, ".devbox", "setup-artifacts.tgz")
|
|
1912
|
+
: null,
|
|
1913
|
+
remoteArtifactsManifestPath: setupArtifacts
|
|
1914
|
+
? path.posix.join(expandedWorkdir, ".devbox", "setup-artifacts.json")
|
|
1915
|
+
: null,
|
|
1916
|
+
status,
|
|
1917
|
+
});
|
|
1918
|
+
},
|
|
1919
|
+
});
|
|
878
1920
|
await updateInitState({ steps: { setupUploaded: true } });
|
|
879
1921
|
await updateRegistryProjectStatus(resolveInitStatus(initState?.steps, initState?.complete));
|
|
880
1922
|
}
|
|
@@ -883,165 +1925,280 @@ export const runInit = async (args) => {
|
|
|
883
1925
|
await fs.rm(tempDir, { recursive: true, force: true });
|
|
884
1926
|
}
|
|
885
1927
|
}
|
|
886
|
-
await
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1928
|
+
await runInitStep({
|
|
1929
|
+
enabled: progressEnabled,
|
|
1930
|
+
title: "Ensuring workdir ownership",
|
|
1931
|
+
fn: async ({ status }) => {
|
|
1932
|
+
await ensureWorkdirOwnership({
|
|
1933
|
+
client,
|
|
1934
|
+
canonical,
|
|
1935
|
+
workdir: expandedWorkdir,
|
|
1936
|
+
status,
|
|
1937
|
+
json: parsed.json === true,
|
|
1938
|
+
});
|
|
1939
|
+
},
|
|
892
1940
|
});
|
|
1941
|
+
const skipGitSafeDirectory = shouldResume && initState?.steps.gitSafeDirectoryConfigured;
|
|
1942
|
+
if (!skipGitSafeDirectory) {
|
|
1943
|
+
await runInitStep({
|
|
1944
|
+
enabled: progressEnabled,
|
|
1945
|
+
title: "Configuring git safe.directory",
|
|
1946
|
+
fn: async ({ status }) => {
|
|
1947
|
+
await ensureGitSafeDirectory({
|
|
1948
|
+
client,
|
|
1949
|
+
canonical,
|
|
1950
|
+
workdir: expandedWorkdir,
|
|
1951
|
+
status,
|
|
1952
|
+
});
|
|
1953
|
+
await updateInitState({ steps: { gitSafeDirectoryConfigured: true } });
|
|
1954
|
+
},
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
893
1957
|
const skipSshdConfig = shouldResume && initState?.steps.sshdConfigured;
|
|
894
1958
|
if (!skipSshdConfig) {
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1959
|
+
await runInitStep({
|
|
1960
|
+
enabled: progressEnabled,
|
|
1961
|
+
title: "Configuring SSH mount access",
|
|
1962
|
+
fn: async ({ fail }) => {
|
|
1963
|
+
try {
|
|
1964
|
+
await ensureLocalMountKey();
|
|
1965
|
+
await ensureKnownHostsFile();
|
|
1966
|
+
const publicKey = await readLocalMountPublicKey();
|
|
1967
|
+
await ensureRemoteMountAccess(client, canonical, publicKey);
|
|
1968
|
+
await ensureSshdService(client, canonical);
|
|
1969
|
+
await updateInitState({ steps: { sshdConfigured: true } });
|
|
1970
|
+
}
|
|
1971
|
+
catch (error) {
|
|
1972
|
+
logger.warn("sshd_config_failed", {
|
|
1973
|
+
box: canonical,
|
|
1974
|
+
error: String(error),
|
|
1975
|
+
});
|
|
1976
|
+
fail("Configuring SSH mount access (failed)");
|
|
1977
|
+
if (!parsed.json) {
|
|
1978
|
+
const message = error instanceof Error && error.message
|
|
1979
|
+
? error.message
|
|
1980
|
+
: String(error);
|
|
1981
|
+
console.warn(`Warning: failed to configure SSH mount access. ${message}`);
|
|
1982
|
+
console.warn(`To retry later: dvb mount ${alias}`);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
},
|
|
1986
|
+
});
|
|
902
1987
|
}
|
|
903
1988
|
const skipSshAuth = nonInteractive ||
|
|
904
1989
|
(shouldResume && initState?.steps.sshAuthConfigured);
|
|
905
1990
|
if (!skipSshAuth) {
|
|
906
|
-
|
|
907
|
-
|
|
1991
|
+
const { remoteOrigin, remoteInfo } = await runInitStep({
|
|
1992
|
+
enabled: progressEnabled,
|
|
1993
|
+
title: "Checking git remote for SSH auth",
|
|
1994
|
+
fn: async () => {
|
|
1995
|
+
const remoteOrigin = await readRemoteOrigin(client, canonical, expandedWorkdir);
|
|
1996
|
+
const remoteInfo = remoteOrigin ? parseGitRemote(remoteOrigin) : null;
|
|
1997
|
+
return { remoteOrigin, remoteInfo };
|
|
1998
|
+
},
|
|
1999
|
+
});
|
|
908
2000
|
if (!remoteOrigin) {
|
|
909
2001
|
if (!parsed.json) {
|
|
910
|
-
status.stop();
|
|
911
2002
|
console.warn("Warning: unable to detect remote origin on sprite. Skipping SSH setup.");
|
|
912
2003
|
}
|
|
913
2004
|
}
|
|
2005
|
+
else if (!remoteInfo) {
|
|
2006
|
+
if (!parsed.json) {
|
|
2007
|
+
console.warn(`Warning: unrecognized git remote format (${remoteOrigin}). Skipping SSH setup.`);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
914
2010
|
else {
|
|
915
|
-
|
|
916
|
-
if (
|
|
2011
|
+
let activeOrigin = remoteOrigin;
|
|
2012
|
+
if (remoteInfo.protocol === "https") {
|
|
917
2013
|
if (!parsed.json) {
|
|
918
|
-
|
|
919
|
-
|
|
2014
|
+
clackNote([
|
|
2015
|
+
`Origin is HTTPS (${remoteOrigin}).`,
|
|
2016
|
+
"",
|
|
2017
|
+
"SSH auth will not work unless we switch this remote to SSH:",
|
|
2018
|
+
remoteInfo.sshUrl,
|
|
2019
|
+
].join("\n"), "Git remote");
|
|
920
2020
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
2021
|
+
const shouldSwitch = await clackSelect({
|
|
2022
|
+
message: "Switch remote origin to SSH for git auth?",
|
|
2023
|
+
options: [
|
|
2024
|
+
{ value: "switch", label: "Switch to SSH (Recommended)" },
|
|
2025
|
+
{ value: "keep", label: "Keep HTTPS (Skip SSH auth setup)" },
|
|
2026
|
+
{ value: "cancel", label: "Cancel init" },
|
|
2027
|
+
],
|
|
2028
|
+
initialValue: "switch",
|
|
2029
|
+
});
|
|
2030
|
+
if (isCancel(shouldSwitch) || shouldSwitch === "cancel") {
|
|
2031
|
+
throwInitCanceled();
|
|
2032
|
+
}
|
|
2033
|
+
if (shouldSwitch === "keep") {
|
|
925
2034
|
if (!parsed.json) {
|
|
926
|
-
|
|
927
|
-
console.log(`Origin is HTTPS (${remoteOrigin}). SSH auth will not work unless we switch this remote to SSH.`);
|
|
2035
|
+
console.warn("Skipping SSH auth setup. Configure git credentials for this repo manually before pulling or pushing.");
|
|
928
2036
|
}
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
2037
|
+
activeOrigin = "";
|
|
2038
|
+
}
|
|
2039
|
+
else {
|
|
2040
|
+
await runInitStep({
|
|
2041
|
+
enabled: progressEnabled,
|
|
2042
|
+
title: "Switching git remote to SSH",
|
|
2043
|
+
fn: async () => {
|
|
2044
|
+
await setRemoteOrigin(client, canonical, expandedWorkdir, remoteInfo.sshUrl);
|
|
2045
|
+
},
|
|
2046
|
+
});
|
|
2047
|
+
activeOrigin = remoteInfo.sshUrl;
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
if (activeOrigin) {
|
|
2051
|
+
const publicKey = await runInitStep({
|
|
2052
|
+
enabled: progressEnabled,
|
|
2053
|
+
title: "Generating SSH key",
|
|
2054
|
+
fn: async () => await ensureSshKey(client, canonical, `${alias}@devbox`),
|
|
2055
|
+
});
|
|
2056
|
+
await runInitStep({
|
|
2057
|
+
enabled: progressEnabled,
|
|
2058
|
+
title: "Updating SSH config",
|
|
2059
|
+
fn: async () => await ensureSshConfig(client, canonical, remoteInfo.host),
|
|
2060
|
+
});
|
|
2061
|
+
if (!parsed.json) {
|
|
2062
|
+
clackNote(publicKey, `Add this SSH public key to ${remoteInfo.host}`);
|
|
2063
|
+
const copied = await copyToClipboard(publicKey);
|
|
2064
|
+
if (copied) {
|
|
2065
|
+
clackLog.success("Copied SSH public key to clipboard.");
|
|
935
2066
|
}
|
|
936
2067
|
else {
|
|
937
|
-
|
|
938
|
-
await setRemoteOrigin(client, canonical, expandedWorkdir, remoteInfo.sshUrl);
|
|
939
|
-
activeOrigin = remoteInfo.sshUrl;
|
|
2068
|
+
clackLog.warn("Could not copy the SSH key automatically.");
|
|
940
2069
|
}
|
|
2070
|
+
const shouldOpen = await promptBeforeOpenBrowser({
|
|
2071
|
+
url: remoteInfo.settingsUrl,
|
|
2072
|
+
title: `${remoteInfo.host} SSH key page`,
|
|
2073
|
+
consequence: [
|
|
2074
|
+
"Skipping browser open.",
|
|
2075
|
+
"You must add the SSH key before git pull/push will work via SSH.",
|
|
2076
|
+
].join(" "),
|
|
2077
|
+
});
|
|
2078
|
+
if (shouldOpen) {
|
|
2079
|
+
const opened = openBrowser(remoteInfo.settingsUrl);
|
|
2080
|
+
if (!opened) {
|
|
2081
|
+
clackLog.warn("Unable to open the browser automatically.");
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
const added = await clackConfirm({
|
|
2086
|
+
message: "Have you added the SSH key?",
|
|
2087
|
+
active: "Yes, verify now",
|
|
2088
|
+
inactive: "Not yet",
|
|
2089
|
+
initialValue: true,
|
|
2090
|
+
});
|
|
2091
|
+
if (isCancel(added)) {
|
|
2092
|
+
throwInitCanceled();
|
|
941
2093
|
}
|
|
942
|
-
if (
|
|
943
|
-
status.stage("Generating SSH key");
|
|
944
|
-
const publicKey = await ensureSshKey(client, canonical, `${alias}@devbox`);
|
|
945
|
-
status.stage("Updating SSH config");
|
|
946
|
-
await ensureSshConfig(client, canonical, remoteInfo.host);
|
|
2094
|
+
if (!added) {
|
|
947
2095
|
if (!parsed.json) {
|
|
948
|
-
|
|
949
|
-
console.log("");
|
|
950
|
-
console.log(`Add this SSH public key to ${remoteInfo.host}:`);
|
|
951
|
-
console.log(publicKey);
|
|
952
|
-
console.log(`Open: ${remoteInfo.settingsUrl}`);
|
|
953
|
-
const copied = await copyToClipboard(publicKey);
|
|
954
|
-
if (copied) {
|
|
955
|
-
console.log("Copied the SSH public key to your clipboard.");
|
|
956
|
-
}
|
|
957
|
-
else {
|
|
958
|
-
console.log("Could not copy the SSH key automatically.");
|
|
959
|
-
}
|
|
960
|
-
const shouldOpen = await promptToContinue("Press Enter to open the SSH key page in your browser...");
|
|
961
|
-
if (shouldOpen) {
|
|
962
|
-
const opened = openBrowser(remoteInfo.settingsUrl);
|
|
963
|
-
if (!opened) {
|
|
964
|
-
console.log("Unable to open the browser automatically.");
|
|
965
|
-
}
|
|
966
|
-
}
|
|
2096
|
+
console.warn("Skipping SSH verification. Add the key and re-run `dvb init --resume` to verify.");
|
|
967
2097
|
}
|
|
968
|
-
|
|
969
|
-
|
|
2098
|
+
}
|
|
2099
|
+
else {
|
|
2100
|
+
const verified = await runInitStep({
|
|
2101
|
+
enabled: progressEnabled,
|
|
2102
|
+
title: "Verifying git SSH auth",
|
|
2103
|
+
fn: async () => await verifySshAuth(client, canonical, remoteInfo.host, expandedWorkdir),
|
|
2104
|
+
});
|
|
2105
|
+
if (!verified) {
|
|
970
2106
|
if (!parsed.json) {
|
|
971
|
-
console.warn("
|
|
2107
|
+
console.warn("SSH auth verification failed. Confirm the key is added and that the repo access is granted.");
|
|
972
2108
|
}
|
|
973
2109
|
}
|
|
974
2110
|
else {
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
if (!parsed.json) {
|
|
979
|
-
status.stop();
|
|
980
|
-
console.warn("SSH auth verification failed. Confirm the key is added and that the repo access is granted.");
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
else {
|
|
984
|
-
await updateInitState({
|
|
985
|
-
steps: { sshAuthConfigured: true },
|
|
986
|
-
});
|
|
987
|
-
}
|
|
2111
|
+
await updateInitState({
|
|
2112
|
+
steps: { sshAuthConfigured: true },
|
|
2113
|
+
});
|
|
988
2114
|
}
|
|
989
2115
|
}
|
|
990
2116
|
}
|
|
991
2117
|
}
|
|
992
2118
|
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
2119
|
+
const weztermMuxPresent = await runInitStep({
|
|
2120
|
+
enabled: progressEnabled,
|
|
2121
|
+
title: "Ensuring WezTerm mux server (optional)",
|
|
2122
|
+
fn: async ({ fail }) => {
|
|
2123
|
+
try {
|
|
2124
|
+
const installResult = !shouldResume || !initState?.steps.weztermMuxInstalled
|
|
2125
|
+
? await ensureWeztermMuxInstalled({
|
|
2126
|
+
client,
|
|
2127
|
+
spriteName: canonical,
|
|
2128
|
+
allowResolveAsset: true,
|
|
2129
|
+
})
|
|
2130
|
+
: await ensureWeztermMuxInstalled({
|
|
2131
|
+
client,
|
|
2132
|
+
spriteName: canonical,
|
|
2133
|
+
// If the mux is missing on resume, we still need to resolve a GitHub asset.
|
|
2134
|
+
allowResolveAsset: true,
|
|
2135
|
+
});
|
|
2136
|
+
if (installResult.muxPresent) {
|
|
2137
|
+
await updateInitState({ steps: { weztermMuxInstalled: true } });
|
|
2138
|
+
}
|
|
2139
|
+
return installResult.muxPresent;
|
|
1010
2140
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
2141
|
+
catch (error) {
|
|
2142
|
+
logger.warn("wezterm_mux_install_failed", {
|
|
2143
|
+
box: canonical,
|
|
2144
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2145
|
+
});
|
|
2146
|
+
fail("Ensuring WezTerm mux server (optional) (failed)");
|
|
2147
|
+
if (!parsed.json) {
|
|
2148
|
+
const message = error instanceof Error && error.message
|
|
2149
|
+
? error.message
|
|
2150
|
+
: String(error);
|
|
2151
|
+
console.warn(`Warning: failed to install WezTerm mux server (optional). ${message}\n` +
|
|
2152
|
+
"Tip: re-run with DEVBOX_LOG_LEVEL=info to see Sprite exec logs on stderr.");
|
|
2153
|
+
}
|
|
2154
|
+
return false;
|
|
2155
|
+
}
|
|
2156
|
+
},
|
|
2157
|
+
});
|
|
2158
|
+
if (weztermMuxPresent &&
|
|
2159
|
+
(!shouldResume || !initState?.steps.weztermMuxServiceEnsured)) {
|
|
2160
|
+
await runInitStep({
|
|
2161
|
+
enabled: progressEnabled,
|
|
2162
|
+
title: "Ensuring WezTerm mux service (optional)",
|
|
2163
|
+
fn: async ({ fail }) => {
|
|
2164
|
+
try {
|
|
2165
|
+
await ensureWeztermMuxService({ client, spriteName: canonical });
|
|
2166
|
+
await updateInitState({ steps: { weztermMuxServiceEnsured: true } });
|
|
2167
|
+
}
|
|
2168
|
+
catch (error) {
|
|
2169
|
+
logger.warn("wezterm_mux_service_failed", {
|
|
2170
|
+
box: canonical,
|
|
2171
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2172
|
+
});
|
|
2173
|
+
fail("Ensuring WezTerm mux service (optional) (failed)");
|
|
2174
|
+
if (!parsed.json) {
|
|
2175
|
+
const message = error instanceof Error && error.message
|
|
2176
|
+
? error.message
|
|
2177
|
+
: String(error);
|
|
2178
|
+
console.warn(`Warning: failed to ensure WezTerm mux service (optional). ${message}`);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
},
|
|
1022
2182
|
});
|
|
1023
|
-
if (!parsed.json) {
|
|
1024
|
-
status.stop();
|
|
1025
|
-
const message = error instanceof Error && error.message
|
|
1026
|
-
? error.message
|
|
1027
|
-
: String(error);
|
|
1028
|
-
console.warn(`Warning: failed to ensure WezTerm mux service. ${message}`);
|
|
1029
|
-
}
|
|
1030
2183
|
}
|
|
1031
2184
|
if (!skipServicesConfig) {
|
|
1032
2185
|
if (!approvedServices) {
|
|
1033
2186
|
throw new Error("Missing services plan output.");
|
|
1034
2187
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
2188
|
+
await runInitStep({
|
|
2189
|
+
enabled: progressEnabled,
|
|
2190
|
+
title: "Writing devbox.toml services",
|
|
2191
|
+
fn: async () => {
|
|
2192
|
+
await writeRemoteServicesToml({
|
|
2193
|
+
client,
|
|
2194
|
+
canonical,
|
|
2195
|
+
workdir: expandedWorkdir,
|
|
2196
|
+
services: approvedServices.backgroundServices,
|
|
2197
|
+
});
|
|
2198
|
+
await updateInitState({ steps: { servicesConfigWritten: true } });
|
|
2199
|
+
},
|
|
1041
2200
|
});
|
|
1042
|
-
await updateInitState({ steps: { servicesConfigWritten: true } });
|
|
1043
2201
|
}
|
|
1044
|
-
status.stage("Registering project");
|
|
1045
2202
|
const projectMeta = {
|
|
1046
2203
|
fingerprint,
|
|
1047
2204
|
canonical,
|
|
@@ -1050,23 +2207,64 @@ export const runInit = async (args) => {
|
|
|
1050
2207
|
origin: projectOrigin,
|
|
1051
2208
|
createdAt: projectCreatedAt,
|
|
1052
2209
|
};
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
2210
|
+
const projectMetaPath = `/home/sprite/.devbox/projects/${fingerprint}.json`;
|
|
2211
|
+
await runInitStep({
|
|
2212
|
+
enabled: progressEnabled,
|
|
2213
|
+
title: "Registering project",
|
|
2214
|
+
fn: async () => {
|
|
2215
|
+
let existing = {};
|
|
2216
|
+
try {
|
|
2217
|
+
const raw = await client.readFile(canonical, { path: projectMetaPath });
|
|
2218
|
+
const parsed = JSON.parse(Buffer.from(raw).toString("utf8"));
|
|
2219
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2220
|
+
existing = parsed;
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
catch (error) {
|
|
2224
|
+
if (!(error instanceof SpritesApiError && error.status === 404)) {
|
|
2225
|
+
throw error;
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
const existingCheckpoints = existing.checkpoints &&
|
|
2229
|
+
typeof existing.checkpoints === "object" &&
|
|
2230
|
+
!Array.isArray(existing.checkpoints)
|
|
2231
|
+
? existing.checkpoints
|
|
2232
|
+
: {};
|
|
2233
|
+
const stateCheckpoints = initState?.checkpoints ?? {};
|
|
2234
|
+
const merged = {
|
|
2235
|
+
...existing,
|
|
2236
|
+
...projectMeta,
|
|
2237
|
+
checkpoints: { ...existingCheckpoints, ...stateCheckpoints },
|
|
2238
|
+
};
|
|
2239
|
+
await client.writeFile(canonical, projectMetaPath, Buffer.from(JSON.stringify(merged, null, 2)));
|
|
2240
|
+
},
|
|
2241
|
+
});
|
|
2242
|
+
await runInitStep({
|
|
2243
|
+
enabled: progressEnabled,
|
|
2244
|
+
title: "Writing local metadata",
|
|
2245
|
+
fn: async () => {
|
|
2246
|
+
await ensureDevboxToml(repoRoot, repoName, slug);
|
|
2247
|
+
await writeRepoMarker(repoRoot, { fingerprint, canonical, alias });
|
|
2248
|
+
},
|
|
2249
|
+
});
|
|
2250
|
+
await runInitStep({
|
|
2251
|
+
enabled: progressEnabled,
|
|
2252
|
+
title: "Updating Codex config on sprite (optional)",
|
|
2253
|
+
fn: async ({ fail }) => {
|
|
2254
|
+
try {
|
|
2255
|
+
await writeRemoteCodexConfig(client, canonical, repoName);
|
|
2256
|
+
}
|
|
2257
|
+
catch (error) {
|
|
2258
|
+
logger.warn("codex_config_update_failed", {
|
|
2259
|
+
error: String(error),
|
|
2260
|
+
});
|
|
2261
|
+
fail("Updating Codex config on sprite (optional) (failed)");
|
|
2262
|
+
if (!parsed.json) {
|
|
2263
|
+
console.warn("Warning: failed to update /home/sprite/.codex/config.toml for full-access sandbox settings.");
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
},
|
|
2267
|
+
});
|
|
1070
2268
|
try {
|
|
1071
2269
|
const gitignore = await fs.readFile(path.join(repoRoot, ".gitignore"), "utf8");
|
|
1072
2270
|
if (!gitignore.includes(".devbox")) {
|
|
@@ -1076,42 +2274,143 @@ export const runInit = async (args) => {
|
|
|
1076
2274
|
catch {
|
|
1077
2275
|
// ignore missing .gitignore
|
|
1078
2276
|
}
|
|
2277
|
+
const skipSetupArtifactsStage = skipCodexApply ||
|
|
2278
|
+
(shouldResume && initState?.steps.setupArtifactsStaged);
|
|
2279
|
+
if (!skipSetupArtifactsStage) {
|
|
2280
|
+
await runInitStep({
|
|
2281
|
+
enabled: progressEnabled,
|
|
2282
|
+
title: "Staging setup artifacts",
|
|
2283
|
+
fn: async ({ status }) => {
|
|
2284
|
+
status.stage("Copying repo artifacts and staging external files");
|
|
2285
|
+
await stageRemoteSetupArtifacts({
|
|
2286
|
+
client,
|
|
2287
|
+
canonical,
|
|
2288
|
+
workdir: expandedWorkdir,
|
|
2289
|
+
artifactsBundlePath: remoteArtifactsBundlePath,
|
|
2290
|
+
artifactsManifestPath: remoteArtifactsManifestPath,
|
|
2291
|
+
});
|
|
2292
|
+
},
|
|
2293
|
+
});
|
|
2294
|
+
await updateInitState({ steps: { setupArtifactsStaged: true } });
|
|
2295
|
+
}
|
|
1079
2296
|
if (!skipCodexApply) {
|
|
1080
|
-
await
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
2297
|
+
await runInitStep({
|
|
2298
|
+
enabled: progressEnabled,
|
|
2299
|
+
title: "Snapshotting filesystem (pre-setup) (optional)",
|
|
2300
|
+
fn: async ({ fail, ok }) => {
|
|
2301
|
+
try {
|
|
2302
|
+
const checkpoint = await recordCodexCheckpoint({
|
|
2303
|
+
client,
|
|
2304
|
+
canonical,
|
|
2305
|
+
phase: "preCodexSetup",
|
|
2306
|
+
});
|
|
2307
|
+
if (checkpoint.id) {
|
|
2308
|
+
ok(`Snapshot created: ${checkpoint.id}`);
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
catch (error) {
|
|
2312
|
+
logger.warn("init_checkpoint_create_failed", {
|
|
2313
|
+
box: canonical,
|
|
2314
|
+
fingerprint,
|
|
2315
|
+
phase: "pre-codex-setup",
|
|
2316
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2317
|
+
});
|
|
2318
|
+
fail("Snapshotting filesystem (pre-setup) (optional) (failed)");
|
|
2319
|
+
if (!parsed.json) {
|
|
2320
|
+
console.warn("Warning: failed to create pre-setup filesystem snapshot. Continuing without it.");
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
},
|
|
2324
|
+
});
|
|
2325
|
+
if (!skipServicesEnable) {
|
|
2326
|
+
await runInitStep({
|
|
2327
|
+
enabled: progressEnabled,
|
|
2328
|
+
title: "Enabling devbox services",
|
|
2329
|
+
fn: async ({ status }) => {
|
|
2330
|
+
await enableRemoteServices({
|
|
2331
|
+
client,
|
|
2332
|
+
canonical,
|
|
2333
|
+
services: approvedServices?.backgroundServices ?? [],
|
|
2334
|
+
status,
|
|
2335
|
+
});
|
|
2336
|
+
await updateInitState({ steps: { servicesEnabled: true } });
|
|
2337
|
+
},
|
|
2338
|
+
});
|
|
2339
|
+
}
|
|
2340
|
+
await runInitStep({
|
|
2341
|
+
enabled: progressEnabled,
|
|
2342
|
+
title: "Running Codex setup",
|
|
2343
|
+
fn: async ({ status }) => {
|
|
2344
|
+
await runRemoteCodexSetup({
|
|
2345
|
+
client,
|
|
2346
|
+
canonical,
|
|
2347
|
+
expandedWorkdir,
|
|
2348
|
+
remoteSetupPath,
|
|
2349
|
+
remoteArtifactsBundlePath,
|
|
2350
|
+
remoteArtifactsManifestPath,
|
|
2351
|
+
socketInfo,
|
|
2352
|
+
status,
|
|
2353
|
+
pathSetup,
|
|
2354
|
+
entrypoints: approvedServices?.appEntrypoints ?? [],
|
|
2355
|
+
});
|
|
2356
|
+
},
|
|
1091
2357
|
});
|
|
1092
2358
|
await updateInitState({ steps: { codexApplied: true } });
|
|
1093
2359
|
await updateRegistryProjectStatus(resolveInitStatus(initState?.steps, initState?.complete));
|
|
2360
|
+
await runInitStep({
|
|
2361
|
+
enabled: progressEnabled,
|
|
2362
|
+
title: "Snapshotting filesystem (post-setup) (optional)",
|
|
2363
|
+
fn: async ({ fail, ok }) => {
|
|
2364
|
+
try {
|
|
2365
|
+
const checkpoint = await recordCodexCheckpoint({
|
|
2366
|
+
client,
|
|
2367
|
+
canonical,
|
|
2368
|
+
phase: "postCodexSetup",
|
|
2369
|
+
});
|
|
2370
|
+
if (checkpoint.id) {
|
|
2371
|
+
ok(`Snapshot created: ${checkpoint.id}`);
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
catch (error) {
|
|
2375
|
+
logger.warn("init_checkpoint_create_failed", {
|
|
2376
|
+
box: canonical,
|
|
2377
|
+
fingerprint,
|
|
2378
|
+
phase: "post-codex-setup",
|
|
2379
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2380
|
+
});
|
|
2381
|
+
fail("Snapshotting filesystem (post-setup) (optional) (failed)");
|
|
2382
|
+
if (!parsed.json) {
|
|
2383
|
+
console.warn("Warning: failed to create post-setup filesystem snapshot. Continuing without it.");
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
},
|
|
2387
|
+
});
|
|
1094
2388
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
2389
|
+
const finalStatus = resolveInitStatus(initState?.steps, initState?.complete);
|
|
2390
|
+
const isComplete = finalStatus === "complete";
|
|
2391
|
+
await runInitStep({
|
|
2392
|
+
enabled: progressEnabled,
|
|
2393
|
+
title: "Finalizing init",
|
|
2394
|
+
fn: async () => {
|
|
2395
|
+
await updateInitState({ complete: isComplete });
|
|
2396
|
+
await updateRegistryProjectStatus(finalStatus);
|
|
2397
|
+
},
|
|
2398
|
+
});
|
|
1098
2399
|
if (parsed.json) {
|
|
1099
|
-
console.log(JSON.stringify({ ok: true, canonical, alias, workdir, fingerprint }, null, 2));
|
|
2400
|
+
console.log(JSON.stringify({ ok: true, status: finalStatus, canonical, alias, workdir, fingerprint }, null, 2));
|
|
2401
|
+
return;
|
|
2402
|
+
}
|
|
2403
|
+
if (!isComplete) {
|
|
2404
|
+
console.log(`devbox initialized (setup incomplete): ${alias} -> ${canonical}`);
|
|
2405
|
+
console.log(`workdir: ${workdir}`);
|
|
2406
|
+
console.log("next: run `dvb init --resume` from this repo to finish setup");
|
|
2407
|
+
console.log("sprites: synced to control plane");
|
|
1100
2408
|
return;
|
|
1101
2409
|
}
|
|
1102
2410
|
console.log(`devbox initialized: ${alias} -> ${canonical}`);
|
|
1103
2411
|
console.log(`workdir: ${workdir}`);
|
|
1104
2412
|
console.log("sprites: synced to control plane");
|
|
1105
2413
|
};
|
|
1106
|
-
|
|
1107
|
-
await run();
|
|
1108
|
-
}
|
|
1109
|
-
catch (error) {
|
|
1110
|
-
status.fail("Init failed");
|
|
1111
|
-
throw error;
|
|
1112
|
-
}
|
|
1113
|
-
finally {
|
|
1114
|
-
status.stop();
|
|
1115
|
-
}
|
|
2414
|
+
await run();
|
|
1116
2415
|
};
|
|
1117
2416
|
//# sourceMappingURL=index.js.map
|