@boxes-dev/dvb 1.0.42 → 1.0.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/bin/dvb.cjs +1577 -749
  2. package/dist/bin/dvb.cjs.map +1 -1
  3. package/dist/bin/dvbd.cjs +5 -5
  4. package/dist/devbox/cli.js +1 -1
  5. package/dist/devbox/cli.js.map +1 -1
  6. package/dist/devbox/commands/agent.d.ts.map +1 -1
  7. package/dist/devbox/commands/agent.js +5 -1
  8. package/dist/devbox/commands/agent.js.map +1 -1
  9. package/dist/devbox/commands/boxSelect.d.ts.map +1 -1
  10. package/dist/devbox/commands/boxSelect.js +7 -7
  11. package/dist/devbox/commands/boxSelect.js.map +1 -1
  12. package/dist/devbox/commands/connect.d.ts.map +1 -1
  13. package/dist/devbox/commands/connect.js +6 -5
  14. package/dist/devbox/commands/connect.js.map +1 -1
  15. package/dist/devbox/commands/destroy.d.ts.map +1 -1
  16. package/dist/devbox/commands/destroy.js +4 -1
  17. package/dist/devbox/commands/destroy.js.map +1 -1
  18. package/dist/devbox/commands/init/args.d.ts +0 -1
  19. package/dist/devbox/commands/init/args.d.ts.map +1 -1
  20. package/dist/devbox/commands/init/args.js +0 -8
  21. package/dist/devbox/commands/init/args.js.map +1 -1
  22. package/dist/devbox/commands/init/codex/artifacts.d.ts +25 -2
  23. package/dist/devbox/commands/init/codex/artifacts.d.ts.map +1 -1
  24. package/dist/devbox/commands/init/codex/artifacts.js +134 -1
  25. package/dist/devbox/commands/init/codex/artifacts.js.map +1 -1
  26. package/dist/devbox/commands/init/codex/index.d.ts +3 -1
  27. package/dist/devbox/commands/init/codex/index.d.ts.map +1 -1
  28. package/dist/devbox/commands/init/codex/index.js +149 -11
  29. package/dist/devbox/commands/init/codex/index.js.map +1 -1
  30. package/dist/devbox/commands/init/codex/local.d.ts +16 -8
  31. package/dist/devbox/commands/init/codex/local.d.ts.map +1 -1
  32. package/dist/devbox/commands/init/codex/local.js +79 -19
  33. package/dist/devbox/commands/init/codex/local.js.map +1 -1
  34. package/dist/devbox/commands/init/index.d.ts.map +1 -1
  35. package/dist/devbox/commands/init/index.js +365 -83
  36. package/dist/devbox/commands/init/index.js.map +1 -1
  37. package/dist/devbox/commands/init/setupArtifactsValidation.d.ts +28 -0
  38. package/dist/devbox/commands/init/setupArtifactsValidation.d.ts.map +1 -0
  39. package/dist/devbox/commands/init/setupArtifactsValidation.js +113 -0
  40. package/dist/devbox/commands/init/setupArtifactsValidation.js.map +1 -0
  41. package/dist/devbox/commands/list.js +1 -1
  42. package/dist/devbox/commands/list.js.map +1 -1
  43. package/dist/devbox/commands/mount.d.ts.map +1 -1
  44. package/dist/devbox/commands/mount.js +5 -1
  45. package/dist/devbox/commands/mount.js.map +1 -1
  46. package/dist/devbox/commands/mountSsh.js +2 -2
  47. package/dist/devbox/commands/mountSsh.js.map +1 -1
  48. package/dist/devbox/commands/ports.d.ts.map +1 -1
  49. package/dist/devbox/commands/ports.js +9 -2
  50. package/dist/devbox/commands/ports.js.map +1 -1
  51. package/dist/devbox/commands/services.d.ts.map +1 -1
  52. package/dist/devbox/commands/services.js +5 -1
  53. package/dist/devbox/commands/services.js.map +1 -1
  54. package/dist/devbox/commands/sessions.d.ts.map +1 -1
  55. package/dist/devbox/commands/sessions.js +12 -3
  56. package/dist/devbox/commands/sessions.js.map +1 -1
  57. package/dist/devbox/commands/wezterm.d.ts.map +1 -1
  58. package/dist/devbox/commands/wezterm.js +5 -1
  59. package/dist/devbox/commands/wezterm.js.map +1 -1
  60. package/dist/prompts/local-scan-env-secrets.md +2 -0
  61. package/dist/prompts/local-scan-external.md +2 -0
  62. package/dist/prompts/local-scan-extra-artifacts.md +2 -0
  63. package/package.json +1 -1
@@ -22,6 +22,7 @@ import { createSetupArtifacts, promptForPlanApproval, promptForServicesApproval,
22
22
  import { mergeServicesToml, splitShellCommand, } from "../servicesToml.js";
23
23
  import { checkSpriteExists, destroySpriteAndClearState, } from "../destroyShared.js";
24
24
  import { runInitStep } from "./progress.js";
25
+ import { collectMissingSetupArtifacts, remapSelectedPathEntries, } from "./setupArtifactsValidation.js";
25
26
  const requireDaemonJsonOk = (response, label) => {
26
27
  if (response.status >= 200 && response.status < 300) {
27
28
  return response.body;
@@ -58,10 +59,25 @@ const ensurePrivateDir = async (dir) => {
58
59
  }
59
60
  };
60
61
  const DEFAULT_INIT_STEP_RETRIES = 3;
62
+ const SETUP_ARTIFACT_REGEN_MAX_ATTEMPTS = 3;
61
63
  const INIT_STEP_RETRYABLE_STATUSES = new Set([
62
64
  408, 409, 425, 429, 500, 502, 503, 504,
63
65
  ]);
66
+ const ALIAS_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
67
+ const RESERVED_ALIAS_PATTERN = /^dvb-[a-f0-9]{12}-/;
64
68
  const delay = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));
69
+ const normalizeAlias = (value) => value.trim().toLowerCase();
70
+ const validateAlias = (alias) => {
71
+ if (!alias) {
72
+ throw new Error("Alias is required.");
73
+ }
74
+ if (RESERVED_ALIAS_PATTERN.test(alias)) {
75
+ throw new Error(`Alias "${alias}" is reserved. Choose an alias that does not start with dvb-<12 hex>-.`);
76
+ }
77
+ if (!ALIAS_PATTERN.test(alias)) {
78
+ throw new Error(`Invalid alias "${alias}". Use lowercase letters, numbers, and dashes.`);
79
+ }
80
+ };
65
81
  const computeRetryDelayMs = (retryIndex) => {
66
82
  // retryIndex is 1-based (1..N)
67
83
  const base = 500;
@@ -161,6 +177,8 @@ const migrateLegacyRepoDevboxDir = async ({ repoRoot, projectDir, }) => {
161
177
  "setup.json",
162
178
  "setup-artifacts.tgz",
163
179
  "setup-artifacts.json",
180
+ "setup-artifacts.parts",
181
+ "setup-artifacts.parts.json",
164
182
  "scans",
165
183
  "logs",
166
184
  ];
@@ -479,8 +497,7 @@ export const runInit = async (args) => {
479
497
  parsed.force ||
480
498
  parsed.yes ||
481
499
  parsed.codexSetupOnly ||
482
- parsed.alias ||
483
- parsed.name) {
500
+ parsed.alias) {
484
501
  throw new Error("`dvb init --status` cannot be combined with other init flags (except --json).");
485
502
  }
486
503
  const socketInfo = resolveSocketInfo();
@@ -769,13 +786,22 @@ export const runInit = async (args) => {
769
786
  }
770
787
  const localArtifactsBundlePath = path.join(setupDir, "setup-artifacts.tgz");
771
788
  const localArtifactsManifestPath = path.join(setupDir, "setup-artifacts.json");
789
+ const localArtifactsPartsDescriptorPath = path.join(setupDir, "setup-artifacts.parts.json");
772
790
  let artifactsBundlePath = null;
773
791
  let artifactsManifestPath = null;
792
+ let artifactsPartsDescriptorPath = null;
774
793
  try {
775
794
  await fs.access(localArtifactsBundlePath);
776
795
  await fs.access(localArtifactsManifestPath);
777
796
  artifactsBundlePath = localArtifactsBundlePath;
778
797
  artifactsManifestPath = localArtifactsManifestPath;
798
+ try {
799
+ await fs.access(localArtifactsPartsDescriptorPath);
800
+ artifactsPartsDescriptorPath = localArtifactsPartsDescriptorPath;
801
+ }
802
+ catch {
803
+ // split artifacts descriptor is optional
804
+ }
779
805
  }
780
806
  catch {
781
807
  // artifacts are optional in setup-only flow
@@ -834,6 +860,7 @@ export const runInit = async (args) => {
834
860
  const remoteSetupPath = path.posix.join(expandedWorkdir, ".devbox", "setup.json");
835
861
  const remoteArtifactsBundlePath = path.posix.join(expandedWorkdir, ".devbox", "setup-artifacts.tgz");
836
862
  const remoteArtifactsManifestPath = path.posix.join(expandedWorkdir, ".devbox", "setup-artifacts.json");
863
+ const remoteArtifactsPartsDescriptorPath = path.posix.join(expandedWorkdir, ".devbox", "setup-artifacts.parts.json");
837
864
  const pathSetup = 'export PATH="$(npm bin -g 2>/dev/null):$PATH"';
838
865
  await runInitStep({
839
866
  enabled: progressEnabled,
@@ -858,12 +885,16 @@ export const runInit = async (args) => {
858
885
  remoteSetupPath,
859
886
  localArtifactsBundlePath: artifactsBundlePath,
860
887
  localArtifactsManifestPath: artifactsManifestPath,
888
+ localArtifactsPartsDescriptorPath: artifactsPartsDescriptorPath,
861
889
  remoteArtifactsBundlePath: artifactsBundlePath
862
890
  ? remoteArtifactsBundlePath
863
891
  : null,
864
892
  remoteArtifactsManifestPath: artifactsBundlePath
865
893
  ? remoteArtifactsManifestPath
866
894
  : null,
895
+ remoteArtifactsPartsDescriptorPath: artifactsBundlePath
896
+ ? remoteArtifactsPartsDescriptorPath
897
+ : null,
867
898
  status,
868
899
  });
869
900
  },
@@ -1036,13 +1067,15 @@ export const runInit = async (args) => {
1036
1067
  throw new Error(`Repo already initialized (box: ${existingEntry.canonical}).`);
1037
1068
  }
1038
1069
  }
1039
- const alias = parsed.alias ?? initState?.alias ?? existingEntry?.alias ?? slug;
1040
- const canonicalHint = parsed.name ?? initState?.canonical ?? existingEntry?.canonical;
1070
+ const alias = normalizeAlias(parsed.alias ?? initState?.alias ?? existingEntry?.alias ?? slug);
1071
+ validateAlias(alias);
1072
+ const canonicalHint = initState?.canonical ?? existingEntry?.canonical;
1041
1073
  const workdir = `~/${slug}`;
1042
1074
  const expandedWorkdir = expandHome(workdir);
1043
1075
  const remoteSetupPath = path.posix.join(expandedWorkdir, ".devbox", "setup.json");
1044
1076
  const remoteArtifactsBundlePath = path.posix.join(expandedWorkdir, ".devbox", "setup-artifacts.tgz");
1045
1077
  const remoteArtifactsManifestPath = path.posix.join(expandedWorkdir, ".devbox", "setup-artifacts.json");
1078
+ const remoteArtifactsPartsDescriptorPath = path.posix.join(expandedWorkdir, ".devbox", "setup-artifacts.parts.json");
1046
1079
  const pathSetup = 'export PATH="$(npm bin -g 2>/dev/null):$PATH"';
1047
1080
  const { config, client, controlPlaneToken } = await runInitStep({
1048
1081
  enabled: progressEnabled,
@@ -1121,15 +1154,17 @@ export const runInit = async (args) => {
1121
1154
  });
1122
1155
  existingEntry = null;
1123
1156
  }
1124
- const username = os.userInfo().username;
1125
1157
  let canonical = (shouldResume && initState?.canonical ? initState.canonical : null) ??
1126
1158
  canonicalHint ??
1127
- `${username}-${slug}`;
1159
+ alias;
1128
1160
  const knownAssociatedCanonicals = new Set([existingEntry?.canonical, initState?.canonical].filter((value) => typeof value === "string" && value.length > 0));
1129
1161
  const createSprite = async (name) => {
1130
1162
  try {
1131
- await client.createSprite(name);
1132
- return "created";
1163
+ const created = await client.createSprite(name);
1164
+ const canonical = typeof created.name === "string" && created.name.trim()
1165
+ ? created.name.trim()
1166
+ : name;
1167
+ return { kind: "created", canonical };
1133
1168
  }
1134
1169
  catch (error) {
1135
1170
  if (error instanceof SpritesApiError) {
@@ -1138,7 +1173,7 @@ export const runInit = async (args) => {
1138
1173
  error.status === 409 ||
1139
1174
  body.includes("exists") ||
1140
1175
  body.includes("already")) {
1141
- return "exists";
1176
+ return { kind: "exists" };
1142
1177
  }
1143
1178
  }
1144
1179
  throw error;
@@ -1163,7 +1198,7 @@ export const runInit = async (args) => {
1163
1198
  fn: async ({ status }) => {
1164
1199
  let nextCanonical = canonical;
1165
1200
  const createResult = await createSprite(nextCanonical);
1166
- if (createResult === "exists") {
1201
+ if (createResult.kind === "exists") {
1167
1202
  const associatedCanonical = knownAssociatedCanonicals.has(nextCanonical);
1168
1203
  if (parsed.force) {
1169
1204
  if (!associatedCanonical) {
@@ -1181,7 +1216,7 @@ export const runInit = async (args) => {
1181
1216
  throwInitCanceled();
1182
1217
  }
1183
1218
  if (!confirmedReuse) {
1184
- throw new Error(`Sprite reuse canceled: ${nextCanonical}. Choose a different --name.`);
1219
+ throw new Error(`Sprite reuse canceled: ${nextCanonical}. Choose a different alias.`);
1185
1220
  }
1186
1221
  status.stage("Creating devbox");
1187
1222
  }
@@ -1194,11 +1229,15 @@ export const runInit = async (args) => {
1194
1229
  nextCanonical = `${nextCanonical}-${suffix}`;
1195
1230
  status.stage("Resolving devbox name");
1196
1231
  const second = await createSprite(nextCanonical);
1197
- if (second === "exists") {
1232
+ if (second.kind === "exists") {
1198
1233
  throw new Error(`Sprite already exists: ${nextCanonical}`);
1199
1234
  }
1235
+ nextCanonical = second.canonical;
1200
1236
  }
1201
1237
  }
1238
+ else {
1239
+ nextCanonical = createResult.canonical;
1240
+ }
1202
1241
  await updateInitState({
1203
1242
  canonical: nextCanonical,
1204
1243
  alias,
@@ -1394,6 +1433,7 @@ export const runInit = async (args) => {
1394
1433
  const setupExternalScanPath = path.join(scansDir, "setup-external.json");
1395
1434
  const setupExtraArtifactsScanPath = path.join(scansDir, "setup-extra-artifacts.json");
1396
1435
  const servicesScanPath = path.join(scansDir, "services.json");
1436
+ const scanThreadsPath = path.join(scansDir, "codex-scan-threads.json");
1397
1437
  let setupArtifacts = null;
1398
1438
  const nonInteractive = !process.stdin.isTTY || parsed.json;
1399
1439
  const skipSetupPlan = shouldResume && initState?.steps.setupPlanWritten;
@@ -1452,7 +1492,74 @@ export const runInit = async (args) => {
1452
1492
  return null;
1453
1493
  }
1454
1494
  };
1495
+ const tryReadScanThreads = async () => {
1496
+ try {
1497
+ const raw = await fs.readFile(scanThreadsPath, "utf8");
1498
+ const parsed = JSON.parse(raw);
1499
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1500
+ return {};
1501
+ }
1502
+ const record = parsed;
1503
+ return {
1504
+ ...(typeof record.envSecretsThreadId === "string"
1505
+ ? { envSecretsThreadId: record.envSecretsThreadId }
1506
+ : {}),
1507
+ ...(typeof record.externalThreadId === "string"
1508
+ ? { externalThreadId: record.externalThreadId }
1509
+ : {}),
1510
+ ...(typeof record.extraArtifactsThreadId === "string"
1511
+ ? { extraArtifactsThreadId: record.extraArtifactsThreadId }
1512
+ : {}),
1513
+ ...(typeof record.servicesThreadId === "string"
1514
+ ? { servicesThreadId: record.servicesThreadId }
1515
+ : {}),
1516
+ };
1517
+ }
1518
+ catch {
1519
+ return {};
1520
+ }
1521
+ };
1522
+ let scanThreads = shouldResume
1523
+ ? await tryReadScanThreads()
1524
+ : {};
1525
+ let scanThreadsDirty = false;
1526
+ const persistScanThreads = async () => {
1527
+ await fs.mkdir(scansDir, { recursive: true });
1528
+ await fs.writeFile(scanThreadsPath, JSON.stringify(scanThreads, null, 2), "utf8");
1529
+ };
1530
+ const flushScanThreads = async () => {
1531
+ if (!scanThreadsDirty)
1532
+ return;
1533
+ await persistScanThreads();
1534
+ scanThreadsDirty = false;
1535
+ };
1536
+ const saveScanThreadId = (key, threadId) => {
1537
+ if (!threadId)
1538
+ return;
1539
+ if (scanThreads[key] === threadId)
1540
+ return;
1541
+ scanThreads = { ...scanThreads, [key]: threadId };
1542
+ scanThreadsDirty = true;
1543
+ };
1455
1544
  const shouldRetryCodexScan = (scanFullyCompleted, ...arrays) => !scanFullyCompleted && arrays.every((array) => array.length === 0);
1545
+ const runCodexScanWithImmediateRetry = async ({ run, read, outputPath, update, shouldRetry, }) => {
1546
+ try {
1547
+ await run();
1548
+ let out = await read();
1549
+ if (shouldRetry(out)) {
1550
+ update("retrying");
1551
+ await fs.rm(outputPath, { force: true });
1552
+ await run();
1553
+ out = await read();
1554
+ }
1555
+ update("done");
1556
+ return out;
1557
+ }
1558
+ catch (error) {
1559
+ update("failed");
1560
+ throw error;
1561
+ }
1562
+ };
1456
1563
  let setupPlan = skipSetupPlan || !shouldResume ? null : await tryReadSetupPlan();
1457
1564
  const needsSetupScan = !skipSetupPlan && !setupPlan;
1458
1565
  let servicesPlan = !needsSetupScan || !shouldResume ? null : await tryReadServicesPlan();
@@ -1516,39 +1623,24 @@ export const runInit = async (args) => {
1516
1623
  if (needsSetupScan) {
1517
1624
  await fs.mkdir(scansDir, { recursive: true });
1518
1625
  }
1519
- const runCodexScanWithImmediateRetry = async ({ run, read, outputPath, update, shouldRetry, }) => {
1520
- try {
1521
- await run();
1522
- let out = await read();
1523
- if (shouldRetry(out)) {
1524
- update("retrying");
1525
- await fs.rm(outputPath, { force: true });
1526
- await run();
1527
- out = await read();
1528
- }
1529
- update("done");
1530
- return out;
1531
- }
1532
- catch (error) {
1533
- update("failed");
1534
- throw error;
1535
- }
1536
- };
1537
1626
  const envSecretsPromise = !needsSetupScan
1538
1627
  ? Promise.resolve(null)
1539
1628
  : !needsEnvSecretsScan
1540
1629
  ? Promise.resolve(envSecretsScan)
1541
1630
  : runCodexScanWithImmediateRetry({
1542
- run: async () => await runLocalSetupEnvSecretsScan({
1543
- cwd: repoRoot,
1544
- logDir,
1545
- schemaPath: envSecretsSchemaPath,
1546
- outputPath: setupEnvSecretsScanPath,
1547
- ...(initCodexProxyOptions
1548
- ? { proxyOptions: initCodexProxyOptions }
1549
- : {}),
1550
- onProgress: updateEnvSecrets,
1551
- }),
1631
+ run: async () => {
1632
+ const threadId = await runLocalSetupEnvSecretsScan({
1633
+ cwd: repoRoot,
1634
+ logDir,
1635
+ schemaPath: envSecretsSchemaPath,
1636
+ outputPath: setupEnvSecretsScanPath,
1637
+ ...(initCodexProxyOptions
1638
+ ? { proxyOptions: initCodexProxyOptions }
1639
+ : {}),
1640
+ onProgress: updateEnvSecrets,
1641
+ });
1642
+ await saveScanThreadId("envSecretsThreadId", threadId);
1643
+ },
1552
1644
  read: async () => await readSetupEnvSecretsPlan(setupEnvSecretsScanPath),
1553
1645
  outputPath: setupEnvSecretsScanPath,
1554
1646
  update: updateEnvSecrets,
@@ -1559,17 +1651,20 @@ export const runInit = async (args) => {
1559
1651
  : !needsExternalScan
1560
1652
  ? Promise.resolve(externalScan)
1561
1653
  : runCodexScanWithImmediateRetry({
1562
- run: async () => await runLocalSetupExternalScan({
1563
- cwd: repoRoot,
1564
- logDir,
1565
- schemaPath: externalSchemaPath,
1566
- outputPath: setupExternalScanPath,
1567
- homeDir: localHomeDir,
1568
- ...(initCodexProxyOptions
1569
- ? { proxyOptions: initCodexProxyOptions }
1570
- : {}),
1571
- onProgress: updateExternal,
1572
- }),
1654
+ run: async () => {
1655
+ const threadId = await runLocalSetupExternalScan({
1656
+ cwd: repoRoot,
1657
+ logDir,
1658
+ schemaPath: externalSchemaPath,
1659
+ outputPath: setupExternalScanPath,
1660
+ homeDir: localHomeDir,
1661
+ ...(initCodexProxyOptions
1662
+ ? { proxyOptions: initCodexProxyOptions }
1663
+ : {}),
1664
+ onProgress: updateExternal,
1665
+ });
1666
+ await saveScanThreadId("externalThreadId", threadId);
1667
+ },
1573
1668
  read: async () => await readSetupExternalPlan(setupExternalScanPath),
1574
1669
  outputPath: setupExternalScanPath,
1575
1670
  update: updateExternal,
@@ -1580,16 +1675,19 @@ export const runInit = async (args) => {
1580
1675
  : !needsExtraArtifactsScan
1581
1676
  ? Promise.resolve(extraArtifactsScan)
1582
1677
  : runCodexScanWithImmediateRetry({
1583
- run: async () => await runLocalSetupExtraArtifactsScan({
1584
- cwd: repoRoot,
1585
- logDir,
1586
- schemaPath: extraArtifactsSchemaPath,
1587
- outputPath: setupExtraArtifactsScanPath,
1588
- ...(initCodexProxyOptions
1589
- ? { proxyOptions: initCodexProxyOptions }
1590
- : {}),
1591
- onProgress: updateExtraArtifacts,
1592
- }),
1678
+ run: async () => {
1679
+ const threadId = await runLocalSetupExtraArtifactsScan({
1680
+ cwd: repoRoot,
1681
+ logDir,
1682
+ schemaPath: extraArtifactsSchemaPath,
1683
+ outputPath: setupExtraArtifactsScanPath,
1684
+ ...(initCodexProxyOptions
1685
+ ? { proxyOptions: initCodexProxyOptions }
1686
+ : {}),
1687
+ onProgress: updateExtraArtifacts,
1688
+ });
1689
+ await saveScanThreadId("extraArtifactsThreadId", threadId);
1690
+ },
1593
1691
  read: async () => await readSetupExtraArtifactsPlan(setupExtraArtifactsScanPath),
1594
1692
  outputPath: setupExtraArtifactsScanPath,
1595
1693
  update: updateExtraArtifacts,
@@ -1598,17 +1696,20 @@ export const runInit = async (args) => {
1598
1696
  const servicesPromise = !needsServicesScan
1599
1697
  ? Promise.resolve(servicesPlan)
1600
1698
  : runCodexScanWithImmediateRetry({
1601
- run: async () => await runLocalServicesScan({
1602
- cwd: repoRoot,
1603
- logDir,
1604
- schemaPath: servicesSchemaPath,
1605
- outputPath: servicesScanPath,
1606
- homeDir: localHomeDir,
1607
- ...(initCodexProxyOptions
1608
- ? { proxyOptions: initCodexProxyOptions }
1609
- : {}),
1610
- onProgress: updateServices,
1611
- }),
1699
+ run: async () => {
1700
+ const threadId = await runLocalServicesScan({
1701
+ cwd: repoRoot,
1702
+ logDir,
1703
+ schemaPath: servicesSchemaPath,
1704
+ outputPath: servicesScanPath,
1705
+ homeDir: localHomeDir,
1706
+ ...(initCodexProxyOptions
1707
+ ? { proxyOptions: initCodexProxyOptions }
1708
+ : {}),
1709
+ onProgress: updateServices,
1710
+ });
1711
+ await saveScanThreadId("servicesThreadId", threadId);
1712
+ },
1612
1713
  read: async () => await readServicesPlan(servicesScanPath),
1613
1714
  outputPath: servicesScanPath,
1614
1715
  update: updateServices,
@@ -1620,6 +1721,7 @@ export const runInit = async (args) => {
1620
1721
  extraArtifactsPromise,
1621
1722
  servicesPromise,
1622
1723
  ]);
1724
+ await flushScanThreads();
1623
1725
  if (needsServicesScan) {
1624
1726
  if (!services) {
1625
1727
  throw new Error("Services scan missing.");
@@ -1779,6 +1881,136 @@ export const runInit = async (args) => {
1779
1881
  await updateInitState({ steps: backfillStepUpdate });
1780
1882
  }
1781
1883
  }
1884
+ const formatMissingSetupArtifact = (entry) => {
1885
+ const categoryLabel = entry.category === "envFiles" || entry.category === "secretFiles"
1886
+ ? "env/secrets"
1887
+ : entry.category === "externalConfigs"
1888
+ ? "external"
1889
+ : "extra artifacts";
1890
+ return `${categoryLabel}: ${entry.path}`;
1891
+ };
1892
+ const buildMissingPathsFeedback = (categoryLabel, entries) => [
1893
+ `Your previous ${categoryLabel} scan output included path(s) that do not exist on disk right now.`,
1894
+ "Fix the result and return corrected JSON. Only include paths that currently exist.",
1895
+ "Missing paths:",
1896
+ ...entries.map((entry) => `- ${entry.path} (resolved: ${toRepoRelativePath(repoRoot, entry.resolvedPath)})`),
1897
+ ].join("\n");
1898
+ const regenerateSetupPlanForMissingArtifacts = async ({ plan, missingArtifacts, status, }) => {
1899
+ const categories = new Set(missingArtifacts.map((entry) => entry.category));
1900
+ const needsEnvSecrets = categories.has("envFiles") || categories.has("secretFiles");
1901
+ const needsExternal = categories.has("externalConfigs");
1902
+ const needsExtraArtifacts = categories.has("extraArtifacts");
1903
+ let nextPlan = plan;
1904
+ if (needsEnvSecrets) {
1905
+ const envSecretsMissing = missingArtifacts.filter((entry) => entry.category === "envFiles" || entry.category === "secretFiles");
1906
+ const envSecretsSchemaPath = await writeSetupEnvSecretsSchema(setupTempDir);
1907
+ envSecretsScan = await runCodexScanWithImmediateRetry({
1908
+ run: async () => {
1909
+ const threadId = await runLocalSetupEnvSecretsScan({
1910
+ cwd: repoRoot,
1911
+ logDir,
1912
+ schemaPath: envSecretsSchemaPath,
1913
+ outputPath: setupEnvSecretsScanPath,
1914
+ ...(scanThreads.envSecretsThreadId
1915
+ ? { resumeThreadId: scanThreads.envSecretsThreadId }
1916
+ : {}),
1917
+ retryFeedback: buildMissingPathsFeedback("env/secrets", envSecretsMissing),
1918
+ ...(initCodexProxyOptions
1919
+ ? { proxyOptions: initCodexProxyOptions }
1920
+ : {}),
1921
+ onProgress: (message) => status.stage(`Regenerating env/secrets scan — ${message}`),
1922
+ });
1923
+ await saveScanThreadId("envSecretsThreadId", threadId);
1924
+ },
1925
+ read: async () => await readSetupEnvSecretsPlan(setupEnvSecretsScanPath),
1926
+ outputPath: setupEnvSecretsScanPath,
1927
+ update: (message) => status.stage(`Regenerating env/secrets scan — ${message}`),
1928
+ shouldRetry: (scan) => shouldRetryCodexScan(scan.scanFullyCompleted, scan.envFiles, scan.secretFiles),
1929
+ });
1930
+ nextPlan = {
1931
+ ...nextPlan,
1932
+ envFiles: remapSelectedPathEntries({
1933
+ selected: nextPlan.envFiles,
1934
+ refreshed: envSecretsScan.envFiles,
1935
+ }),
1936
+ secretFiles: remapSelectedPathEntries({
1937
+ selected: nextPlan.secretFiles,
1938
+ refreshed: envSecretsScan.secretFiles,
1939
+ }),
1940
+ };
1941
+ }
1942
+ if (needsExternal) {
1943
+ const externalMissing = missingArtifacts.filter((entry) => entry.category === "externalConfigs");
1944
+ const externalSchemaPath = await writeSetupExternalSchema(setupTempDir);
1945
+ externalScan = await runCodexScanWithImmediateRetry({
1946
+ run: async () => {
1947
+ const threadId = await runLocalSetupExternalScan({
1948
+ cwd: repoRoot,
1949
+ logDir,
1950
+ schemaPath: externalSchemaPath,
1951
+ outputPath: setupExternalScanPath,
1952
+ homeDir: localHomeDir,
1953
+ ...(scanThreads.externalThreadId
1954
+ ? { resumeThreadId: scanThreads.externalThreadId }
1955
+ : {}),
1956
+ retryFeedback: buildMissingPathsFeedback("external", externalMissing),
1957
+ ...(initCodexProxyOptions
1958
+ ? { proxyOptions: initCodexProxyOptions }
1959
+ : {}),
1960
+ onProgress: (message) => status.stage(`Regenerating external scan — ${message}`),
1961
+ });
1962
+ await saveScanThreadId("externalThreadId", threadId);
1963
+ },
1964
+ read: async () => await readSetupExternalPlan(setupExternalScanPath),
1965
+ outputPath: setupExternalScanPath,
1966
+ update: (message) => status.stage(`Regenerating external scan — ${message}`),
1967
+ shouldRetry: (scan) => shouldRetryCodexScan(scan.scanFullyCompleted, scan.externalDependencies, scan.externalConfigs),
1968
+ });
1969
+ nextPlan = {
1970
+ ...nextPlan,
1971
+ externalConfigs: remapSelectedPathEntries({
1972
+ selected: nextPlan.externalConfigs,
1973
+ refreshed: externalScan.externalConfigs,
1974
+ }),
1975
+ };
1976
+ }
1977
+ if (needsExtraArtifacts) {
1978
+ const extraArtifactsMissing = missingArtifacts.filter((entry) => entry.category === "extraArtifacts");
1979
+ const extraArtifactsSchemaPath = await writeSetupExtraArtifactsSchema(setupTempDir);
1980
+ extraArtifactsScan = await runCodexScanWithImmediateRetry({
1981
+ run: async () => {
1982
+ const threadId = await runLocalSetupExtraArtifactsScan({
1983
+ cwd: repoRoot,
1984
+ logDir,
1985
+ schemaPath: extraArtifactsSchemaPath,
1986
+ outputPath: setupExtraArtifactsScanPath,
1987
+ ...(scanThreads.extraArtifactsThreadId
1988
+ ? { resumeThreadId: scanThreads.extraArtifactsThreadId }
1989
+ : {}),
1990
+ retryFeedback: buildMissingPathsFeedback("extra artifacts", extraArtifactsMissing),
1991
+ ...(initCodexProxyOptions
1992
+ ? { proxyOptions: initCodexProxyOptions }
1993
+ : {}),
1994
+ onProgress: (message) => status.stage(`Regenerating extra artifacts scan — ${message}`),
1995
+ });
1996
+ await saveScanThreadId("extraArtifactsThreadId", threadId);
1997
+ },
1998
+ read: async () => await readSetupExtraArtifactsPlan(setupExtraArtifactsScanPath),
1999
+ outputPath: setupExtraArtifactsScanPath,
2000
+ update: (message) => status.stage(`Regenerating extra artifacts scan — ${message}`),
2001
+ shouldRetry: (scan) => shouldRetryCodexScan(scan.scanFullyCompleted, scan.extraArtifacts),
2002
+ });
2003
+ nextPlan = {
2004
+ ...nextPlan,
2005
+ extraArtifacts: remapSelectedPathEntries({
2006
+ selected: nextPlan.extraArtifacts,
2007
+ refreshed: extraArtifactsScan.extraArtifacts,
2008
+ }),
2009
+ };
2010
+ }
2011
+ await flushScanThreads();
2012
+ return nextPlan;
2013
+ };
1782
2014
  if (nonInteractive) {
1783
2015
  if (setupPlan)
1784
2016
  approvedPlan = setupPlan;
@@ -1938,7 +2170,7 @@ export const runInit = async (args) => {
1938
2170
  if (!approvedPlan) {
1939
2171
  throw new Error("Setup plan missing.");
1940
2172
  }
1941
- const ensuredPlan = approvedPlan;
2173
+ let ensuredPlan = approvedPlan;
1942
2174
  if (!skipSetupPlan) {
1943
2175
  await writeSetupPlan(setupPath, ensuredPlan);
1944
2176
  await updateInitState({ steps: { setupPlanWritten: true } });
@@ -1947,13 +2179,54 @@ export const runInit = async (args) => {
1947
2179
  setupArtifacts = await runInitStep({
1948
2180
  enabled: progressEnabled,
1949
2181
  title: "Packaging setup artifacts",
1950
- fn: async () => await createSetupArtifacts({
1951
- repoRoot,
1952
- plan: ensuredPlan,
1953
- outputDir: setupDir,
1954
- tempDir: setupTempDir,
1955
- homeDir: localHomeDir,
1956
- }),
2182
+ fn: async ({ status }) => {
2183
+ let planForArtifacts = ensuredPlan;
2184
+ for (let attempt = 1; attempt <= SETUP_ARTIFACT_REGEN_MAX_ATTEMPTS + 1; attempt += 1) {
2185
+ const missingArtifacts = await collectMissingSetupArtifacts({
2186
+ repoRoot,
2187
+ homeDir: localHomeDir,
2188
+ plan: planForArtifacts,
2189
+ });
2190
+ if (missingArtifacts.length === 0) {
2191
+ ensuredPlan = planForArtifacts;
2192
+ approvedPlan = planForArtifacts;
2193
+ return await createSetupArtifacts({
2194
+ repoRoot,
2195
+ plan: planForArtifacts,
2196
+ outputDir: setupDir,
2197
+ tempDir: setupTempDir,
2198
+ homeDir: localHomeDir,
2199
+ });
2200
+ }
2201
+ if (attempt > SETUP_ARTIFACT_REGEN_MAX_ATTEMPTS) {
2202
+ const lines = missingArtifacts
2203
+ .map((entry) => `- ${formatMissingSetupArtifact(entry)}`)
2204
+ .sort((a, b) => a.localeCompare(b));
2205
+ throw new Error([
2206
+ "Setup artifact scan returned paths that do not exist locally.",
2207
+ `Retried regeneration ${SETUP_ARTIFACT_REGEN_MAX_ATTEMPTS} time(s) and still found missing files:`,
2208
+ ...lines,
2209
+ ].join("\n"));
2210
+ }
2211
+ const needsEnvSecrets = missingArtifacts.some((entry) => entry.category === "envFiles" ||
2212
+ entry.category === "secretFiles");
2213
+ const needsExternal = missingArtifacts.some((entry) => entry.category === "externalConfigs");
2214
+ const needsExtraArtifacts = missingArtifacts.some((entry) => entry.category === "extraArtifacts");
2215
+ const scanLabels = [
2216
+ ...(needsEnvSecrets ? ["env/secrets"] : []),
2217
+ ...(needsExternal ? ["external"] : []),
2218
+ ...(needsExtraArtifacts ? ["extra artifacts"] : []),
2219
+ ];
2220
+ status.stage(`Found ${missingArtifacts.length} missing setup artifact path(s); regenerating ${scanLabels.join(", ")} scan(s) (${attempt}/${SETUP_ARTIFACT_REGEN_MAX_ATTEMPTS})`);
2221
+ planForArtifacts = await regenerateSetupPlanForMissingArtifacts({
2222
+ plan: planForArtifacts,
2223
+ missingArtifacts,
2224
+ status,
2225
+ });
2226
+ await writeSetupPlan(setupPath, planForArtifacts);
2227
+ }
2228
+ throw new Error("Unreachable setup artifacts retry path.");
2229
+ },
1957
2230
  });
1958
2231
  }
1959
2232
  }
@@ -1963,6 +2236,7 @@ export const runInit = async (args) => {
1963
2236
  if (!approvedPlan) {
1964
2237
  throw new Error("Setup plan missing.");
1965
2238
  }
2239
+ const finalApprovedPlan = approvedPlan;
1966
2240
  const skipProvision = shouldResume && initState?.steps.workdirProvisioned;
1967
2241
  if (skipProvision && !skipSetupUpload) {
1968
2242
  await runInitStep({
@@ -1976,12 +2250,16 @@ export const runInit = async (args) => {
1976
2250
  remoteSetupPath,
1977
2251
  localArtifactsBundlePath: setupArtifacts?.bundlePath ?? null,
1978
2252
  localArtifactsManifestPath: setupArtifacts?.manifestPath ?? null,
2253
+ localArtifactsPartsDescriptorPath: setupArtifacts?.partsDescriptorPath ?? null,
1979
2254
  remoteArtifactsBundlePath: setupArtifacts
1980
2255
  ? path.posix.join(expandedWorkdir, ".devbox", "setup-artifacts.tgz")
1981
2256
  : null,
1982
2257
  remoteArtifactsManifestPath: setupArtifacts
1983
2258
  ? path.posix.join(expandedWorkdir, ".devbox", "setup-artifacts.json")
1984
2259
  : null,
2260
+ remoteArtifactsPartsDescriptorPath: setupArtifacts
2261
+ ? remoteArtifactsPartsDescriptorPath
2262
+ : null,
1985
2263
  status,
1986
2264
  });
1987
2265
  },
@@ -2244,12 +2522,16 @@ export const runInit = async (args) => {
2244
2522
  remoteSetupPath,
2245
2523
  localArtifactsBundlePath: setupArtifacts?.bundlePath ?? null,
2246
2524
  localArtifactsManifestPath: setupArtifacts?.manifestPath ?? null,
2525
+ localArtifactsPartsDescriptorPath: setupArtifacts?.partsDescriptorPath ?? null,
2247
2526
  remoteArtifactsBundlePath: setupArtifacts
2248
2527
  ? path.posix.join(expandedWorkdir, ".devbox", "setup-artifacts.tgz")
2249
2528
  : null,
2250
2529
  remoteArtifactsManifestPath: setupArtifacts
2251
2530
  ? path.posix.join(expandedWorkdir, ".devbox", "setup-artifacts.json")
2252
2531
  : null,
2532
+ remoteArtifactsPartsDescriptorPath: setupArtifacts
2533
+ ? remoteArtifactsPartsDescriptorPath
2534
+ : null,
2253
2535
  status,
2254
2536
  });
2255
2537
  },
@@ -2566,7 +2848,7 @@ export const runInit = async (args) => {
2566
2848
  client,
2567
2849
  canonical,
2568
2850
  workdir: expandedWorkdir,
2569
- services: approvedPlan.services.backgroundServices,
2851
+ services: finalApprovedPlan.services.backgroundServices,
2570
2852
  });
2571
2853
  await updateInitState({ steps: { servicesConfigWritten: true } });
2572
2854
  },
@@ -2703,7 +2985,7 @@ export const runInit = async (args) => {
2703
2985
  await enableRemoteServices({
2704
2986
  client,
2705
2987
  canonical,
2706
- services: approvedPlan.services.backgroundServices,
2988
+ services: finalApprovedPlan.services.backgroundServices,
2707
2989
  status,
2708
2990
  });
2709
2991
  await updateInitState({ steps: { servicesEnabled: true } });
@@ -2724,7 +3006,7 @@ export const runInit = async (args) => {
2724
3006
  socketInfo,
2725
3007
  status,
2726
3008
  pathSetup,
2727
- entrypoints: approvedPlan.services.appEntrypoints,
3009
+ entrypoints: finalApprovedPlan.services.appEntrypoints,
2728
3010
  emitCodexOutput: !parsed.json,
2729
3011
  ...(initCodexProxyOptions
2730
3012
  ? { proxyOptions: initCodexProxyOptions }