@headband/cli 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +36 -16
  2. package/dist/index.js +588 -91
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -35,14 +35,15 @@ __export(index_exports, {
35
35
  main: () => main,
36
36
  parseArgv: () => parseArgv,
37
37
  parseExpires: () => parseExpires,
38
+ parseIndexList: () => parseIndexList,
38
39
  parsePermissions: () => parsePermissions,
39
40
  run: () => run,
40
41
  stdinIsInteractive: () => stdinIsInteractive,
41
42
  syncClientFromFlags: () => syncClientFromFlags
42
43
  });
43
44
  module.exports = __toCommonJS(index_exports);
44
- var import_fs11 = require("fs");
45
45
  var import_fs12 = require("fs");
46
+ var import_fs13 = require("fs");
46
47
  var import_path11 = require("path");
47
48
  var import_prompts2 = __toESM(require("prompts"));
48
49
  var import_picocolors3 = __toESM(require("picocolors"));
@@ -527,7 +528,7 @@ function errorMessageOf(status, errorBody) {
527
528
  }
528
529
  return message;
529
530
  }
530
- var defaultSleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
531
+ var defaultSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
531
532
  async function readJson(response) {
532
533
  const text = await response.text();
533
534
  if (!text) return {};
@@ -547,7 +548,7 @@ async function apiRequest(baseUrl, path2, init2 = {}) {
547
548
  headers.set("authorization", `Bearer ${init2.apiKey}`);
548
549
  }
549
550
  const base = normalizeUrl(baseUrl);
550
- const { rateLimitRetries = 0, sleep = defaultSleep, apiKey: _apiKey, ...fetchInit } = init2;
551
+ const { rateLimitRetries = 0, sleep: sleep2 = defaultSleep, apiKey: _apiKey, ...fetchInit } = init2;
551
552
  void _apiKey;
552
553
  let response;
553
554
  try {
@@ -560,7 +561,7 @@ async function apiRequest(baseUrl, path2, init2 = {}) {
560
561
  const wait = response.status === 429 ? retryAfterSeconds(response) : null;
561
562
  if (wait === null || attempt >= rateLimitRetries) break;
562
563
  await response.body?.cancel().catch(() => void 0);
563
- await sleep(wait * 1e3);
564
+ await sleep2(wait * 1e3);
564
565
  }
565
566
  } catch (error) {
566
567
  const cause = error?.cause;
@@ -599,7 +600,7 @@ function createApiClient(url, profile) {
599
600
  }
600
601
 
601
602
  // src/auth.ts
602
- var defaultSleep2 = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
603
+ var defaultSleep2 = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
603
604
  function openBrowser(url) {
604
605
  try {
605
606
  if (process.platform === "darwin") {
@@ -614,7 +615,7 @@ function openBrowser(url) {
614
615
  }
615
616
  async function runDeviceFlow(url, io, options = {}) {
616
617
  const baseUrl = normalizeUrl(url);
617
- const sleep = options.sleep || defaultSleep2;
618
+ const sleep2 = options.sleep || defaultSleep2;
618
619
  const now = options.now || Date.now;
619
620
  const device = await apiRequest(baseUrl, "/api/cli/device", {
620
621
  method: "POST",
@@ -629,7 +630,7 @@ async function runDeviceFlow(url, io, options = {}) {
629
630
  let interval = device.data.interval;
630
631
  const deadline = now() + device.data.expiresIn * 1e3;
631
632
  while (now() < deadline) {
632
- await sleep(interval * 1e3);
633
+ await sleep2(interval * 1e3);
633
634
  try {
634
635
  const token = await apiRequest(baseUrl, "/api/cli/token", {
635
636
  method: "POST",
@@ -684,6 +685,7 @@ async function login(url, io, options = {}) {
684
685
  }
685
686
 
686
687
  // src/branch.ts
688
+ var import_fs4 = require("fs");
687
689
  var import_path5 = require("path");
688
690
 
689
691
  // src/output.ts
@@ -893,14 +895,22 @@ var PART_2 = /* @__PURE__ */ new Set(["reset", "rollback", "restore", "set-prima
893
895
  var BRANCH_USAGE = [
894
896
  "Usage:",
895
897
  " headband branch list [--project <id>] [--json]",
896
- " headband branch create <name> --empty [--parent <name>] [--expires-in <2h|7d>] [--project <id>] [--json]",
898
+ " headband branch create <name> [--empty] [--parent <name>] [--expires-in <2h|7d>] [--project <id>] [--json]",
899
+ " headband branch ensure <name> [--from <parent>] [--expires-in <7d>] [--env-out <file>] [--project <id>] [--json]",
897
900
  " headband branch delete <name> [--project <id>] [--yes] [--json]",
898
901
  " headband branch protect <name> [--project <id>] [--json]",
899
902
  " headband branch unprotect <name> [--project <id>] [--json]",
900
903
  " headband branch expire <name> (--in <2h|7d> | --at <iso> | --clear) [--project <id>] [--json]",
901
904
  " headband branch checkout <name> [--project <id>] [--json]",
902
- " headband branch create <name> (without --empty, a fork) coming with bucket storage",
903
- " headband branch reset|rollback|restore|set-primary <name> coming with bucket storage",
905
+ " headband branch reset|rollback|restore|set-primary <name> not available yet",
906
+ "",
907
+ "create without --empty forks the parent (default: main): its indexes, documents and settings,",
908
+ "copy-on-write. --empty makes a branch with no indexes.",
909
+ "",
910
+ "ensure is for preview deploys: it forks the branch unless it exists, pushes its expiry out",
911
+ "(--expires-in, default 7d), mints a search key and an admin key for it, and writes HEADBAND_*",
912
+ "variables to --env-out (or prints them). The name is made branch-safe first: feature/Foo_Bar",
913
+ "becomes feature/foo_bar. It refuses the primary branch.",
904
914
  "",
905
915
  "The project comes from --project, else the directory's checkout (.headband/project.json).",
906
916
  "Durations take s, m, h, d or w: 30m, 2h, 7d.",
@@ -999,10 +1009,7 @@ async function runBranch(args, flags, deps) {
999
1009
  const action = args[0];
1000
1010
  const json = flags["--json"] === true;
1001
1011
  if (action && PART_2.has(action)) {
1002
- throw new UsageError(
1003
- `headband branch ${action} is coming with bucket storage.`,
1004
- "not_available"
1005
- );
1012
+ throw new UsageError(`headband branch ${action} is not available yet.`, "not_available");
1006
1013
  }
1007
1014
  const client = createApiClient(auth.url, { apiKey: auth.apiKey });
1008
1015
  const checkout = () => checkoutFor(cwd, auth.profile?.orgId);
@@ -1031,18 +1038,35 @@ async function runBranch(args, flags, deps) {
1031
1038
  if (error instanceof ApiError && error.code === "branches_not_open") {
1032
1039
  throw restate(error, "Branches other than main are not open yet.");
1033
1040
  }
1034
- if (error instanceof ApiError && error.code === "fork_not_available") {
1035
- throw restate(
1036
- error,
1037
- "Forking arrives with bucket storage. Pass --empty for an empty branch."
1038
- );
1039
- }
1040
- throw error;
1041
+ throw restateCreate(error);
1041
1042
  }
1042
1043
  if (json) printJson(io, response);
1043
1044
  else printTable(io, [response.data], COLUMNS);
1044
1045
  return;
1045
1046
  }
1047
+ if (action === "ensure") {
1048
+ const result = await ensureBranch(client, {
1049
+ name: branchNameFrom(requiredName(args[1])),
1050
+ project: projectFor(flags, checkout),
1051
+ from: stringFlag2(flags, "--from"),
1052
+ expiresInMs: parseDuration(stringFlag2(flags, "--expires-in") ?? "7d"),
1053
+ url: auth.url
1054
+ });
1055
+ const envOut = stringFlag2(flags, "--env-out");
1056
+ if (envOut && envOut !== "-") writeEnvFile((0, import_path5.resolve)(cwd, envOut), result.env);
1057
+ if (json) {
1058
+ printJson(io, { branch: result.branch, created: result.created, env: result.env });
1059
+ } else if (envOut && envOut !== "-") {
1060
+ const engines = result.branch.instances?.length ?? 0;
1061
+ io.stdout(
1062
+ `${result.created ? "Forked" : "Reused"} branch ${result.branch.name} (${engines} engine${engines === 1 ? "" : "s"}), expires ${result.branch.expiresAt}.`
1063
+ );
1064
+ io.stdout(`Wrote ${Object.keys(result.env).length} HEADBAND_* variables to ${envOut}.`);
1065
+ } else {
1066
+ io.stdout(dotenv(result.env));
1067
+ }
1068
+ return;
1069
+ }
1046
1070
  if (action === "delete") {
1047
1071
  const name = requiredName(args[1]);
1048
1072
  const project = projectFor(flags, checkout);
@@ -1114,9 +1138,109 @@ async function runBranch(args, flags, deps) {
1114
1138
  io.stdout(BRANCH_USAGE);
1115
1139
  throw new UsageError("Unknown branch command");
1116
1140
  }
1141
+ function restateCreate(error) {
1142
+ if (error instanceof ApiError && error.code === "branches_not_open") {
1143
+ return restate(error, "Branches other than main are not open yet.");
1144
+ }
1145
+ if (error instanceof ApiError && error.code === "fork_not_available") {
1146
+ return restate(error, "Forking is not open yet. Pass --empty for an empty branch.");
1147
+ }
1148
+ return error;
1149
+ }
1150
+ function branchNameFrom(ref) {
1151
+ const name = ref.trim().toLowerCase().replace(/[^a-z0-9._/-]+/g, "-").slice(0, 63).replace(/^[^a-z0-9]+/, "").replace(/[^a-z0-9]+$/, "");
1152
+ if (!name) throw new UsageError(`"${ref}" has no letters or digits to make a branch name from.`);
1153
+ return name;
1154
+ }
1155
+ var ENSURE_WAIT_MS = 6e4;
1156
+ var ENSURE_POLL_MS = 2e3;
1157
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1158
+ var engineVar = (engine) => `HEADBAND_${engine.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_URL`;
1159
+ async function ensureBranch(client, o, now = Date.now) {
1160
+ var _a;
1161
+ const path2 = `/v1/branches/${encodeURIComponent(o.name)}?project=${encodeURIComponent(o.project)}`;
1162
+ const read = async () => {
1163
+ try {
1164
+ return (await client.get(path2)).data;
1165
+ } catch (error) {
1166
+ if (error instanceof ApiError && error.status === 404 && error.code === "branch_not_found") {
1167
+ return null;
1168
+ }
1169
+ throw error;
1170
+ }
1171
+ };
1172
+ const expiresAt = () => new Date(now() + o.expiresInMs).toISOString();
1173
+ let branch = await read();
1174
+ let created = false;
1175
+ if (!branch) {
1176
+ const body = {
1177
+ name: o.name,
1178
+ projectId: o.project,
1179
+ expiresAt: expiresAt()
1180
+ };
1181
+ if (o.from) body.parent = o.from;
1182
+ try {
1183
+ branch = (await client.post("/v1/branches", body)).data;
1184
+ created = true;
1185
+ } catch (error) {
1186
+ if (!(error instanceof ApiError && error.code === "branch_exists"))
1187
+ throw restateCreate(error);
1188
+ branch = await read();
1189
+ if (!branch) throw error;
1190
+ }
1191
+ }
1192
+ if (branch.primary) {
1193
+ throw new UsageError(
1194
+ `${branch.name} is the primary branch. ensure is for preview branches; run it only on preview builds.`
1195
+ );
1196
+ }
1197
+ for (const until = now() + ENSURE_WAIT_MS; branch.state === "creating" && now() < until; ) {
1198
+ await sleep(ENSURE_POLL_MS);
1199
+ branch = await read() ?? branch;
1200
+ }
1201
+ if (branch.state !== "ready") {
1202
+ throw new CliError(`Branch ${branch.name} is ${branch.state}, not ready. Try again shortly.`, {
1203
+ exitCode: EXIT_CODES.conflict,
1204
+ code: "branch_not_ready"
1205
+ });
1206
+ }
1207
+ if (!created && !branch.protected) {
1208
+ branch = {
1209
+ ...branch,
1210
+ ...(await client.patch(path2, { expiresAt: expiresAt() })).data
1211
+ };
1212
+ }
1213
+ if (!branch.instances)
1214
+ branch = { ...branch, ...(await client.get(path2)).data };
1215
+ const mint = async (type) => (await client.post("/v1/keys", {
1216
+ name: `preview ${branch.name} ${type}`,
1217
+ type,
1218
+ projectId: o.project,
1219
+ branch: branch.id
1220
+ })).data.apiKey;
1221
+ const env = {
1222
+ HEADBAND_URL: o.url,
1223
+ HEADBAND_PROJECT_ID: o.project,
1224
+ HEADBAND_BRANCH: branch.name,
1225
+ HEADBAND_BRANCH_ID: branch.id,
1226
+ HEADBAND_SEARCH_KEY: await mint("search"),
1227
+ HEADBAND_ADMIN_KEY: await mint("admin")
1228
+ };
1229
+ for (const i of branch.instances ?? []) env[_a = engineVar(i.engine)] ?? (env[_a] = i.endpoint);
1230
+ return { branch, created, env };
1231
+ }
1232
+ var quote = (v) => /^[A-Za-z0-9_./:@+-]*$/.test(v) ? v : JSON.stringify(v);
1233
+ function dotenv(env) {
1234
+ return Object.entries(env).map(([k, v]) => `${k}=${quote(v)}`).join("\n");
1235
+ }
1236
+ function writeEnvFile(file, env) {
1237
+ const kept = (0, import_fs4.existsSync)(file) ? (0, import_fs4.readFileSync)(file, "utf8").split("\n").filter((line) => line.trim() !== "" && !/^\s*(export\s+)?HEADBAND_[A-Z0-9_]*=/.test(line)) : [];
1238
+ (0, import_fs4.writeFileSync)(file, [...kept, dotenv(env)].join("\n") + "\n", { mode: 384 });
1239
+ (0, import_fs4.chmodSync)(file, 384);
1240
+ }
1117
1241
 
1118
1242
  // src/config-load.ts
1119
- var import_fs4 = require("fs");
1243
+ var import_fs5 = require("fs");
1120
1244
  var import_path6 = require("path");
1121
1245
  var import_url = require("url");
1122
1246
  var import_jiti = require("jiti");
@@ -1202,7 +1326,7 @@ function validateConfig(config, ormSchema) {
1202
1326
  continue;
1203
1327
  }
1204
1328
  const hasPath = model.relations.some(
1205
- (r) => r.fromModel === spec.model && r.toModel === index.source && r.kind === "many"
1329
+ (r) => r.fromModel === index.source && r.toModel === spec.model && r.kind === "many"
1206
1330
  );
1207
1331
  if (!hasPath) {
1208
1332
  errors.push({
@@ -1252,7 +1376,7 @@ var SYNC_CONFIG_SHAPE = 'a headband.config.ts whose default export is defineConf
1252
1376
  function findConfigFile(cwd) {
1253
1377
  for (const name of CONFIG_FILENAMES) {
1254
1378
  const path2 = (0, import_path6.join)(cwd, name);
1255
- if ((0, import_fs4.existsSync)(path2)) return path2;
1379
+ if ((0, import_fs5.existsSync)(path2)) return path2;
1256
1380
  }
1257
1381
  return void 0;
1258
1382
  }
@@ -1372,10 +1496,10 @@ function missingAdapterError(command, why) {
1372
1496
 
1373
1497
  // src/docs.ts
1374
1498
  var import_path7 = require("path");
1375
- var import_fs5 = require("fs");
1499
+ var import_fs6 = require("fs");
1376
1500
  var DOCS_BATCH_SIZE = 1e3;
1377
1501
  function parseDocumentsFile(path2) {
1378
- const content = (0, import_fs5.readFileSync)(path2, "utf-8");
1502
+ const content = (0, import_fs6.readFileSync)(path2, "utf-8");
1379
1503
  const ext = (0, import_path7.extname)(path2).toLowerCase();
1380
1504
  if (ext === ".jsonl") {
1381
1505
  return content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
@@ -1488,6 +1612,67 @@ var ExportInterruptedError = class extends Error {
1488
1612
  this.name = "ExportInterruptedError";
1489
1613
  }
1490
1614
  };
1615
+ function refusedWalk(error) {
1616
+ if (!(error instanceof ApiError) || error.code !== "feature_not_supported") return null;
1617
+ if (!/primary key/i.test(error.message)) return null;
1618
+ const pk = /add `([^`]+)`/.exec(error.message)?.[1] ?? null;
1619
+ const named = ["filterableAttributes", "sortableAttributes", "displayedAttributes"].filter(
1620
+ (name) => error.message.includes(name)
1621
+ );
1622
+ return { pk, lists: named.length ? named : ["filterableAttributes", "sortableAttributes"] };
1623
+ }
1624
+ var DECLARE_WAIT_MS = 15 * 6e4;
1625
+ var DECLARE_POLL_MS = 3e3;
1626
+ async function declareExportKey(client, index, io, opts = {}) {
1627
+ const sleep2 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
1628
+ const now = opts.now ?? Date.now;
1629
+ const probe = () => client.get(`/v1/documents?index=${encodeURIComponent(index)}&limit=1&cursor=`);
1630
+ let refused;
1631
+ try {
1632
+ await probe();
1633
+ return false;
1634
+ } catch (error) {
1635
+ refused = refusedWalk(error);
1636
+ if (!refused) throw error;
1637
+ if (!refused.pk) throw error;
1638
+ }
1639
+ const { pk, lists } = refused;
1640
+ const path2 = `/v1/indexes/${encodeURIComponent(index)}/settings`;
1641
+ const current = await client.get(path2);
1642
+ const patch = {};
1643
+ for (const name of lists) {
1644
+ const list = Array.isArray(current?.[name]) ? current[name] : [];
1645
+ if (list.includes("*") || list.includes(pk)) continue;
1646
+ patch[name] = [...list, pk];
1647
+ }
1648
+ io.stderr(
1649
+ `Declaring \`${pk}\` in ${Object.keys(patch).join(" and ") || lists.join(" and ")} on ${index}, so the export can read past 1000 documents. Visor rebuilds the index for this change, which can take minutes on a large one.`
1650
+ );
1651
+ if (Object.keys(patch).length > 0) {
1652
+ try {
1653
+ await client.patch(path2, patch);
1654
+ } catch (error) {
1655
+ if (!(error instanceof ApiError) || error.status < 500) throw error;
1656
+ }
1657
+ }
1658
+ const deadline = now() + (opts.waitMs ?? DECLARE_WAIT_MS);
1659
+ for (; ; ) {
1660
+ try {
1661
+ await probe();
1662
+ io.stderr(`\`${pk}\` is declared. Exporting.`);
1663
+ return true;
1664
+ } catch (error) {
1665
+ const waiting = refusedWalk(error) || error instanceof ApiError && error.status >= 500;
1666
+ if (!waiting) throw error;
1667
+ }
1668
+ if (now() >= deadline) {
1669
+ throw new CliError(
1670
+ `\`${pk}\` is still not declared on ${index} after ${Math.round((opts.waitMs ?? DECLARE_WAIT_MS) / 6e4)} minutes. Visor may still be rebuilding it: run headband docs export again later.`
1671
+ );
1672
+ }
1673
+ await sleep2(DECLARE_POLL_MS);
1674
+ }
1675
+ }
1491
1676
  function exportRefusalAdvice(error, index) {
1492
1677
  if (!(error instanceof ApiError) || error.code !== "feature_not_supported") return null;
1493
1678
  if (!/primary key/i.test(error.message)) return null;
@@ -1498,7 +1683,8 @@ function exportRefusalAdvice(error, index) {
1498
1683
  );
1499
1684
  const lists = missing.length ? missing.join(" and ") : "filterableAttributes and sortableAttributes";
1500
1685
  return [
1501
- `To export the whole index, declare ${key} on it once:`,
1686
+ `To export the whole index, run it again with --declare-key: it declares ${key} on the index`,
1687
+ `once (Visor rebuilds the index for it), waits for it, and exports. Or declare it yourself:`,
1502
1688
  ` 1. headband indexes settings get ${index} > settings.json`,
1503
1689
  ` 2. Add ${key} to ${lists} in settings.json.`,
1504
1690
  ` 3. headband indexes settings set ${index} --file settings.json`,
@@ -1514,6 +1700,41 @@ function exportFailure(error, index) {
1514
1700
  ${advice}`);
1515
1701
  }
1516
1702
 
1703
+ // src/content-drift.ts
1704
+ async function readRemoteDocuments(client, index, primaryKey) {
1705
+ const batchSize = await defaultExportBatchSize(client, index);
1706
+ const docs = /* @__PURE__ */ new Map();
1707
+ for await (const { results } of walkDocuments(client, { index, batchSize })) {
1708
+ for (const doc of results) {
1709
+ const id = doc?.[primaryKey];
1710
+ if (id !== void 0 && id !== null) docs.set(String(id), doc);
1711
+ }
1712
+ }
1713
+ return docs;
1714
+ }
1715
+ function wire(value) {
1716
+ return value === void 0 ? null : JSON.parse(JSON.stringify(value));
1717
+ }
1718
+ function canonical(value) {
1719
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
1720
+ if (value !== null && typeof value === "object") {
1721
+ const entries = Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${canonical(value[k])}`);
1722
+ return `{${entries.join(",")}}`;
1723
+ }
1724
+ return JSON.stringify(value ?? null);
1725
+ }
1726
+ function documentsMatch(expected, remote, primaryKey) {
1727
+ if (!remote) return false;
1728
+ for (const [key, raw] of Object.entries(expected)) {
1729
+ if (key === primaryKey) {
1730
+ if (String(raw) !== String(remote[key])) return false;
1731
+ continue;
1732
+ }
1733
+ if (canonical(wire(raw)) !== canonical(wire(remote[key]))) return false;
1734
+ }
1735
+ return true;
1736
+ }
1737
+
1517
1738
  // ../../src/lib/density/catalog-constants.ts
1518
1739
  function formatMemory(mb) {
1519
1740
  return mb >= 1024 ? `${mb / 1024} GB` : `${mb} MB`;
@@ -2069,7 +2290,7 @@ async function runSwitchEngine(client, opts, io, chatter) {
2069
2290
  );
2070
2291
  return;
2071
2292
  }
2072
- const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
2293
+ const sleep2 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
2073
2294
  let lastPhase = "";
2074
2295
  for (; ; ) {
2075
2296
  if (view.phase !== lastPhase) {
@@ -2077,7 +2298,7 @@ async function runSwitchEngine(client, opts, io, chatter) {
2077
2298
  chatter.stdout(`${opts.uid}: ${PHASES[view.phase] ?? view.phase}`);
2078
2299
  }
2079
2300
  if (view.status === "succeeded" || view.status === "failed") break;
2080
- await sleep(opts.pollMs ?? 3e3);
2301
+ await sleep2(opts.pollMs ?? 3e3);
2081
2302
  const status = await client.get(path2);
2082
2303
  if (!status.switch || status.switch.switchId !== view.switchId) {
2083
2304
  throw new CliError("The switch is no longer listed for this index.", {
@@ -2214,11 +2435,11 @@ function planMigration(config, applied, opts) {
2214
2435
  }
2215
2436
 
2216
2437
  // src/schema.ts
2217
- var import_fs7 = require("fs");
2438
+ var import_fs8 = require("fs");
2218
2439
  var import_path9 = require("path");
2219
2440
 
2220
2441
  // ../sync/src/migrations.ts
2221
- var import_fs6 = require("fs");
2442
+ var import_fs7 = require("fs");
2222
2443
  var import_path8 = require("path");
2223
2444
  var FILE_NAME_RE = /^(\d{4})_(.+)\.json$/;
2224
2445
  function sanitizeSlug(slug) {
@@ -2226,7 +2447,7 @@ function sanitizeSlug(slug) {
2226
2447
  return sanitized || "migration";
2227
2448
  }
2228
2449
  function generateMigrationFile(dir, ops, meta, now = () => /* @__PURE__ */ new Date()) {
2229
- (0, import_fs6.mkdirSync)(dir, { recursive: true });
2450
+ (0, import_fs7.mkdirSync)(dir, { recursive: true });
2230
2451
  const existing = readMigrations(dir);
2231
2452
  const nextVersion = existing.length === 0 ? 1 : existing[existing.length - 1].version + 1;
2232
2453
  const safeSlug = sanitizeSlug(meta.slug);
@@ -2239,13 +2460,13 @@ function generateMigrationFile(dir, ops, meta, now = () => /* @__PURE__ */ new D
2239
2460
  ...meta.targetHashes ? { targetHashes: meta.targetHashes } : {},
2240
2461
  ops
2241
2462
  };
2242
- (0, import_fs6.writeFileSync)(path2, JSON.stringify(file, null, 2));
2463
+ (0, import_fs7.writeFileSync)(path2, JSON.stringify(file, null, 2));
2243
2464
  return path2;
2244
2465
  }
2245
2466
  function readMigrations(dir) {
2246
2467
  let entries;
2247
2468
  try {
2248
- entries = (0, import_fs6.readdirSync)(dir);
2469
+ entries = (0, import_fs7.readdirSync)(dir);
2249
2470
  } catch {
2250
2471
  return [];
2251
2472
  }
@@ -2254,7 +2475,7 @@ function readMigrations(dir) {
2254
2475
  const match = FILE_NAME_RE.exec(entry);
2255
2476
  if (!match) continue;
2256
2477
  const path2 = (0, import_path8.join)(dir, entry);
2257
- const raw = (0, import_fs6.readFileSync)(path2, "utf-8");
2478
+ const raw = (0, import_fs7.readFileSync)(path2, "utf-8");
2258
2479
  let parsed;
2259
2480
  try {
2260
2481
  parsed = JSON.parse(raw);
@@ -2284,7 +2505,7 @@ function pendingMigrations(files, appliedVersion) {
2284
2505
  var BASE_BACKOFF_MS = 500;
2285
2506
  var DEFAULT_MAX_RETRIES = 5;
2286
2507
  function defaultSleep3(ms) {
2287
- return new Promise((resolve2) => setTimeout(resolve2, ms));
2508
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
2288
2509
  }
2289
2510
  var SchemaCasError = class extends Error {
2290
2511
  constructor(message) {
@@ -2357,6 +2578,14 @@ var HeadbandClient = class {
2357
2578
  if (opts?.projectId !== void 0) body.projectId = opts.projectId;
2358
2579
  await this.request("POST", "/v1/indexes", body);
2359
2580
  }
2581
+ /** The index (GET /v1/indexes/{uid}): its engine among the rest. */
2582
+ async getIndex(uid) {
2583
+ return this.request("GET", `/v1/indexes/${encodeURIComponent(uid)}`);
2584
+ }
2585
+ /** Deletes an index (DELETE /v1/indexes/{uid}): `schema apply` removes a reindex it could not set up. */
2586
+ async deleteIndex(uid) {
2587
+ await this.request("DELETE", `/v1/indexes/${encodeURIComponent(uid)}`);
2588
+ }
2360
2589
  async patchSettings(uid, settings) {
2361
2590
  await this.request("PATCH", `/v1/indexes/${uid}/settings`, settings);
2362
2591
  }
@@ -2443,17 +2672,17 @@ function printOpsTable(io, ops) {
2443
2672
  }
2444
2673
  function readAppliedMarker(dir) {
2445
2674
  const path2 = (0, import_path9.join)(dir, APPLIED_MARKER_FILE);
2446
- if (!(0, import_fs7.existsSync)(path2)) return 0;
2675
+ if (!(0, import_fs8.existsSync)(path2)) return 0;
2447
2676
  try {
2448
- const parsed = JSON.parse((0, import_fs7.readFileSync)(path2, "utf-8"));
2677
+ const parsed = JSON.parse((0, import_fs8.readFileSync)(path2, "utf-8"));
2449
2678
  return typeof parsed.version === "number" ? parsed.version : 0;
2450
2679
  } catch {
2451
2680
  return 0;
2452
2681
  }
2453
2682
  }
2454
2683
  function writeAppliedMarker(dir, version) {
2455
- (0, import_fs7.mkdirSync)(dir, { recursive: true });
2456
- (0, import_fs7.writeFileSync)((0, import_path9.join)(dir, APPLIED_MARKER_FILE), JSON.stringify({ version }, null, 2));
2684
+ (0, import_fs8.mkdirSync)(dir, { recursive: true });
2685
+ (0, import_fs8.writeFileSync)((0, import_path9.join)(dir, APPLIED_MARKER_FILE), JSON.stringify({ version }, null, 2));
2457
2686
  }
2458
2687
  function configHashes(config, index) {
2459
2688
  const def = config.indexes.find((d) => d.name === index);
@@ -2512,7 +2741,36 @@ async function createIndexIdempotent(ctx, uid, opts) {
2512
2741
  ctx.io.stdout(`Index "${uid}" already exists. Continuing.`);
2513
2742
  }
2514
2743
  }
2744
+ var ES_EVERY_FIELD = [
2745
+ "searchableAttributes",
2746
+ "filterableAttributes",
2747
+ "sortableAttributes"
2748
+ ];
2749
+ async function indexEngine(ctx, uid) {
2750
+ try {
2751
+ return (await ctx.client.getIndex(uid)).engine ?? null;
2752
+ } catch {
2753
+ return null;
2754
+ }
2755
+ }
2756
+ function settingsForEngine(ctx, uid, engine, settings) {
2757
+ if (engine !== "elasticsearch") return settings;
2758
+ const out = { ...settings };
2759
+ const left = [];
2760
+ for (const key of ES_EVERY_FIELD) {
2761
+ if (out[key] === void 0) continue;
2762
+ delete out[key];
2763
+ left.push(key);
2764
+ }
2765
+ if (left.length > 0) {
2766
+ ctx.io.stdout(
2767
+ `"${uid}" is on Elasticsearch, which searches, filters and sorts every field without these lists, so they are not sent: ${left.join(", ")}.`
2768
+ );
2769
+ }
2770
+ return out;
2771
+ }
2515
2772
  async function patchSettingsBeforeBackfill(ctx, uid, settings) {
2773
+ if (Object.keys(settings).length === 0) return false;
2516
2774
  try {
2517
2775
  await ctx.client.patchSettings(uid, settings);
2518
2776
  return false;
@@ -2536,13 +2794,14 @@ async function patchSettingsAfterBackfill(ctx, uid, settings) {
2536
2794
  function settingsRefused(uid, err) {
2537
2795
  const message = err instanceof Error ? err.message : String(err);
2538
2796
  const { status, code } = err ?? {};
2539
- return new CliError(
2797
+ const refused = new CliError(
2540
2798
  `Index "${uid}" exists, but its settings were refused: ${message.replace(/\.?$/, ".")} Change the index's settings in headband.config.ts, then re-run "headband schema apply": it picks up the existing index.`,
2541
2799
  {
2542
2800
  ...typeof status === "number" ? { exitCode: exitCodeForStatus(status) } : {},
2543
2801
  ...typeof code === "string" ? { code } : {}
2544
2802
  }
2545
2803
  );
2804
+ return Object.assign(refused, { refusal: message });
2546
2805
  }
2547
2806
  function postState(client, state) {
2548
2807
  return client.postSchemaState(state);
@@ -2590,13 +2849,83 @@ async function backfill(ctx, def, targetUid, abortPoint, onPage) {
2590
2849
  );
2591
2850
  return { pushed, excluded };
2592
2851
  }
2852
+ async function catchUp(ctx, def, targetUid, when) {
2853
+ if (!ctx.readRemoteDocuments) return;
2854
+ const remote = await ctx.readRemoteDocuments(targetUid, def.primaryKey);
2855
+ const keep = /* @__PURE__ */ new Set();
2856
+ let pushed = 0;
2857
+ let cursor = null;
2858
+ for (; ; ) {
2859
+ const page = await ctx.adapter.listIds(def.source, cursor, ctx.backfillBatchSize);
2860
+ if (page.ids.length === 0) break;
2861
+ const docs = await ctx.adapter.resolveDocs(def.name, page.ids);
2862
+ const toPush = [];
2863
+ for (const doc of docs) {
2864
+ const sourceRow = doc.__source ?? doc;
2865
+ delete doc.__source;
2866
+ if (def.shouldIndex && !def.shouldIndex(sourceRow)) continue;
2867
+ const key = String(doc[def.primaryKey]);
2868
+ keep.add(key);
2869
+ if (!documentsMatch(doc, remote.get(key), def.primaryKey)) toPush.push(doc);
2870
+ }
2871
+ if (toPush.length > 0) {
2872
+ const result = await ctx.client.bulkPush(targetUid, toPush, def.primaryKey);
2873
+ if (result?.partialFailure) {
2874
+ throw new CliError(
2875
+ `Catching up "${def.name}" ${when} stopped: ${partialFailureSummary(result.partialFailure)}. Re-run "headband schema apply" once resolved.`
2876
+ );
2877
+ }
2878
+ pushed += toPush.length;
2879
+ }
2880
+ cursor = page.nextCursor;
2881
+ if (cursor === null || page.ids.length < ctx.backfillBatchSize) break;
2882
+ }
2883
+ const extra = [...remote.keys()].filter((key) => !keep.has(key));
2884
+ for (let offset = 0; offset < extra.length; offset += ctx.backfillBatchSize) {
2885
+ const result = await ctx.client.deleteDocs(
2886
+ targetUid,
2887
+ extra.slice(offset, offset + ctx.backfillBatchSize)
2888
+ );
2889
+ if (result?.partialFailure) {
2890
+ throw new CliError(
2891
+ `Catching up "${def.name}" ${when} stopped: ${partialFailureSummary(result.partialFailure)}. Re-run "headband schema apply" once resolved.`
2892
+ );
2893
+ }
2894
+ }
2895
+ ctx.io.stdout(
2896
+ `Caught up "${def.name}" ${when}: ${pushed} changed or new document(s) written, ${extra.length} removed.`
2897
+ );
2898
+ }
2899
+ async function abandonReindex(ctx, index, physical, err) {
2900
+ let removed;
2901
+ try {
2902
+ await ctx.client.deleteIndex(physical);
2903
+ removed = `The new index "${physical}" was removed`;
2904
+ } catch (deleteErr) {
2905
+ const why = deleteErr instanceof Error ? deleteErr.message : String(deleteErr);
2906
+ removed = `The new index "${physical}" could not be removed (${why}): remove it with "headband indexes delete ${physical}"`;
2907
+ }
2908
+ const reason = err?.refusal ?? (err instanceof Error ? err.message : String(err));
2909
+ return new CliError(
2910
+ `The reindex of "${index}" stopped: the new index's settings were refused: ${String(reason).replace(/\.?$/, ".")} ${removed}, and "${index}" is unchanged. Change the index's settings in headband.config.ts, then re-run "headband schema apply".`,
2911
+ {
2912
+ ...err instanceof CliError ? { exitCode: err.exitCode, code: err.code } : {}
2913
+ }
2914
+ );
2915
+ }
2593
2916
  async function applyReindex(op, ctx, existing, expectedParentHash) {
2594
2917
  const def = ctx.config.indexes.find((d) => d.name === op.index);
2595
2918
  if (!def) {
2596
2919
  throw new CliError(`Cannot reindex "${op.index}": no matching index in the loaded config.`);
2597
2920
  }
2921
+ const settings = settingsForEngine(ctx, op.index, await indexEngine(ctx, op.index), op.settings);
2598
2922
  await createIndexIdempotent(ctx, op.toPhysicalUid, { primaryKey: def.primaryKey });
2599
- const settingsAfter = await patchSettingsBeforeBackfill(ctx, op.toPhysicalUid, op.settings);
2923
+ let settingsAfter;
2924
+ try {
2925
+ settingsAfter = await patchSettingsBeforeBackfill(ctx, op.toPhysicalUid, settings);
2926
+ } catch (err) {
2927
+ throw await abandonReindex(ctx, op.index, op.toPhysicalUid, err);
2928
+ }
2600
2929
  const version = (existing?.version ?? 0) + 1;
2601
2930
  const schemaHashValue = docShapeHash(def);
2602
2931
  const settingsHashValue = settingsHash(def);
@@ -2625,8 +2954,10 @@ async function applyReindex(op, ctx, existing, expectedParentHash) {
2625
2954
  "before the alias flip",
2626
2955
  (cursor) => writeState("migrating", cursor)
2627
2956
  );
2628
- if (settingsAfter) await patchSettingsAfterBackfill(ctx, op.toPhysicalUid, op.settings);
2957
+ if (settingsAfter) await patchSettingsAfterBackfill(ctx, op.toPhysicalUid, settings);
2958
+ await catchUp(ctx, def, op.toPhysicalUid, "before the alias flip");
2629
2959
  await ctx.client.patchAlias(op.index, op.toPhysicalUid);
2960
+ await catchUp(ctx, def, op.index, "after the alias flip");
2630
2961
  await writeState("applied", null);
2631
2962
  return {
2632
2963
  indexUid: op.index,
@@ -2646,9 +2977,15 @@ async function applyOp(op, ctx, existing, expectedParentHash) {
2646
2977
  );
2647
2978
  }
2648
2979
  await createIndexIdempotent(ctx, op.index, { primaryKey: def.primaryKey });
2649
- const settingsAfter = await patchSettingsBeforeBackfill(ctx, op.index, op.settings);
2980
+ const settings = settingsForEngine(
2981
+ ctx,
2982
+ op.index,
2983
+ await indexEngine(ctx, op.index),
2984
+ op.settings
2985
+ );
2986
+ const settingsAfter = await patchSettingsBeforeBackfill(ctx, op.index, settings);
2650
2987
  await backfill(ctx, def, op.index, "before recording the index as applied");
2651
- if (settingsAfter) await patchSettingsAfterBackfill(ctx, op.index, op.settings);
2988
+ if (settingsAfter) await patchSettingsAfterBackfill(ctx, op.index, settings);
2652
2989
  const next = {
2653
2990
  indexUid: op.index,
2654
2991
  version: 1,
@@ -2664,7 +3001,13 @@ async function applyOp(op, ctx, existing, expectedParentHash) {
2664
3001
  if (!existing) {
2665
3002
  throw new CliError(`Cannot patch settings for "${op.index}": no applied schema state found.`);
2666
3003
  }
2667
- await ctx.client.patchSettings(op.index, op.settings);
3004
+ const settings = settingsForEngine(
3005
+ ctx,
3006
+ op.index,
3007
+ await indexEngine(ctx, op.index),
3008
+ op.settings
3009
+ );
3010
+ if (Object.keys(settings).length > 0) await ctx.client.patchSettings(op.index, settings);
2668
3011
  const def = ctx.config.indexes.find((d) => d.name === op.index);
2669
3012
  const next = {
2670
3013
  ...existing,
@@ -2720,7 +3063,8 @@ async function runSchemaApply(deps) {
2720
3063
  client: deps.client,
2721
3064
  backfillBatchSize: deps.backfillBatchSize ?? DEFAULT_BACKFILL_BATCH_SIZE,
2722
3065
  io: deps.io,
2723
- projectId: deps.projectId
3066
+ projectId: deps.projectId,
3067
+ readRemoteDocuments: deps.readRemoteDocuments
2724
3068
  };
2725
3069
  let appliedOps = 0;
2726
3070
  let appliedFiles = 0;
@@ -2847,7 +3191,7 @@ async function runSchemaForget(deps) {
2847
3191
  }
2848
3192
 
2849
3193
  // src/sync.ts
2850
- var import_fs8 = require("fs");
3194
+ var import_fs9 = require("fs");
2851
3195
  var import_path10 = require("path");
2852
3196
  var import_crypto = require("crypto");
2853
3197
  var import_pg = require("pg");
@@ -2917,6 +3261,12 @@ function pgOutboxDriver(exec, opts) {
2917
3261
  );
2918
3262
  return rows?.[0]?.status === "dead_letter" ? "dead_letter" : "pending";
2919
3263
  },
3264
+ async release(ids, error) {
3265
+ await exec(
3266
+ `UPDATE ${table} SET last_error = $2, claimed_at = NULL WHERE id = ANY($1) AND status = 'pending'`,
3267
+ [ids, error]
3268
+ );
3269
+ },
2920
3270
  async deadLetters(limit) {
2921
3271
  const { rows } = await exec(
2922
3272
  `SELECT * FROM ${table} WHERE status = 'dead_letter' ORDER BY id LIMIT $1`,
@@ -3132,7 +3482,7 @@ function rowOutcomes(rows, resolved, failures) {
3132
3482
  return { done, retry, deadLetter };
3133
3483
  }
3134
3484
  function defaultSleep4(ms) {
3135
- return new Promise((resolve2) => setTimeout(resolve2, ms));
3485
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
3136
3486
  }
3137
3487
  function errorMessage(err) {
3138
3488
  if (!(err instanceof Error)) return String(err);
@@ -3154,6 +3504,7 @@ function describeRow(row) {
3154
3504
  var emptyOutcome = () => ({
3155
3505
  done: [],
3156
3506
  retry: [],
3507
+ release: [],
3157
3508
  deadLetter: [],
3158
3509
  pushed: 0,
3159
3510
  deleted: 0
@@ -3171,7 +3522,7 @@ async function runSyncWorker(opts) {
3171
3522
  batchSize = DEFAULT_BATCH_SIZE,
3172
3523
  stopSignal,
3173
3524
  now = Date.now,
3174
- sleep = defaultSleep4,
3525
+ sleep: sleep2 = defaultSleep4,
3175
3526
  onHeartbeat,
3176
3527
  log = (message) => console.warn(message)
3177
3528
  } = opts;
@@ -3198,7 +3549,7 @@ async function runSyncWorker(opts) {
3198
3549
  log(`[headband] could not record this worker run: ${errorMessage(err)}`);
3199
3550
  }
3200
3551
  };
3201
- const backoff = (attempt) => sleep(Math.min(failureDelayMs * 2 ** (attempt - 1), MAX_FAILURE_DELAY_MS));
3552
+ const backoff = (attempt) => sleep2(Math.min(failureDelayMs * 2 ** (attempt - 1), MAX_FAILURE_DELAY_MS));
3202
3553
  const processBatch = async (batch) => {
3203
3554
  const changes = batch.map((row) => ({
3204
3555
  model: row.model,
@@ -3213,7 +3564,7 @@ async function runSyncWorker(opts) {
3213
3564
  primaryKeyByIndex,
3214
3565
  backoff
3215
3566
  );
3216
- return { ...rowOutcomes(batch, resolved, failures), pushed, deleted };
3567
+ return { ...rowOutcomes(batch, resolved, failures), release: [], pushed, deleted };
3217
3568
  };
3218
3569
  const processIsolated = async (batch) => {
3219
3570
  const outcome = emptyOutcome();
@@ -3226,7 +3577,10 @@ async function runSyncWorker(opts) {
3226
3577
  outcome.pushed += one.pushed;
3227
3578
  outcome.deleted += one.deleted;
3228
3579
  } catch (err) {
3229
- outcome.retry.push([row, errorMessage(err)]);
3580
+ (isTransientBatchError(err) ? outcome.release : outcome.retry).push([
3581
+ row,
3582
+ errorMessage(err)
3583
+ ]);
3230
3584
  }
3231
3585
  }
3232
3586
  return outcome;
@@ -3237,7 +3591,7 @@ async function runSyncWorker(opts) {
3237
3591
  const rows = await outbox.claim(batchSize);
3238
3592
  if (rows.length === 0) {
3239
3593
  if (!loop) break;
3240
- await sleep(idleDelayMs);
3594
+ await sleep2(idleDelayMs);
3241
3595
  continue;
3242
3596
  }
3243
3597
  let outcome;
@@ -3245,12 +3599,14 @@ async function runSyncWorker(opts) {
3245
3599
  try {
3246
3600
  outcome = await processBatch(rows);
3247
3601
  } catch (err) {
3248
- if (rows.length > 1 && !isTransientBatchError(err)) {
3602
+ const transient = isTransientBatchError(err);
3603
+ if (rows.length > 1 && !transient) {
3249
3604
  outcome = await processIsolated(rows);
3250
3605
  } else {
3251
3606
  wholeBatchFailed = true;
3252
3607
  const message = errorMessage(err);
3253
- outcome = { ...emptyOutcome(), retry: rows.map((row) => [row, message]) };
3608
+ const failed = rows.map((row) => [row, message]);
3609
+ outcome = transient ? { ...emptyOutcome(), release: failed } : { ...emptyOutcome(), retry: failed };
3254
3610
  log(
3255
3611
  `[headband] a batch of ${rows.length} outbox row(s) failed and will be retried: ${message}`
3256
3612
  );
@@ -3272,6 +3628,17 @@ async function runSyncWorker(opts) {
3272
3628
  log(`[headband] ${describeRow(row)} failed: ${message}`);
3273
3629
  }
3274
3630
  }
3631
+ if (outcome.release.length > 0) {
3632
+ if (outbox.release) {
3633
+ await outbox.release(
3634
+ outcome.release.map(([row]) => row.id),
3635
+ outcome.release[0][1]
3636
+ );
3637
+ report.retried += outcome.release.length;
3638
+ } else {
3639
+ outcome.retry.push(...outcome.release);
3640
+ }
3641
+ }
3275
3642
  for (const [row, message] of outcome.retry) {
3276
3643
  const status = await outbox.markFailed(row.id, message);
3277
3644
  if (status === "dead_letter") {
@@ -3288,7 +3655,7 @@ async function runSyncWorker(opts) {
3288
3655
  }
3289
3656
  }
3290
3657
  }
3291
- if (outcome.retry.length > 0) {
3658
+ if (outcome.retry.length > 0 || outcome.release.length > 0) {
3292
3659
  consecutiveFailures += 1;
3293
3660
  await backoff(consecutiveFailures);
3294
3661
  } else {
@@ -3383,8 +3750,8 @@ async function runSyncInit(deps) {
3383
3750
  deps.io.stdout(ddl);
3384
3751
  if (deps.write) {
3385
3752
  const path2 = (0, import_path10.join)(deps.cwd ?? process.cwd(), OUTBOX_SQL_RELATIVE_PATH);
3386
- (0, import_fs8.mkdirSync)((0, import_path10.join)(deps.cwd ?? process.cwd(), "headband"), { recursive: true });
3387
- (0, import_fs8.writeFileSync)(path2, `${ddl}
3753
+ (0, import_fs9.mkdirSync)((0, import_path10.join)(deps.cwd ?? process.cwd(), "headband"), { recursive: true });
3754
+ (0, import_fs9.writeFileSync)(path2, `${ddl}
3388
3755
  `);
3389
3756
  deps.io.stdout(`Wrote ${path2}`);
3390
3757
  }
@@ -3512,6 +3879,66 @@ async function runSyncWorkerCmd(deps) {
3512
3879
  }
3513
3880
  }
3514
3881
  }
3882
+ async function reconcileContent(deps, def, pageSize) {
3883
+ const remote = await deps.readRemoteDocuments(def.name, def.primaryKey);
3884
+ const keep = /* @__PURE__ */ new Set();
3885
+ let missing = 0;
3886
+ let stale = 0;
3887
+ let cursor = null;
3888
+ for (; ; ) {
3889
+ const page = await deps.adapter.listIds(def.source, cursor, pageSize);
3890
+ if (page.ids.length === 0) break;
3891
+ const docs = await deps.adapter.resolveDocs(def.name, page.ids);
3892
+ const toPush = [];
3893
+ for (const doc of docs) {
3894
+ const sourceRow = doc.__source ?? doc;
3895
+ delete doc.__source;
3896
+ if (def.shouldIndex && !def.shouldIndex(sourceRow)) continue;
3897
+ const key = String(doc[def.primaryKey]);
3898
+ keep.add(key);
3899
+ const indexed = remote.get(key);
3900
+ if (!indexed) missing += 1;
3901
+ else if (!documentsMatch(doc, indexed, def.primaryKey)) stale += 1;
3902
+ else continue;
3903
+ toPush.push(doc);
3904
+ }
3905
+ if (deps.fix && toPush.length > 0) {
3906
+ const result = await deps.client.bulkPush(def.name, toPush, def.primaryKey);
3907
+ if (result?.partialFailure) {
3908
+ throw new CliError(
3909
+ `Repair push for "${def.name}" was incomplete: ${partialFailureSummary(result.partialFailure)}.`
3910
+ );
3911
+ }
3912
+ }
3913
+ if (page.nextCursor === null) break;
3914
+ cursor = page.nextCursor;
3915
+ }
3916
+ const extraIds = [...remote.keys()].filter((key) => !keep.has(key));
3917
+ if (deps.fix) {
3918
+ for (let offset = 0; offset < extraIds.length; offset += pageSize) {
3919
+ const result = await deps.client.deleteDocs(
3920
+ def.name,
3921
+ extraIds.slice(offset, offset + pageSize)
3922
+ );
3923
+ if (result?.partialFailure) {
3924
+ throw new CliError(
3925
+ `Removing extra documents from "${def.name}" was incomplete: ${partialFailureSummary(result.partialFailure)}.`
3926
+ );
3927
+ }
3928
+ }
3929
+ }
3930
+ const drift = missing + extraIds.length + stale;
3931
+ return {
3932
+ index: def.name,
3933
+ sourceCount: keep.size,
3934
+ remoteCount: remote.size,
3935
+ missing,
3936
+ extra: extraIds.length,
3937
+ stale,
3938
+ drift,
3939
+ fixed: Boolean(deps.fix) && drift !== 0
3940
+ };
3941
+ }
3515
3942
  async function indexableSourceIds(adapter, def, pageSize) {
3516
3943
  const ids = /* @__PURE__ */ new Map();
3517
3944
  let cursor = null;
@@ -3556,6 +3983,17 @@ async function runSyncReconcile(deps) {
3556
3983
  const pageSize = deps.pageSize ?? RECONCILE_PAGE_SIZE;
3557
3984
  const rows = [];
3558
3985
  for (const def of deps.config.indexes) {
3986
+ if (deps.content && deps.readRemoteDocuments) {
3987
+ try {
3988
+ rows.push(await reconcileContent(deps, def, pageSize));
3989
+ continue;
3990
+ } catch (err) {
3991
+ if (err instanceof CliError) throw err;
3992
+ deps.io.stderr(
3993
+ `Could not read the documents of "${def.name}", comparing ids only: ${err instanceof Error ? err.message : String(err)}`
3994
+ );
3995
+ }
3996
+ }
3559
3997
  const source = await indexableSourceIds(deps.adapter, def, pageSize);
3560
3998
  let remoteKeys = null;
3561
3999
  let remoteCount = -1;
@@ -3619,6 +4057,7 @@ async function runSyncReconcile(deps) {
3619
4057
  }
3620
4058
  rows.push(row);
3621
4059
  }
4060
+ const contentCompared = rows.some((r) => r.stale !== void 0 && r.stale !== null);
3622
4061
  printTable(
3623
4062
  deps.io,
3624
4063
  rows.map((r) => ({
@@ -3626,9 +4065,10 @@ async function runSyncReconcile(deps) {
3626
4065
  remoteCount: r.remoteCount === -1 ? "unknown" : r.remoteCount,
3627
4066
  missing: r.missing ?? "",
3628
4067
  extra: r.extra ?? "",
4068
+ stale: r.stale ?? "",
3629
4069
  fixed: r.fixed ? "yes" : ""
3630
4070
  })),
3631
- ["index", "sourceCount", "remoteCount", "missing", "extra", "drift", "fixed"]
4071
+ contentCompared ? ["index", "sourceCount", "remoteCount", "missing", "extra", "stale", "drift", "fixed"] : ["index", "sourceCount", "remoteCount", "missing", "extra", "drift", "fixed"]
3632
4072
  );
3633
4073
  const unfixed = rows.filter((r) => r.drift !== 0 && !r.fixed);
3634
4074
  if (unfixed.length > 0 && !deps.fix) {
@@ -3697,10 +4137,10 @@ async function runSelfHostedSyncStatus(deps) {
3697
4137
  }
3698
4138
  function runSyncScaffoldDocker(deps) {
3699
4139
  const path2 = (0, import_path10.join)(deps.cwd, DOCKERFILE_NAME);
3700
- if ((0, import_fs8.existsSync)(path2) && !deps.force) {
4140
+ if ((0, import_fs9.existsSync)(path2) && !deps.force) {
3701
4141
  throw new CliError(`${DOCKERFILE_NAME} already exists at ${path2}. Pass --force to overwrite.`);
3702
4142
  }
3703
- (0, import_fs8.writeFileSync)(path2, dockerfileContent(deps.cliVersion));
4143
+ (0, import_fs9.writeFileSync)(path2, dockerfileContent(deps.cliVersion));
3704
4144
  deps.io.stdout(`Wrote ${path2}`);
3705
4145
  deps.io.stdout("Add this service to your docker-compose.yml:");
3706
4146
  deps.io.stdout(COMPOSE_SNIPPET);
@@ -3798,13 +4238,14 @@ var syncWorker = [
3798
4238
  "package name, and prints a docker-compose service."
3799
4239
  ].join("\n");
3800
4240
  var syncReconcile = [
3801
- "Usage: headband sync reconcile [--fix] [--json]",
4241
+ "Usage: headband sync reconcile [--content] [--fix] [--json]",
3802
4242
  "",
3803
4243
  "Per index, compares the source rows that belong in it (every row shouldIndex keeps) with",
3804
4244
  "the documents in it, and prints how many documents are missing and how many are extra",
3805
4245
  "(counts, not ids; --json gives the same counts). --fix pushes the missing ones and deletes",
3806
- "the extra ones. Exits 1 when drift is left. Documents whose content went",
3807
- "stale are not detected: a reindex (schema apply) rewrites every document.",
4246
+ "the extra ones. Exits 1 when drift is left. --content also compares every document with its",
4247
+ "freshly resolved source row and counts the stale ones; with --fix it rewrites them. It reads",
4248
+ "the whole index, so it takes about as long as a backfill.",
3808
4249
  "Needs the adapter export and DATABASE_URL."
3809
4250
  ].join("\n");
3810
4251
  var syncRetry = [
@@ -3839,7 +4280,7 @@ var SUBCOMMAND_HELP = {
3839
4280
  " headband sync init [--table <name>] [--write] [--apply] [--json]",
3840
4281
  " headband sync worker [--loop] [--budget-ms <ms>] [--batch-size <n>] [--reconcile-every <n>] [--project <id>] [--json]",
3841
4282
  " headband sync worker --scaffold docker [--force] [--json]",
3842
- " headband sync reconcile [--fix] [--json]",
4283
+ " headband sync reconcile [--content] [--fix] [--json]",
3843
4284
  " headband sync retry-dead-letters [--json]",
3844
4285
  " headband sync status [--json]",
3845
4286
  "",
@@ -3876,10 +4317,10 @@ ${CONFIG_NOTE}`,
3876
4317
  };
3877
4318
 
3878
4319
  // src/sync-indexes.ts
3879
- var import_fs10 = require("fs");
4320
+ var import_fs11 = require("fs");
3880
4321
 
3881
4322
  // src/sync-sources.ts
3882
- var import_fs9 = require("fs");
4323
+ var import_fs10 = require("fs");
3883
4324
  var import_pg2 = require("pg");
3884
4325
 
3885
4326
  // ../../src/lib/sync/copy.ts
@@ -3946,7 +4387,7 @@ var SOURCE_USAGE = [
3946
4387
  function readCaFile(deps, file) {
3947
4388
  let text;
3948
4389
  try {
3949
- text = (deps.readFile ?? ((p) => (0, import_fs9.readFileSync)(p, "utf8")))(file);
4390
+ text = (deps.readFile ?? ((p) => (0, import_fs10.readFileSync)(p, "utf8")))(file);
3950
4391
  } catch {
3951
4392
  throw new CliError("Cannot read the --ca-file.");
3952
4393
  }
@@ -4587,7 +5028,7 @@ var PREVIEW_MAX_IDS = 20;
4587
5028
  var ATTENTION_INDEX = /* @__PURE__ */ new Set(["error", "needs_rebuild", "awaiting_setup", "degraded"]);
4588
5029
  var ATTENTION_SOURCE = /* @__PURE__ */ new Set(["error", "awaiting_setup"]);
4589
5030
  var path = (id, suffix = "") => `/v1/sync/indexes/${encodeURIComponent(id)}${suffix}`;
4590
- function readDefinition(file, readFile = (p) => (0, import_fs10.readFileSync)(p, "utf8")) {
5031
+ function readDefinition(file, readFile = (p) => (0, import_fs11.readFileSync)(p, "utf8")) {
4591
5032
  let text;
4592
5033
  try {
4593
5034
  text = readFile(file);
@@ -4984,6 +5425,7 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
4984
5425
  "--client",
4985
5426
  "--instance",
4986
5427
  "--instances",
5428
+ "--indexes",
4987
5429
  "--db-url",
4988
5430
  "--owner-url",
4989
5431
  "--tls-mode",
@@ -5005,6 +5447,8 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
5005
5447
  "--branch",
5006
5448
  "--parent",
5007
5449
  "--expires-in",
5450
+ "--env-out",
5451
+ "--from",
5008
5452
  "--in",
5009
5453
  "--at",
5010
5454
  "--level",
@@ -5020,6 +5464,7 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
5020
5464
  "--write",
5021
5465
  "--force",
5022
5466
  "--fix",
5467
+ "--content",
5023
5468
  "--loop",
5024
5469
  "--accept-broad-role",
5025
5470
  "--rebuild",
@@ -5176,14 +5621,15 @@ Commands:
5176
5621
  projects create <name> --engine <meilisearch|typesense|elasticsearch|visor> [--json]
5177
5622
  projects delete <id> [--yes] [--json]
5178
5623
  branch list [--project <id>] [--json]
5179
- branch create <name> --empty [--parent <name>] [--expires-in <2h|7d>] [--project <id>] [--json]
5624
+ branch create <name> [--empty] [--parent <name>] [--expires-in <2h|7d>] [--project <id>] [--json]
5625
+ branch ensure <name> [--from <parent>] [--expires-in <7d>] [--env-out <file>] [--project <id>] [--json]
5180
5626
  branch delete <name> [--project <id>] [--yes] [--json]
5181
5627
  branch protect|unprotect <name> [--project <id>] [--json]
5182
5628
  branch expire <name> (--in <2h|7d> | --at <iso> | --clear) [--project <id>] [--json]
5183
5629
  branch checkout <name> [--project <id>] [--json]
5184
- branch reset|rollback|restore|set-primary (coming with bucket storage)
5630
+ branch reset|rollback|restore|set-primary (not available yet)
5185
5631
  keys list [--json]
5186
- keys create <name> --type <admin|search> [--project <id>] [--branch <name>] [--instances <id,id>] [--permissions <a,b>] [--expires <30d|ISO date>] [--json]
5632
+ keys create <name> --type <admin|search> [--project <id>] [--branch <name>] [--instances <id,id>] [--indexes <uid,uid>] [--permissions <a,b>] [--expires <30d|ISO date>] [--json]
5187
5633
  keys revoke <id> [--yes] [--json]
5188
5634
  indexes list [--json] (index works in place of indexes)
5189
5635
  indexes create <uid> [--primary-key <pk>] [--engine <engine>] [--instance <id>] [--json]
@@ -5201,7 +5647,7 @@ Commands:
5201
5647
  instances connect <id> [--client <node|curl>] [--json]
5202
5648
  warm <index>... | --project <id> [--branch <name>] [--level auto|stage|start] [--json]
5203
5649
  docs push <file> --index <uid> [--json]
5204
- docs export --index <uid> [--out <file>] [--format <jsonl|json>] [--batch <n>]
5650
+ docs export --index <uid> [--out <file>] [--format <jsonl|json>] [--batch <n>] [--declare-key]
5205
5651
  docs delete <docId>... --index <uid> [--json]
5206
5652
  search <index> [<query>] [--limit N] [--filter <expr>] [--mode <instant|retrieval>] [--json]
5207
5653
  tasks list [--index <uid>] [--limit <n>] [--json]
@@ -5216,7 +5662,7 @@ Commands:
5216
5662
  sync init [--table <name>] [--write] [--apply] [--json]
5217
5663
  sync worker [--loop] [--budget-ms <ms>] [--batch-size <n>] [--reconcile-every <n>] [--project <id>] [--json]
5218
5664
  sync worker --scaffold docker [--force] [--json]
5219
- sync reconcile [--fix] [--json]
5665
+ sync reconcile [--content] [--fix] [--json]
5220
5666
  sync retry-dead-letters [--json]
5221
5667
  sync status [--json]
5222
5668
 
@@ -5252,13 +5698,14 @@ function printCommandHelp(command, io) {
5252
5698
  keys: [
5253
5699
  "Usage:",
5254
5700
  " headband keys list [--json]",
5255
- " headband keys create <name> --type <admin|search> [--project <id>] [--branch <name>] [--instances <id,id>] [--permissions <a,b>] [--expires <30d|ISO date>] [--json]",
5701
+ " headband keys create <name> --type <admin|search> [--project <id>] [--branch <name>] [--instances <id,id>] [--indexes <uid,uid>] [--permissions <a,b>] [--expires <30d|ISO date>] [--json]",
5256
5702
  " headband keys revoke <id> [--yes] [--json]",
5257
5703
  "",
5258
5704
  "keys create mints a key for one branch: --branch (with --project, else the checkout's project),",
5259
5705
  "else the checked-out branch, else the --project's primary branch. With none of these,",
5260
5706
  "--type admin mints an org key, and --type search is refused: a search key needs a branch.",
5261
- "--instances <id,id> limits the key to those dedicated engines (on a branch: its engines among them)."
5707
+ "--instances <id,id> limits the key to those dedicated engines (on a branch: its engines among them).",
5708
+ "--indexes <uid,uid> limits a search key to those indexes; any other index answers 404."
5262
5709
  ].join("\n"),
5263
5710
  indexes: [
5264
5711
  "Usage:",
@@ -5290,7 +5737,7 @@ function printCommandHelp(command, io) {
5290
5737
  docs: [
5291
5738
  "Usage:",
5292
5739
  " headband docs push <file> --index <uid> [--json]",
5293
- " headband docs export --index <uid> [--out <file>] [--format <jsonl|json>] [--batch <n>] [--json]",
5740
+ " headband docs export --index <uid> [--out <file>] [--format <jsonl|json>] [--batch <n>] [--declare-key] [--json]",
5294
5741
  " headband docs delete <docId>... --index <uid> [--json]",
5295
5742
  "",
5296
5743
  "docs push sends the documents in a .json (an array) or .jsonl file, 1000 at a time.",
@@ -5300,6 +5747,9 @@ function printCommandHelp(command, io) {
5300
5747
  " --out <file> to a file. The format follows the extension (.json is one array,",
5301
5748
  " anything else JSONL) unless --format says otherwise.",
5302
5749
  " --batch <n> documents per request. Default 1000, or 250 on Typesense.",
5750
+ " --declare-key a Visor index past 1000 documents whose primary key is not",
5751
+ " filterable and sortable (made before 2026-10-21): declare the",
5752
+ " key on it first (Visor rebuilds the index), wait, then export.",
5303
5753
  " If the export stops on an error, the documents written so far are kept in",
5304
5754
  " <file>.partial, and <file> is not written.",
5305
5755
  "",
@@ -5510,7 +5960,12 @@ function parsePermissions(raw) {
5510
5960
  }
5511
5961
  function keyRow(key) {
5512
5962
  const branch = key.branch;
5513
- return { ...key, branch: branch?.name ?? "", engines: enginesColumn(key.instanceIds) };
5963
+ return {
5964
+ ...key,
5965
+ branch: branch?.name ?? "",
5966
+ engines: enginesColumn(key.instanceIds),
5967
+ indexes: enginesColumn(key.indexUids)
5968
+ };
5514
5969
  }
5515
5970
  function enginesColumn(instanceIds) {
5516
5971
  if (instanceIds === null) return "all";
@@ -5521,6 +5976,21 @@ function enginesColumn(instanceIds) {
5521
5976
  }
5522
5977
  return "none";
5523
5978
  }
5979
+ function parseIndexList(raw) {
5980
+ const uids = [
5981
+ ...new Set(
5982
+ raw.split(",").map((x) => x.trim()).filter(Boolean)
5983
+ )
5984
+ ];
5985
+ if (uids.length === 0) throw new CliError("--indexes needs at least one index uid.");
5986
+ const bad = uids.find((uid) => !/^[A-Za-z0-9_-]{1,64}$/.test(uid));
5987
+ if (bad) {
5988
+ throw new CliError(
5989
+ `Not an index uid: ${bad}. Index uids are letters, numbers, hyphens and underscores.`
5990
+ );
5991
+ }
5992
+ return uids;
5993
+ }
5524
5994
  function parseInstanceList(raw) {
5525
5995
  const ids = [
5526
5996
  ...new Set(
@@ -5553,6 +6023,7 @@ async function runKeys(args, flags, io, confirm, cwd) {
5553
6023
  "projectId",
5554
6024
  "branch",
5555
6025
  "engines",
6026
+ "indexes",
5556
6027
  "environment",
5557
6028
  "lastUsedAt",
5558
6029
  "expiresAt"
@@ -5574,6 +6045,11 @@ async function runKeys(args, flags, io, confirm, cwd) {
5574
6045
  }
5575
6046
  const instances = stringFlag3(flags, "--instances");
5576
6047
  if (instances !== void 0) body.instanceIds = parseInstanceList(instances);
6048
+ if (stringFlag3(flags, "--index") !== void 0) {
6049
+ throw new CliError("Unknown option --index for keys create. Did you mean --indexes?");
6050
+ }
6051
+ const indexes = stringFlag3(flags, "--indexes");
6052
+ if (indexes !== void 0) body.indexUids = parseIndexList(indexes);
5577
6053
  const permissions = stringFlag3(flags, "--permissions");
5578
6054
  if (permissions) body.permissions = parsePermissions(permissions);
5579
6055
  const expires = stringFlag3(flags, "--expires");
@@ -5593,6 +6069,12 @@ async function runKeys(args, flags, io, confirm, cwd) {
5593
6069
  typeof keyId === "string" && keyId ? `This API did not limit the new key to the chosen engines, so key ${keyId} reaches wider than asked. Revoke it now: headband keys revoke ${keyId}` : "This API did not limit the new key to the chosen engines, so it reaches wider than asked. Find it with `headband keys list` and revoke it with `headband keys revoke <id>`."
5594
6070
  );
5595
6071
  }
6072
+ if (indexes !== void 0 && !Array.isArray(response.data.key.indexUids)) {
6073
+ const keyId = response.data.key.id;
6074
+ throw new CliError(
6075
+ typeof keyId === "string" && keyId ? `This API did not limit the new key to the chosen indexes, so key ${keyId} reaches every index of its branch. Revoke it now: headband keys revoke ${keyId}` : "This API did not limit the new key to the chosen indexes, so it reaches every index of its branch. Find it with `headband keys list` and revoke it with `headband keys revoke <id>`."
6076
+ );
6077
+ }
5596
6078
  if (boolFlag(flags, "--json")) {
5597
6079
  printJson(io, response);
5598
6080
  return;
@@ -5602,7 +6084,17 @@ async function runKeys(args, flags, io, confirm, cwd) {
5602
6084
  printTable(
5603
6085
  io,
5604
6086
  [keyRow(response.data.key)],
5605
- ["id", "name", "type", "environment", "branch", "engines", "keyPrefix", "expiresAt"]
6087
+ [
6088
+ "id",
6089
+ "name",
6090
+ "type",
6091
+ "environment",
6092
+ "branch",
6093
+ "engines",
6094
+ "indexes",
6095
+ "keyPrefix",
6096
+ "expiresAt"
6097
+ ]
5606
6098
  );
5607
6099
  return;
5608
6100
  }
@@ -5688,7 +6180,7 @@ async function runIndexes(args, flags, io, confirm, cwd) {
5688
6180
  }
5689
6181
  if (settingsAction === "set") {
5690
6182
  const file = requiredFlag(flags, "--file");
5691
- const settings = JSON.parse((0, import_fs12.readFileSync)(file, "utf-8"));
6183
+ const settings = JSON.parse((0, import_fs13.readFileSync)(file, "utf-8"));
5692
6184
  const response = await client.patch(`/v1/indexes/${encode(uid)}/settings`, settings);
5693
6185
  printJson(io, response);
5694
6186
  return;
@@ -5738,6 +6230,7 @@ async function runDocsExport(flags, context, cwd) {
5738
6230
  const format = exportFormat(flags, out);
5739
6231
  const client = clientFromFlags(flags, cwd);
5740
6232
  const batchSize = parseIntFlag(flags, "--batch") ?? await defaultExportBatchSize(client, index);
6233
+ if (boolFlag(flags, "--declare-key")) await declareExportKey(client, index, io);
5741
6234
  if (!out) {
5742
6235
  const writeLine = context.writeLine ?? ((line) => io.stdout(line));
5743
6236
  try {
@@ -5748,7 +6241,7 @@ async function runDocsExport(flags, context, cwd) {
5748
6241
  return;
5749
6242
  }
5750
6243
  const partial = `${out}.partial`;
5751
- const fd = (0, import_fs11.openSync)(partial, "w", 384);
6244
+ const fd = (0, import_fs12.openSync)(partial, "w", 384);
5752
6245
  let exported;
5753
6246
  try {
5754
6247
  exported = await exportDocuments(client, {
@@ -5756,26 +6249,26 @@ async function runDocsExport(flags, context, cwd) {
5756
6249
  format,
5757
6250
  batchSize,
5758
6251
  write: (line) => {
5759
- (0, import_fs11.writeSync)(fd, `${line}
6252
+ (0, import_fs12.writeSync)(fd, `${line}
5760
6253
  `);
5761
6254
  },
5762
6255
  onPage: (count, total) => io.stderr(`Exported ${count}${total === null ? "" : `/${total}`} documents`),
5763
6256
  finishOnError: true
5764
6257
  });
5765
- (0, import_fs11.closeSync)(fd);
6258
+ (0, import_fs12.closeSync)(fd);
5766
6259
  } catch (error) {
5767
- (0, import_fs11.closeSync)(fd);
6260
+ (0, import_fs12.closeSync)(fd);
5768
6261
  const kept = error instanceof ExportInterruptedError ? error.exported : 0;
5769
6262
  if (kept > 0) {
5770
6263
  io.stderr(
5771
6264
  `Kept the ${kept} ${kept === 1 ? "document" : "documents"} exported before the error in ${partial}. ${out} was not written.`
5772
6265
  );
5773
6266
  } else {
5774
- (0, import_fs11.rmSync)(partial, { force: true });
6267
+ (0, import_fs12.rmSync)(partial, { force: true });
5775
6268
  }
5776
6269
  throw exportFailure(error instanceof ExportInterruptedError ? error.cause : error, index);
5777
6270
  }
5778
- (0, import_fs11.renameSync)(partial, out);
6271
+ (0, import_fs12.renameSync)(partial, out);
5779
6272
  if (boolFlag(flags, "--json")) printJson(io, { index, out, format, exported });
5780
6273
  else io.stdout(`Exported ${exported} documents to ${out}.`);
5781
6274
  }
@@ -5940,7 +6433,8 @@ async function runSchema(args, flags, io, confirm, cwd) {
5940
6433
  dir,
5941
6434
  allowDestructive,
5942
6435
  io: chatter,
5943
- projectId
6436
+ projectId,
6437
+ readRemoteDocuments: (uid, primaryKey) => readRemoteDocuments(apiClient, uid, primaryKey)
5944
6438
  });
5945
6439
  if (json) printJson(io, { ...result, projectId: projectId ?? null });
5946
6440
  else {
@@ -6120,8 +6614,10 @@ async function runSync(args, flags, io, confirm, cwd) {
6120
6614
  client,
6121
6615
  io: chatter,
6122
6616
  fix: boolFlag(flags, "--fix"),
6617
+ content: boolFlag(flags, "--content"),
6123
6618
  getStats: (uid) => apiClient.get(`/v1/indexes/${encode(uid)}/stats`),
6124
- listRemoteIds: (uid, primaryKey) => listRemoteIds(apiClient, uid, primaryKey)
6619
+ listRemoteIds: (uid, primaryKey) => listRemoteIds(apiClient, uid, primaryKey),
6620
+ readRemoteDocuments: (uid, primaryKey) => readRemoteDocuments(apiClient, uid, primaryKey)
6125
6621
  });
6126
6622
  if (json) printJson(io, { indexes: rows, drift: reconcileHasDrift(rows) });
6127
6623
  if (reconcileHasDrift(rows)) process.exitCode = 1;
@@ -6143,7 +6639,7 @@ async function runSync(args, flags, io, confirm, cwd) {
6143
6639
  printCommandHelp("sync", io);
6144
6640
  throw new UsageError("Unknown sync command");
6145
6641
  }
6146
- var CLI_VERSION = true ? "0.6.0" : "dev";
6642
+ var CLI_VERSION = true ? "0.7.0" : "dev";
6147
6643
  async function run(argv, context = { io: defaultIO }) {
6148
6644
  if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v" || argv[0] === "version")) {
6149
6645
  context.io.stdout(CLI_VERSION);
@@ -6245,7 +6741,7 @@ async function main(argv, context = { io: defaultIO }) {
6245
6741
  function stdoutLineWriter(line) {
6246
6742
  if (process.stdout.write(`${line}
6247
6743
  `)) return;
6248
- return new Promise((resolve2) => process.stdout.once("drain", () => resolve2()));
6744
+ return new Promise((resolve3) => process.stdout.once("drain", () => resolve3()));
6249
6745
  }
6250
6746
  function isDirectRun() {
6251
6747
  return typeof require !== "undefined" && typeof module !== "undefined" && require.main === module;
@@ -6277,6 +6773,7 @@ function exitAfterFlush(code) {
6277
6773
  main,
6278
6774
  parseArgv,
6279
6775
  parseExpires,
6776
+ parseIndexList,
6280
6777
  parsePermissions,
6281
6778
  run,
6282
6779
  stdinIsInteractive,