@headband/cli 0.6.1 → 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 +22 -10
  2. package/dist/index.js +190 -64
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -187,7 +187,8 @@ has its own indexes and its own keys.
187
187
 
188
188
  ```bash
189
189
  headband branch list [--project <id>] [--json]
190
- headband branch create <name> --empty [--parent <name>] [--expires-in <2h|7d>] [--project <id>] [--json]
190
+ headband branch create <name> [--empty] [--parent <name>] [--expires-in <2h|7d>] [--project <id>] [--json]
191
+ headband branch ensure <name> [--from <parent>] [--expires-in <7d>] [--env-out <file>] [--project <id>] [--json]
191
192
  headband branch delete <name> [--project <id>] [--yes] [--json]
192
193
  headband branch protect <name> [--project <id>] [--json]
193
194
  headband branch unprotect <name> [--project <id>] [--json]
@@ -197,13 +198,25 @@ headband branch checkout <name> [--project <id>] [--json]
197
198
 
198
199
  - The project comes from `--project`, else the directory's checkout (below).
199
200
  With neither, the command is a usage error.
200
- - `create` makes an empty branch. Without `--empty` it fails with exit `6`
201
- (code `fork_not_available`): copying a parent's data into a new branch
202
- arrives with bucket storage. Until branches other than main open on the
203
- server, every `create` fails with exit `6` (code `branches_not_open`),
204
- `--empty` or not. `--parent` records the branch it comes from
205
- (default: the primary branch). `--expires-in` deletes the branch on its own
206
- after that long.
201
+ - `create` forks the parent (`--parent`, default: the primary branch): the
202
+ new branch starts with the parent's indexes, documents and settings,
203
+ copy-on-write. `--empty` makes a branch with no indexes instead. A parent
204
+ with an index that cannot fork (Typesense, Visor, your own engine) fails
205
+ with exit `6` (code `fork_not_supported`). Until forking is on for your
206
+ organization, a fork fails with exit `6` (code `fork_not_available`), and
207
+ until branches other than main open, every `create` fails with exit `6`
208
+ (code `branches_not_open`). `--expires-in` deletes the branch on its own
209
+ after that long. Forking by default is new in CLI 0.7.0: before it,
210
+ `create` needed `--empty`.
211
+ - `ensure` is for preview deploys, such as a Vercel build step. It forks the
212
+ branch from `--from` (default: the primary) unless it exists, pushes its
213
+ expiry `--expires-in` out (default `7d`), mints a search key and an admin
214
+ key on it, and writes `HEADBAND_URL`, `HEADBAND_PROJECT_ID`,
215
+ `HEADBAND_BRANCH`, `HEADBAND_BRANCH_ID`, `HEADBAND_SEARCH_KEY`,
216
+ `HEADBAND_ADMIN_KEY` and one `HEADBAND_<ENGINE>_URL` per engine to
217
+ `--env-out <file>`, replacing any `HEADBAND_*` lines there. Without
218
+ `--env-out` it prints them. The name is made branch-safe first, so a git
219
+ branch name works as is. It refuses the primary branch (CLI 0.7.0).
207
220
  - `delete` removes the branch with its indexes, keys and engines. It asks
208
221
  first. The primary branch, a protected branch, a branch with live child
209
222
  branches and a branch with Sync-owned indexes are refused (exit `6`).
@@ -269,8 +282,7 @@ error, so a name is never matched across projects by accident.
269
282
  project of their own and would ignore it. The exception is `keys create`, which
270
283
  sends the project in its body (see Keys).
271
284
 
272
- Coming with bucket storage: `branch create` without `--empty` (a copy of its
273
- parent), `reset`, `rollback`, `restore` and `set-primary`.
285
+ Not available yet: `reset`, `rollback`, `restore` and `set-primary`.
274
286
 
275
287
  ### Keys
276
288
 
package/dist/index.js CHANGED
@@ -42,8 +42,8 @@ __export(index_exports, {
42
42
  syncClientFromFlags: () => syncClientFromFlags
43
43
  });
44
44
  module.exports = __toCommonJS(index_exports);
45
- var import_fs11 = require("fs");
46
45
  var import_fs12 = require("fs");
46
+ var import_fs13 = require("fs");
47
47
  var import_path11 = require("path");
48
48
  var import_prompts2 = __toESM(require("prompts"));
49
49
  var import_picocolors3 = __toESM(require("picocolors"));
@@ -528,7 +528,7 @@ function errorMessageOf(status, errorBody) {
528
528
  }
529
529
  return message;
530
530
  }
531
- var defaultSleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
531
+ var defaultSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
532
532
  async function readJson(response) {
533
533
  const text = await response.text();
534
534
  if (!text) return {};
@@ -548,7 +548,7 @@ async function apiRequest(baseUrl, path2, init2 = {}) {
548
548
  headers.set("authorization", `Bearer ${init2.apiKey}`);
549
549
  }
550
550
  const base = normalizeUrl(baseUrl);
551
- const { rateLimitRetries = 0, sleep = defaultSleep, apiKey: _apiKey, ...fetchInit } = init2;
551
+ const { rateLimitRetries = 0, sleep: sleep2 = defaultSleep, apiKey: _apiKey, ...fetchInit } = init2;
552
552
  void _apiKey;
553
553
  let response;
554
554
  try {
@@ -561,7 +561,7 @@ async function apiRequest(baseUrl, path2, init2 = {}) {
561
561
  const wait = response.status === 429 ? retryAfterSeconds(response) : null;
562
562
  if (wait === null || attempt >= rateLimitRetries) break;
563
563
  await response.body?.cancel().catch(() => void 0);
564
- await sleep(wait * 1e3);
564
+ await sleep2(wait * 1e3);
565
565
  }
566
566
  } catch (error) {
567
567
  const cause = error?.cause;
@@ -600,7 +600,7 @@ function createApiClient(url, profile) {
600
600
  }
601
601
 
602
602
  // src/auth.ts
603
- var defaultSleep2 = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
603
+ var defaultSleep2 = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
604
604
  function openBrowser(url) {
605
605
  try {
606
606
  if (process.platform === "darwin") {
@@ -615,7 +615,7 @@ function openBrowser(url) {
615
615
  }
616
616
  async function runDeviceFlow(url, io, options = {}) {
617
617
  const baseUrl = normalizeUrl(url);
618
- const sleep = options.sleep || defaultSleep2;
618
+ const sleep2 = options.sleep || defaultSleep2;
619
619
  const now = options.now || Date.now;
620
620
  const device = await apiRequest(baseUrl, "/api/cli/device", {
621
621
  method: "POST",
@@ -630,7 +630,7 @@ async function runDeviceFlow(url, io, options = {}) {
630
630
  let interval = device.data.interval;
631
631
  const deadline = now() + device.data.expiresIn * 1e3;
632
632
  while (now() < deadline) {
633
- await sleep(interval * 1e3);
633
+ await sleep2(interval * 1e3);
634
634
  try {
635
635
  const token = await apiRequest(baseUrl, "/api/cli/token", {
636
636
  method: "POST",
@@ -685,6 +685,7 @@ async function login(url, io, options = {}) {
685
685
  }
686
686
 
687
687
  // src/branch.ts
688
+ var import_fs4 = require("fs");
688
689
  var import_path5 = require("path");
689
690
 
690
691
  // src/output.ts
@@ -894,14 +895,22 @@ var PART_2 = /* @__PURE__ */ new Set(["reset", "rollback", "restore", "set-prima
894
895
  var BRANCH_USAGE = [
895
896
  "Usage:",
896
897
  " headband branch list [--project <id>] [--json]",
897
- " 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]",
898
900
  " headband branch delete <name> [--project <id>] [--yes] [--json]",
899
901
  " headband branch protect <name> [--project <id>] [--json]",
900
902
  " headband branch unprotect <name> [--project <id>] [--json]",
901
903
  " headband branch expire <name> (--in <2h|7d> | --at <iso> | --clear) [--project <id>] [--json]",
902
904
  " headband branch checkout <name> [--project <id>] [--json]",
903
- " headband branch create <name> (without --empty, a fork) coming with bucket storage",
904
- " 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.",
905
914
  "",
906
915
  "The project comes from --project, else the directory's checkout (.headband/project.json).",
907
916
  "Durations take s, m, h, d or w: 30m, 2h, 7d.",
@@ -1000,10 +1009,7 @@ async function runBranch(args, flags, deps) {
1000
1009
  const action = args[0];
1001
1010
  const json = flags["--json"] === true;
1002
1011
  if (action && PART_2.has(action)) {
1003
- throw new UsageError(
1004
- `headband branch ${action} is coming with bucket storage.`,
1005
- "not_available"
1006
- );
1012
+ throw new UsageError(`headband branch ${action} is not available yet.`, "not_available");
1007
1013
  }
1008
1014
  const client = createApiClient(auth.url, { apiKey: auth.apiKey });
1009
1015
  const checkout = () => checkoutFor(cwd, auth.profile?.orgId);
@@ -1032,18 +1038,35 @@ async function runBranch(args, flags, deps) {
1032
1038
  if (error instanceof ApiError && error.code === "branches_not_open") {
1033
1039
  throw restate(error, "Branches other than main are not open yet.");
1034
1040
  }
1035
- if (error instanceof ApiError && error.code === "fork_not_available") {
1036
- throw restate(
1037
- error,
1038
- "Forking arrives with bucket storage. Pass --empty for an empty branch."
1039
- );
1040
- }
1041
- throw error;
1041
+ throw restateCreate(error);
1042
1042
  }
1043
1043
  if (json) printJson(io, response);
1044
1044
  else printTable(io, [response.data], COLUMNS);
1045
1045
  return;
1046
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
+ }
1047
1070
  if (action === "delete") {
1048
1071
  const name = requiredName(args[1]);
1049
1072
  const project = projectFor(flags, checkout);
@@ -1115,9 +1138,109 @@ async function runBranch(args, flags, deps) {
1115
1138
  io.stdout(BRANCH_USAGE);
1116
1139
  throw new UsageError("Unknown branch command");
1117
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
+ }
1118
1241
 
1119
1242
  // src/config-load.ts
1120
- var import_fs4 = require("fs");
1243
+ var import_fs5 = require("fs");
1121
1244
  var import_path6 = require("path");
1122
1245
  var import_url = require("url");
1123
1246
  var import_jiti = require("jiti");
@@ -1253,7 +1376,7 @@ var SYNC_CONFIG_SHAPE = 'a headband.config.ts whose default export is defineConf
1253
1376
  function findConfigFile(cwd) {
1254
1377
  for (const name of CONFIG_FILENAMES) {
1255
1378
  const path2 = (0, import_path6.join)(cwd, name);
1256
- if ((0, import_fs4.existsSync)(path2)) return path2;
1379
+ if ((0, import_fs5.existsSync)(path2)) return path2;
1257
1380
  }
1258
1381
  return void 0;
1259
1382
  }
@@ -1373,10 +1496,10 @@ function missingAdapterError(command, why) {
1373
1496
 
1374
1497
  // src/docs.ts
1375
1498
  var import_path7 = require("path");
1376
- var import_fs5 = require("fs");
1499
+ var import_fs6 = require("fs");
1377
1500
  var DOCS_BATCH_SIZE = 1e3;
1378
1501
  function parseDocumentsFile(path2) {
1379
- const content = (0, import_fs5.readFileSync)(path2, "utf-8");
1502
+ const content = (0, import_fs6.readFileSync)(path2, "utf-8");
1380
1503
  const ext = (0, import_path7.extname)(path2).toLowerCase();
1381
1504
  if (ext === ".jsonl") {
1382
1505
  return content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
@@ -1501,7 +1624,7 @@ function refusedWalk(error) {
1501
1624
  var DECLARE_WAIT_MS = 15 * 6e4;
1502
1625
  var DECLARE_POLL_MS = 3e3;
1503
1626
  async function declareExportKey(client, index, io, opts = {}) {
1504
- const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
1627
+ const sleep2 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
1505
1628
  const now = opts.now ?? Date.now;
1506
1629
  const probe = () => client.get(`/v1/documents?index=${encodeURIComponent(index)}&limit=1&cursor=`);
1507
1630
  let refused;
@@ -1547,7 +1670,7 @@ async function declareExportKey(client, index, io, opts = {}) {
1547
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.`
1548
1671
  );
1549
1672
  }
1550
- await sleep(DECLARE_POLL_MS);
1673
+ await sleep2(DECLARE_POLL_MS);
1551
1674
  }
1552
1675
  }
1553
1676
  function exportRefusalAdvice(error, index) {
@@ -2167,7 +2290,7 @@ async function runSwitchEngine(client, opts, io, chatter) {
2167
2290
  );
2168
2291
  return;
2169
2292
  }
2170
- const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
2293
+ const sleep2 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
2171
2294
  let lastPhase = "";
2172
2295
  for (; ; ) {
2173
2296
  if (view.phase !== lastPhase) {
@@ -2175,7 +2298,7 @@ async function runSwitchEngine(client, opts, io, chatter) {
2175
2298
  chatter.stdout(`${opts.uid}: ${PHASES[view.phase] ?? view.phase}`);
2176
2299
  }
2177
2300
  if (view.status === "succeeded" || view.status === "failed") break;
2178
- await sleep(opts.pollMs ?? 3e3);
2301
+ await sleep2(opts.pollMs ?? 3e3);
2179
2302
  const status = await client.get(path2);
2180
2303
  if (!status.switch || status.switch.switchId !== view.switchId) {
2181
2304
  throw new CliError("The switch is no longer listed for this index.", {
@@ -2312,11 +2435,11 @@ function planMigration(config, applied, opts) {
2312
2435
  }
2313
2436
 
2314
2437
  // src/schema.ts
2315
- var import_fs7 = require("fs");
2438
+ var import_fs8 = require("fs");
2316
2439
  var import_path9 = require("path");
2317
2440
 
2318
2441
  // ../sync/src/migrations.ts
2319
- var import_fs6 = require("fs");
2442
+ var import_fs7 = require("fs");
2320
2443
  var import_path8 = require("path");
2321
2444
  var FILE_NAME_RE = /^(\d{4})_(.+)\.json$/;
2322
2445
  function sanitizeSlug(slug) {
@@ -2324,7 +2447,7 @@ function sanitizeSlug(slug) {
2324
2447
  return sanitized || "migration";
2325
2448
  }
2326
2449
  function generateMigrationFile(dir, ops, meta, now = () => /* @__PURE__ */ new Date()) {
2327
- (0, import_fs6.mkdirSync)(dir, { recursive: true });
2450
+ (0, import_fs7.mkdirSync)(dir, { recursive: true });
2328
2451
  const existing = readMigrations(dir);
2329
2452
  const nextVersion = existing.length === 0 ? 1 : existing[existing.length - 1].version + 1;
2330
2453
  const safeSlug = sanitizeSlug(meta.slug);
@@ -2337,13 +2460,13 @@ function generateMigrationFile(dir, ops, meta, now = () => /* @__PURE__ */ new D
2337
2460
  ...meta.targetHashes ? { targetHashes: meta.targetHashes } : {},
2338
2461
  ops
2339
2462
  };
2340
- (0, import_fs6.writeFileSync)(path2, JSON.stringify(file, null, 2));
2463
+ (0, import_fs7.writeFileSync)(path2, JSON.stringify(file, null, 2));
2341
2464
  return path2;
2342
2465
  }
2343
2466
  function readMigrations(dir) {
2344
2467
  let entries;
2345
2468
  try {
2346
- entries = (0, import_fs6.readdirSync)(dir);
2469
+ entries = (0, import_fs7.readdirSync)(dir);
2347
2470
  } catch {
2348
2471
  return [];
2349
2472
  }
@@ -2352,7 +2475,7 @@ function readMigrations(dir) {
2352
2475
  const match = FILE_NAME_RE.exec(entry);
2353
2476
  if (!match) continue;
2354
2477
  const path2 = (0, import_path8.join)(dir, entry);
2355
- const raw = (0, import_fs6.readFileSync)(path2, "utf-8");
2478
+ const raw = (0, import_fs7.readFileSync)(path2, "utf-8");
2356
2479
  let parsed;
2357
2480
  try {
2358
2481
  parsed = JSON.parse(raw);
@@ -2382,7 +2505,7 @@ function pendingMigrations(files, appliedVersion) {
2382
2505
  var BASE_BACKOFF_MS = 500;
2383
2506
  var DEFAULT_MAX_RETRIES = 5;
2384
2507
  function defaultSleep3(ms) {
2385
- return new Promise((resolve2) => setTimeout(resolve2, ms));
2508
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
2386
2509
  }
2387
2510
  var SchemaCasError = class extends Error {
2388
2511
  constructor(message) {
@@ -2549,17 +2672,17 @@ function printOpsTable(io, ops) {
2549
2672
  }
2550
2673
  function readAppliedMarker(dir) {
2551
2674
  const path2 = (0, import_path9.join)(dir, APPLIED_MARKER_FILE);
2552
- if (!(0, import_fs7.existsSync)(path2)) return 0;
2675
+ if (!(0, import_fs8.existsSync)(path2)) return 0;
2553
2676
  try {
2554
- const parsed = JSON.parse((0, import_fs7.readFileSync)(path2, "utf-8"));
2677
+ const parsed = JSON.parse((0, import_fs8.readFileSync)(path2, "utf-8"));
2555
2678
  return typeof parsed.version === "number" ? parsed.version : 0;
2556
2679
  } catch {
2557
2680
  return 0;
2558
2681
  }
2559
2682
  }
2560
2683
  function writeAppliedMarker(dir, version) {
2561
- (0, import_fs7.mkdirSync)(dir, { recursive: true });
2562
- (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));
2563
2686
  }
2564
2687
  function configHashes(config, index) {
2565
2688
  const def = config.indexes.find((d) => d.name === index);
@@ -3068,7 +3191,7 @@ async function runSchemaForget(deps) {
3068
3191
  }
3069
3192
 
3070
3193
  // src/sync.ts
3071
- var import_fs8 = require("fs");
3194
+ var import_fs9 = require("fs");
3072
3195
  var import_path10 = require("path");
3073
3196
  var import_crypto = require("crypto");
3074
3197
  var import_pg = require("pg");
@@ -3359,7 +3482,7 @@ function rowOutcomes(rows, resolved, failures) {
3359
3482
  return { done, retry, deadLetter };
3360
3483
  }
3361
3484
  function defaultSleep4(ms) {
3362
- return new Promise((resolve2) => setTimeout(resolve2, ms));
3485
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
3363
3486
  }
3364
3487
  function errorMessage(err) {
3365
3488
  if (!(err instanceof Error)) return String(err);
@@ -3399,7 +3522,7 @@ async function runSyncWorker(opts) {
3399
3522
  batchSize = DEFAULT_BATCH_SIZE,
3400
3523
  stopSignal,
3401
3524
  now = Date.now,
3402
- sleep = defaultSleep4,
3525
+ sleep: sleep2 = defaultSleep4,
3403
3526
  onHeartbeat,
3404
3527
  log = (message) => console.warn(message)
3405
3528
  } = opts;
@@ -3426,7 +3549,7 @@ async function runSyncWorker(opts) {
3426
3549
  log(`[headband] could not record this worker run: ${errorMessage(err)}`);
3427
3550
  }
3428
3551
  };
3429
- 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));
3430
3553
  const processBatch = async (batch) => {
3431
3554
  const changes = batch.map((row) => ({
3432
3555
  model: row.model,
@@ -3468,7 +3591,7 @@ async function runSyncWorker(opts) {
3468
3591
  const rows = await outbox.claim(batchSize);
3469
3592
  if (rows.length === 0) {
3470
3593
  if (!loop) break;
3471
- await sleep(idleDelayMs);
3594
+ await sleep2(idleDelayMs);
3472
3595
  continue;
3473
3596
  }
3474
3597
  let outcome;
@@ -3627,8 +3750,8 @@ async function runSyncInit(deps) {
3627
3750
  deps.io.stdout(ddl);
3628
3751
  if (deps.write) {
3629
3752
  const path2 = (0, import_path10.join)(deps.cwd ?? process.cwd(), OUTBOX_SQL_RELATIVE_PATH);
3630
- (0, import_fs8.mkdirSync)((0, import_path10.join)(deps.cwd ?? process.cwd(), "headband"), { recursive: true });
3631
- (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}
3632
3755
  `);
3633
3756
  deps.io.stdout(`Wrote ${path2}`);
3634
3757
  }
@@ -4014,10 +4137,10 @@ async function runSelfHostedSyncStatus(deps) {
4014
4137
  }
4015
4138
  function runSyncScaffoldDocker(deps) {
4016
4139
  const path2 = (0, import_path10.join)(deps.cwd, DOCKERFILE_NAME);
4017
- if ((0, import_fs8.existsSync)(path2) && !deps.force) {
4140
+ if ((0, import_fs9.existsSync)(path2) && !deps.force) {
4018
4141
  throw new CliError(`${DOCKERFILE_NAME} already exists at ${path2}. Pass --force to overwrite.`);
4019
4142
  }
4020
- (0, import_fs8.writeFileSync)(path2, dockerfileContent(deps.cliVersion));
4143
+ (0, import_fs9.writeFileSync)(path2, dockerfileContent(deps.cliVersion));
4021
4144
  deps.io.stdout(`Wrote ${path2}`);
4022
4145
  deps.io.stdout("Add this service to your docker-compose.yml:");
4023
4146
  deps.io.stdout(COMPOSE_SNIPPET);
@@ -4194,10 +4317,10 @@ ${CONFIG_NOTE}`,
4194
4317
  };
4195
4318
 
4196
4319
  // src/sync-indexes.ts
4197
- var import_fs10 = require("fs");
4320
+ var import_fs11 = require("fs");
4198
4321
 
4199
4322
  // src/sync-sources.ts
4200
- var import_fs9 = require("fs");
4323
+ var import_fs10 = require("fs");
4201
4324
  var import_pg2 = require("pg");
4202
4325
 
4203
4326
  // ../../src/lib/sync/copy.ts
@@ -4264,7 +4387,7 @@ var SOURCE_USAGE = [
4264
4387
  function readCaFile(deps, file) {
4265
4388
  let text;
4266
4389
  try {
4267
- text = (deps.readFile ?? ((p) => (0, import_fs9.readFileSync)(p, "utf8")))(file);
4390
+ text = (deps.readFile ?? ((p) => (0, import_fs10.readFileSync)(p, "utf8")))(file);
4268
4391
  } catch {
4269
4392
  throw new CliError("Cannot read the --ca-file.");
4270
4393
  }
@@ -4905,7 +5028,7 @@ var PREVIEW_MAX_IDS = 20;
4905
5028
  var ATTENTION_INDEX = /* @__PURE__ */ new Set(["error", "needs_rebuild", "awaiting_setup", "degraded"]);
4906
5029
  var ATTENTION_SOURCE = /* @__PURE__ */ new Set(["error", "awaiting_setup"]);
4907
5030
  var path = (id, suffix = "") => `/v1/sync/indexes/${encodeURIComponent(id)}${suffix}`;
4908
- function readDefinition(file, readFile = (p) => (0, import_fs10.readFileSync)(p, "utf8")) {
5031
+ function readDefinition(file, readFile = (p) => (0, import_fs11.readFileSync)(p, "utf8")) {
4909
5032
  let text;
4910
5033
  try {
4911
5034
  text = readFile(file);
@@ -5324,6 +5447,8 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
5324
5447
  "--branch",
5325
5448
  "--parent",
5326
5449
  "--expires-in",
5450
+ "--env-out",
5451
+ "--from",
5327
5452
  "--in",
5328
5453
  "--at",
5329
5454
  "--level",
@@ -5496,12 +5621,13 @@ Commands:
5496
5621
  projects create <name> --engine <meilisearch|typesense|elasticsearch|visor> [--json]
5497
5622
  projects delete <id> [--yes] [--json]
5498
5623
  branch list [--project <id>] [--json]
5499
- 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]
5500
5626
  branch delete <name> [--project <id>] [--yes] [--json]
5501
5627
  branch protect|unprotect <name> [--project <id>] [--json]
5502
5628
  branch expire <name> (--in <2h|7d> | --at <iso> | --clear) [--project <id>] [--json]
5503
5629
  branch checkout <name> [--project <id>] [--json]
5504
- branch reset|rollback|restore|set-primary (coming with bucket storage)
5630
+ branch reset|rollback|restore|set-primary (not available yet)
5505
5631
  keys list [--json]
5506
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]
5507
5633
  keys revoke <id> [--yes] [--json]
@@ -6054,7 +6180,7 @@ async function runIndexes(args, flags, io, confirm, cwd) {
6054
6180
  }
6055
6181
  if (settingsAction === "set") {
6056
6182
  const file = requiredFlag(flags, "--file");
6057
- const settings = JSON.parse((0, import_fs12.readFileSync)(file, "utf-8"));
6183
+ const settings = JSON.parse((0, import_fs13.readFileSync)(file, "utf-8"));
6058
6184
  const response = await client.patch(`/v1/indexes/${encode(uid)}/settings`, settings);
6059
6185
  printJson(io, response);
6060
6186
  return;
@@ -6115,7 +6241,7 @@ async function runDocsExport(flags, context, cwd) {
6115
6241
  return;
6116
6242
  }
6117
6243
  const partial = `${out}.partial`;
6118
- const fd = (0, import_fs11.openSync)(partial, "w", 384);
6244
+ const fd = (0, import_fs12.openSync)(partial, "w", 384);
6119
6245
  let exported;
6120
6246
  try {
6121
6247
  exported = await exportDocuments(client, {
@@ -6123,26 +6249,26 @@ async function runDocsExport(flags, context, cwd) {
6123
6249
  format,
6124
6250
  batchSize,
6125
6251
  write: (line) => {
6126
- (0, import_fs11.writeSync)(fd, `${line}
6252
+ (0, import_fs12.writeSync)(fd, `${line}
6127
6253
  `);
6128
6254
  },
6129
6255
  onPage: (count, total) => io.stderr(`Exported ${count}${total === null ? "" : `/${total}`} documents`),
6130
6256
  finishOnError: true
6131
6257
  });
6132
- (0, import_fs11.closeSync)(fd);
6258
+ (0, import_fs12.closeSync)(fd);
6133
6259
  } catch (error) {
6134
- (0, import_fs11.closeSync)(fd);
6260
+ (0, import_fs12.closeSync)(fd);
6135
6261
  const kept = error instanceof ExportInterruptedError ? error.exported : 0;
6136
6262
  if (kept > 0) {
6137
6263
  io.stderr(
6138
6264
  `Kept the ${kept} ${kept === 1 ? "document" : "documents"} exported before the error in ${partial}. ${out} was not written.`
6139
6265
  );
6140
6266
  } else {
6141
- (0, import_fs11.rmSync)(partial, { force: true });
6267
+ (0, import_fs12.rmSync)(partial, { force: true });
6142
6268
  }
6143
6269
  throw exportFailure(error instanceof ExportInterruptedError ? error.cause : error, index);
6144
6270
  }
6145
- (0, import_fs11.renameSync)(partial, out);
6271
+ (0, import_fs12.renameSync)(partial, out);
6146
6272
  if (boolFlag(flags, "--json")) printJson(io, { index, out, format, exported });
6147
6273
  else io.stdout(`Exported ${exported} documents to ${out}.`);
6148
6274
  }
@@ -6513,7 +6639,7 @@ async function runSync(args, flags, io, confirm, cwd) {
6513
6639
  printCommandHelp("sync", io);
6514
6640
  throw new UsageError("Unknown sync command");
6515
6641
  }
6516
- var CLI_VERSION = true ? "0.6.1" : "dev";
6642
+ var CLI_VERSION = true ? "0.7.0" : "dev";
6517
6643
  async function run(argv, context = { io: defaultIO }) {
6518
6644
  if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v" || argv[0] === "version")) {
6519
6645
  context.io.stdout(CLI_VERSION);
@@ -6615,7 +6741,7 @@ async function main(argv, context = { io: defaultIO }) {
6615
6741
  function stdoutLineWriter(line) {
6616
6742
  if (process.stdout.write(`${line}
6617
6743
  `)) return;
6618
- return new Promise((resolve2) => process.stdout.once("drain", () => resolve2()));
6744
+ return new Promise((resolve3) => process.stdout.once("drain", () => resolve3()));
6619
6745
  }
6620
6746
  function isDirectRun() {
6621
6747
  return typeof require !== "undefined" && typeof module !== "undefined" && require.main === module;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@headband/cli",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "CLI tool for setting up Headband search in any project",
5
5
  "bin": {
6
6
  "headband": "dist/index.js"