@headband/cli 0.6.1 → 0.7.1

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 +27 -10
  2. package/dist/index.js +202 -66
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -83,6 +83,7 @@ A script, CI job or agent does not need to log in. Give it a key:
83
83
  | `HEADBAND_API_KEY` | `--token` | The API key to send. Used instead of any stored login. |
84
84
  | `HEADBAND_URL` | `--url` | The Headband URL. |
85
85
  | `HEADBAND_PROFILE` | `--profile` | Which stored login to use. |
86
+ | `HEADBAND_API_URL` | | Where `/v1` calls go, if not the default below. |
86
87
 
87
88
  Precedence, highest first:
88
89
 
@@ -92,6 +93,10 @@ Precedence, highest first:
92
93
  name the profile with `--profile`.
93
94
  - **Profile:** `--profile`, then `HEADBAND_PROFILE`, then the current profile,
94
95
  then the only stored profile when there is one.
96
+ - **API host:** for `https://headband.dev`, every `/v1` call goes to
97
+ `https://api.headband.dev`, the API host on the servers that run the engines
98
+ (sign-in stays on `headband.dev`). Any other URL is used as is.
99
+ `HEADBAND_API_URL` overrides both.
95
100
 
96
101
  A stored key is only ever sent to the URL it was issued for. `--url` with no
97
102
  `--profile` picks the stored profile for that URL; `--url` with a `--profile`
@@ -187,7 +192,8 @@ has its own indexes and its own keys.
187
192
 
188
193
  ```bash
189
194
  headband branch list [--project <id>] [--json]
190
- headband branch create <name> --empty [--parent <name>] [--expires-in <2h|7d>] [--project <id>] [--json]
195
+ headband branch create <name> [--empty] [--parent <name>] [--expires-in <2h|7d>] [--project <id>] [--json]
196
+ headband branch ensure <name> [--from <parent>] [--expires-in <7d>] [--env-out <file>] [--project <id>] [--json]
191
197
  headband branch delete <name> [--project <id>] [--yes] [--json]
192
198
  headband branch protect <name> [--project <id>] [--json]
193
199
  headband branch unprotect <name> [--project <id>] [--json]
@@ -197,13 +203,25 @@ headband branch checkout <name> [--project <id>] [--json]
197
203
 
198
204
  - The project comes from `--project`, else the directory's checkout (below).
199
205
  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.
206
+ - `create` forks the parent (`--parent`, default: the primary branch): the
207
+ new branch starts with the parent's indexes, documents and settings,
208
+ copy-on-write. `--empty` makes a branch with no indexes instead. A parent
209
+ with an index that cannot fork (Typesense, Visor, your own engine) fails
210
+ with exit `6` (code `fork_not_supported`). Until forking is on for your
211
+ organization, a fork fails with exit `6` (code `fork_not_available`), and
212
+ until branches other than main open, every `create` fails with exit `6`
213
+ (code `branches_not_open`). `--expires-in` deletes the branch on its own
214
+ after that long. Forking by default is new in CLI 0.7.0: before it,
215
+ `create` needed `--empty`.
216
+ - `ensure` is for preview deploys, such as a Vercel build step. It forks the
217
+ branch from `--from` (default: the primary) unless it exists, pushes its
218
+ expiry `--expires-in` out (default `7d`), mints a search key and an admin
219
+ key on it, and writes `HEADBAND_URL`, `HEADBAND_PROJECT_ID`,
220
+ `HEADBAND_BRANCH`, `HEADBAND_BRANCH_ID`, `HEADBAND_SEARCH_KEY`,
221
+ `HEADBAND_ADMIN_KEY` and one `HEADBAND_<ENGINE>_URL` per engine to
222
+ `--env-out <file>`, replacing any `HEADBAND_*` lines there. Without
223
+ `--env-out` it prints them. The name is made branch-safe first, so a git
224
+ branch name works as is. It refuses the primary branch (CLI 0.7.0).
207
225
  - `delete` removes the branch with its indexes, keys and engines. It asks
208
226
  first. The primary branch, a protected branch, a branch with live child
209
227
  branches and a branch with Sync-owned indexes are refused (exit `6`).
@@ -269,8 +287,7 @@ error, so a name is never matched across projects by accident.
269
287
  project of their own and would ignore it. The exception is `keys create`, which
270
288
  sends the project in its body (see Keys).
271
289
 
272
- Coming with bucket storage: `branch create` without `--empty` (a copy of its
273
- parent), `reset`, `rollback`, `restore` and `set-primary`.
290
+ Not available yet: `reset`, `rollback`, `restore` and `set-primary`.
274
291
 
275
292
  ### Keys
276
293
 
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;
@@ -583,8 +583,16 @@ async function apiRequest(baseUrl, path2, init2 = {}) {
583
583
  }
584
584
  return body;
585
585
  }
586
+ var DEFAULT_API_URL = "https://api.headband.dev";
587
+ function apiUrlFor(url, env = process.env) {
588
+ const override = env.HEADBAND_API_URL?.trim();
589
+ if (override) return normalizeUrl(override);
590
+ const normalized = normalizeUrl(url);
591
+ return normalized === DEFAULT_URL ? DEFAULT_API_URL : normalized;
592
+ }
586
593
  function createApiClient(url, profile) {
587
- const request = (path2, init2 = {}) => apiRequest(url, path2, {
594
+ const apiUrl = apiUrlFor(url);
595
+ const request = (path2, init2 = {}) => apiRequest(apiUrl, path2, {
588
596
  ...init2,
589
597
  apiKey: profile.apiKey,
590
598
  headers: profile.headers,
@@ -600,7 +608,7 @@ function createApiClient(url, profile) {
600
608
  }
601
609
 
602
610
  // src/auth.ts
603
- var defaultSleep2 = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
611
+ var defaultSleep2 = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
604
612
  function openBrowser(url) {
605
613
  try {
606
614
  if (process.platform === "darwin") {
@@ -615,7 +623,7 @@ function openBrowser(url) {
615
623
  }
616
624
  async function runDeviceFlow(url, io, options = {}) {
617
625
  const baseUrl = normalizeUrl(url);
618
- const sleep = options.sleep || defaultSleep2;
626
+ const sleep2 = options.sleep || defaultSleep2;
619
627
  const now = options.now || Date.now;
620
628
  const device = await apiRequest(baseUrl, "/api/cli/device", {
621
629
  method: "POST",
@@ -630,7 +638,7 @@ async function runDeviceFlow(url, io, options = {}) {
630
638
  let interval = device.data.interval;
631
639
  const deadline = now() + device.data.expiresIn * 1e3;
632
640
  while (now() < deadline) {
633
- await sleep(interval * 1e3);
641
+ await sleep2(interval * 1e3);
634
642
  try {
635
643
  const token = await apiRequest(baseUrl, "/api/cli/token", {
636
644
  method: "POST",
@@ -685,6 +693,7 @@ async function login(url, io, options = {}) {
685
693
  }
686
694
 
687
695
  // src/branch.ts
696
+ var import_fs4 = require("fs");
688
697
  var import_path5 = require("path");
689
698
 
690
699
  // src/output.ts
@@ -894,14 +903,22 @@ var PART_2 = /* @__PURE__ */ new Set(["reset", "rollback", "restore", "set-prima
894
903
  var BRANCH_USAGE = [
895
904
  "Usage:",
896
905
  " headband branch list [--project <id>] [--json]",
897
- " headband branch create <name> --empty [--parent <name>] [--expires-in <2h|7d>] [--project <id>] [--json]",
906
+ " headband branch create <name> [--empty] [--parent <name>] [--expires-in <2h|7d>] [--project <id>] [--json]",
907
+ " headband branch ensure <name> [--from <parent>] [--expires-in <7d>] [--env-out <file>] [--project <id>] [--json]",
898
908
  " headband branch delete <name> [--project <id>] [--yes] [--json]",
899
909
  " headband branch protect <name> [--project <id>] [--json]",
900
910
  " headband branch unprotect <name> [--project <id>] [--json]",
901
911
  " headband branch expire <name> (--in <2h|7d> | --at <iso> | --clear) [--project <id>] [--json]",
902
912
  " 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",
913
+ " headband branch reset|rollback|restore|set-primary <name> not available yet",
914
+ "",
915
+ "create without --empty forks the parent (default: main): its indexes, documents and settings,",
916
+ "copy-on-write. --empty makes a branch with no indexes.",
917
+ "",
918
+ "ensure is for preview deploys: it forks the branch unless it exists, pushes its expiry out",
919
+ "(--expires-in, default 7d), mints a search key and an admin key for it, and writes HEADBAND_*",
920
+ "variables to --env-out (or prints them). The name is made branch-safe first: feature/Foo_Bar",
921
+ "becomes feature/foo_bar. It refuses the primary branch.",
905
922
  "",
906
923
  "The project comes from --project, else the directory's checkout (.headband/project.json).",
907
924
  "Durations take s, m, h, d or w: 30m, 2h, 7d.",
@@ -1000,10 +1017,7 @@ async function runBranch(args, flags, deps) {
1000
1017
  const action = args[0];
1001
1018
  const json = flags["--json"] === true;
1002
1019
  if (action && PART_2.has(action)) {
1003
- throw new UsageError(
1004
- `headband branch ${action} is coming with bucket storage.`,
1005
- "not_available"
1006
- );
1020
+ throw new UsageError(`headband branch ${action} is not available yet.`, "not_available");
1007
1021
  }
1008
1022
  const client = createApiClient(auth.url, { apiKey: auth.apiKey });
1009
1023
  const checkout = () => checkoutFor(cwd, auth.profile?.orgId);
@@ -1032,18 +1046,35 @@ async function runBranch(args, flags, deps) {
1032
1046
  if (error instanceof ApiError && error.code === "branches_not_open") {
1033
1047
  throw restate(error, "Branches other than main are not open yet.");
1034
1048
  }
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;
1049
+ throw restateCreate(error);
1042
1050
  }
1043
1051
  if (json) printJson(io, response);
1044
1052
  else printTable(io, [response.data], COLUMNS);
1045
1053
  return;
1046
1054
  }
1055
+ if (action === "ensure") {
1056
+ const result = await ensureBranch(client, {
1057
+ name: branchNameFrom(requiredName(args[1])),
1058
+ project: projectFor(flags, checkout),
1059
+ from: stringFlag2(flags, "--from"),
1060
+ expiresInMs: parseDuration(stringFlag2(flags, "--expires-in") ?? "7d"),
1061
+ url: auth.url
1062
+ });
1063
+ const envOut = stringFlag2(flags, "--env-out");
1064
+ if (envOut && envOut !== "-") writeEnvFile((0, import_path5.resolve)(cwd, envOut), result.env);
1065
+ if (json) {
1066
+ printJson(io, { branch: result.branch, created: result.created, env: result.env });
1067
+ } else if (envOut && envOut !== "-") {
1068
+ const engines = result.branch.instances?.length ?? 0;
1069
+ io.stdout(
1070
+ `${result.created ? "Forked" : "Reused"} branch ${result.branch.name} (${engines} engine${engines === 1 ? "" : "s"}), expires ${result.branch.expiresAt}.`
1071
+ );
1072
+ io.stdout(`Wrote ${Object.keys(result.env).length} HEADBAND_* variables to ${envOut}.`);
1073
+ } else {
1074
+ io.stdout(dotenv(result.env));
1075
+ }
1076
+ return;
1077
+ }
1047
1078
  if (action === "delete") {
1048
1079
  const name = requiredName(args[1]);
1049
1080
  const project = projectFor(flags, checkout);
@@ -1115,9 +1146,109 @@ async function runBranch(args, flags, deps) {
1115
1146
  io.stdout(BRANCH_USAGE);
1116
1147
  throw new UsageError("Unknown branch command");
1117
1148
  }
1149
+ function restateCreate(error) {
1150
+ if (error instanceof ApiError && error.code === "branches_not_open") {
1151
+ return restate(error, "Branches other than main are not open yet.");
1152
+ }
1153
+ if (error instanceof ApiError && error.code === "fork_not_available") {
1154
+ return restate(error, "Forking is not open yet. Pass --empty for an empty branch.");
1155
+ }
1156
+ return error;
1157
+ }
1158
+ function branchNameFrom(ref) {
1159
+ const name = ref.trim().toLowerCase().replace(/[^a-z0-9._/-]+/g, "-").slice(0, 63).replace(/^[^a-z0-9]+/, "").replace(/[^a-z0-9]+$/, "");
1160
+ if (!name) throw new UsageError(`"${ref}" has no letters or digits to make a branch name from.`);
1161
+ return name;
1162
+ }
1163
+ var ENSURE_WAIT_MS = 6e4;
1164
+ var ENSURE_POLL_MS = 2e3;
1165
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1166
+ var engineVar = (engine) => `HEADBAND_${engine.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_URL`;
1167
+ async function ensureBranch(client, o, now = Date.now) {
1168
+ var _a;
1169
+ const path2 = `/v1/branches/${encodeURIComponent(o.name)}?project=${encodeURIComponent(o.project)}`;
1170
+ const read = async () => {
1171
+ try {
1172
+ return (await client.get(path2)).data;
1173
+ } catch (error) {
1174
+ if (error instanceof ApiError && error.status === 404 && error.code === "branch_not_found") {
1175
+ return null;
1176
+ }
1177
+ throw error;
1178
+ }
1179
+ };
1180
+ const expiresAt = () => new Date(now() + o.expiresInMs).toISOString();
1181
+ let branch = await read();
1182
+ let created = false;
1183
+ if (!branch) {
1184
+ const body = {
1185
+ name: o.name,
1186
+ projectId: o.project,
1187
+ expiresAt: expiresAt()
1188
+ };
1189
+ if (o.from) body.parent = o.from;
1190
+ try {
1191
+ branch = (await client.post("/v1/branches", body)).data;
1192
+ created = true;
1193
+ } catch (error) {
1194
+ if (!(error instanceof ApiError && error.code === "branch_exists"))
1195
+ throw restateCreate(error);
1196
+ branch = await read();
1197
+ if (!branch) throw error;
1198
+ }
1199
+ }
1200
+ if (branch.primary) {
1201
+ throw new UsageError(
1202
+ `${branch.name} is the primary branch. ensure is for preview branches; run it only on preview builds.`
1203
+ );
1204
+ }
1205
+ for (const until = now() + ENSURE_WAIT_MS; branch.state === "creating" && now() < until; ) {
1206
+ await sleep(ENSURE_POLL_MS);
1207
+ branch = await read() ?? branch;
1208
+ }
1209
+ if (branch.state !== "ready") {
1210
+ throw new CliError(`Branch ${branch.name} is ${branch.state}, not ready. Try again shortly.`, {
1211
+ exitCode: EXIT_CODES.conflict,
1212
+ code: "branch_not_ready"
1213
+ });
1214
+ }
1215
+ if (!created && !branch.protected) {
1216
+ branch = {
1217
+ ...branch,
1218
+ ...(await client.patch(path2, { expiresAt: expiresAt() })).data
1219
+ };
1220
+ }
1221
+ if (!branch.instances)
1222
+ branch = { ...branch, ...(await client.get(path2)).data };
1223
+ const mint = async (type) => (await client.post("/v1/keys", {
1224
+ name: `preview ${branch.name} ${type}`,
1225
+ type,
1226
+ projectId: o.project,
1227
+ branch: branch.id
1228
+ })).data.apiKey;
1229
+ const env = {
1230
+ HEADBAND_URL: o.url,
1231
+ HEADBAND_PROJECT_ID: o.project,
1232
+ HEADBAND_BRANCH: branch.name,
1233
+ HEADBAND_BRANCH_ID: branch.id,
1234
+ HEADBAND_SEARCH_KEY: await mint("search"),
1235
+ HEADBAND_ADMIN_KEY: await mint("admin")
1236
+ };
1237
+ for (const i of branch.instances ?? []) env[_a = engineVar(i.engine)] ?? (env[_a] = i.endpoint);
1238
+ return { branch, created, env };
1239
+ }
1240
+ var quote = (v) => /^[A-Za-z0-9_./:@+-]*$/.test(v) ? v : JSON.stringify(v);
1241
+ function dotenv(env) {
1242
+ return Object.entries(env).map(([k, v]) => `${k}=${quote(v)}`).join("\n");
1243
+ }
1244
+ function writeEnvFile(file, env) {
1245
+ 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)) : [];
1246
+ (0, import_fs4.writeFileSync)(file, [...kept, dotenv(env)].join("\n") + "\n", { mode: 384 });
1247
+ (0, import_fs4.chmodSync)(file, 384);
1248
+ }
1118
1249
 
1119
1250
  // src/config-load.ts
1120
- var import_fs4 = require("fs");
1251
+ var import_fs5 = require("fs");
1121
1252
  var import_path6 = require("path");
1122
1253
  var import_url = require("url");
1123
1254
  var import_jiti = require("jiti");
@@ -1253,7 +1384,7 @@ var SYNC_CONFIG_SHAPE = 'a headband.config.ts whose default export is defineConf
1253
1384
  function findConfigFile(cwd) {
1254
1385
  for (const name of CONFIG_FILENAMES) {
1255
1386
  const path2 = (0, import_path6.join)(cwd, name);
1256
- if ((0, import_fs4.existsSync)(path2)) return path2;
1387
+ if ((0, import_fs5.existsSync)(path2)) return path2;
1257
1388
  }
1258
1389
  return void 0;
1259
1390
  }
@@ -1373,10 +1504,10 @@ function missingAdapterError(command, why) {
1373
1504
 
1374
1505
  // src/docs.ts
1375
1506
  var import_path7 = require("path");
1376
- var import_fs5 = require("fs");
1507
+ var import_fs6 = require("fs");
1377
1508
  var DOCS_BATCH_SIZE = 1e3;
1378
1509
  function parseDocumentsFile(path2) {
1379
- const content = (0, import_fs5.readFileSync)(path2, "utf-8");
1510
+ const content = (0, import_fs6.readFileSync)(path2, "utf-8");
1380
1511
  const ext = (0, import_path7.extname)(path2).toLowerCase();
1381
1512
  if (ext === ".jsonl") {
1382
1513
  return content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
@@ -1501,7 +1632,7 @@ function refusedWalk(error) {
1501
1632
  var DECLARE_WAIT_MS = 15 * 6e4;
1502
1633
  var DECLARE_POLL_MS = 3e3;
1503
1634
  async function declareExportKey(client, index, io, opts = {}) {
1504
- const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
1635
+ const sleep2 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
1505
1636
  const now = opts.now ?? Date.now;
1506
1637
  const probe = () => client.get(`/v1/documents?index=${encodeURIComponent(index)}&limit=1&cursor=`);
1507
1638
  let refused;
@@ -1547,7 +1678,7 @@ async function declareExportKey(client, index, io, opts = {}) {
1547
1678
  `\`${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
1679
  );
1549
1680
  }
1550
- await sleep(DECLARE_POLL_MS);
1681
+ await sleep2(DECLARE_POLL_MS);
1551
1682
  }
1552
1683
  }
1553
1684
  function exportRefusalAdvice(error, index) {
@@ -1613,6 +1744,7 @@ function documentsMatch(expected, remote, primaryKey) {
1613
1744
  }
1614
1745
 
1615
1746
  // ../../src/lib/density/catalog-constants.ts
1747
+ var FREE_ALWAYS_ON_CLOSED_AT = Date.parse("2026-11-10T00:00:00Z");
1616
1748
  function formatMemory(mb) {
1617
1749
  return mb >= 1024 ? `${mb / 1024} GB` : `${mb} MB`;
1618
1750
  }
@@ -2167,7 +2299,7 @@ async function runSwitchEngine(client, opts, io, chatter) {
2167
2299
  );
2168
2300
  return;
2169
2301
  }
2170
- const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
2302
+ const sleep2 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
2171
2303
  let lastPhase = "";
2172
2304
  for (; ; ) {
2173
2305
  if (view.phase !== lastPhase) {
@@ -2175,7 +2307,7 @@ async function runSwitchEngine(client, opts, io, chatter) {
2175
2307
  chatter.stdout(`${opts.uid}: ${PHASES[view.phase] ?? view.phase}`);
2176
2308
  }
2177
2309
  if (view.status === "succeeded" || view.status === "failed") break;
2178
- await sleep(opts.pollMs ?? 3e3);
2310
+ await sleep2(opts.pollMs ?? 3e3);
2179
2311
  const status = await client.get(path2);
2180
2312
  if (!status.switch || status.switch.switchId !== view.switchId) {
2181
2313
  throw new CliError("The switch is no longer listed for this index.", {
@@ -2312,11 +2444,11 @@ function planMigration(config, applied, opts) {
2312
2444
  }
2313
2445
 
2314
2446
  // src/schema.ts
2315
- var import_fs7 = require("fs");
2447
+ var import_fs8 = require("fs");
2316
2448
  var import_path9 = require("path");
2317
2449
 
2318
2450
  // ../sync/src/migrations.ts
2319
- var import_fs6 = require("fs");
2451
+ var import_fs7 = require("fs");
2320
2452
  var import_path8 = require("path");
2321
2453
  var FILE_NAME_RE = /^(\d{4})_(.+)\.json$/;
2322
2454
  function sanitizeSlug(slug) {
@@ -2324,7 +2456,7 @@ function sanitizeSlug(slug) {
2324
2456
  return sanitized || "migration";
2325
2457
  }
2326
2458
  function generateMigrationFile(dir, ops, meta, now = () => /* @__PURE__ */ new Date()) {
2327
- (0, import_fs6.mkdirSync)(dir, { recursive: true });
2459
+ (0, import_fs7.mkdirSync)(dir, { recursive: true });
2328
2460
  const existing = readMigrations(dir);
2329
2461
  const nextVersion = existing.length === 0 ? 1 : existing[existing.length - 1].version + 1;
2330
2462
  const safeSlug = sanitizeSlug(meta.slug);
@@ -2337,13 +2469,13 @@ function generateMigrationFile(dir, ops, meta, now = () => /* @__PURE__ */ new D
2337
2469
  ...meta.targetHashes ? { targetHashes: meta.targetHashes } : {},
2338
2470
  ops
2339
2471
  };
2340
- (0, import_fs6.writeFileSync)(path2, JSON.stringify(file, null, 2));
2472
+ (0, import_fs7.writeFileSync)(path2, JSON.stringify(file, null, 2));
2341
2473
  return path2;
2342
2474
  }
2343
2475
  function readMigrations(dir) {
2344
2476
  let entries;
2345
2477
  try {
2346
- entries = (0, import_fs6.readdirSync)(dir);
2478
+ entries = (0, import_fs7.readdirSync)(dir);
2347
2479
  } catch {
2348
2480
  return [];
2349
2481
  }
@@ -2352,7 +2484,7 @@ function readMigrations(dir) {
2352
2484
  const match = FILE_NAME_RE.exec(entry);
2353
2485
  if (!match) continue;
2354
2486
  const path2 = (0, import_path8.join)(dir, entry);
2355
- const raw = (0, import_fs6.readFileSync)(path2, "utf-8");
2487
+ const raw = (0, import_fs7.readFileSync)(path2, "utf-8");
2356
2488
  let parsed;
2357
2489
  try {
2358
2490
  parsed = JSON.parse(raw);
@@ -2382,7 +2514,7 @@ function pendingMigrations(files, appliedVersion) {
2382
2514
  var BASE_BACKOFF_MS = 500;
2383
2515
  var DEFAULT_MAX_RETRIES = 5;
2384
2516
  function defaultSleep3(ms) {
2385
- return new Promise((resolve2) => setTimeout(resolve2, ms));
2517
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
2386
2518
  }
2387
2519
  var SchemaCasError = class extends Error {
2388
2520
  constructor(message) {
@@ -2549,17 +2681,17 @@ function printOpsTable(io, ops) {
2549
2681
  }
2550
2682
  function readAppliedMarker(dir) {
2551
2683
  const path2 = (0, import_path9.join)(dir, APPLIED_MARKER_FILE);
2552
- if (!(0, import_fs7.existsSync)(path2)) return 0;
2684
+ if (!(0, import_fs8.existsSync)(path2)) return 0;
2553
2685
  try {
2554
- const parsed = JSON.parse((0, import_fs7.readFileSync)(path2, "utf-8"));
2686
+ const parsed = JSON.parse((0, import_fs8.readFileSync)(path2, "utf-8"));
2555
2687
  return typeof parsed.version === "number" ? parsed.version : 0;
2556
2688
  } catch {
2557
2689
  return 0;
2558
2690
  }
2559
2691
  }
2560
2692
  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));
2693
+ (0, import_fs8.mkdirSync)(dir, { recursive: true });
2694
+ (0, import_fs8.writeFileSync)((0, import_path9.join)(dir, APPLIED_MARKER_FILE), JSON.stringify({ version }, null, 2));
2563
2695
  }
2564
2696
  function configHashes(config, index) {
2565
2697
  const def = config.indexes.find((d) => d.name === index);
@@ -3068,7 +3200,7 @@ async function runSchemaForget(deps) {
3068
3200
  }
3069
3201
 
3070
3202
  // src/sync.ts
3071
- var import_fs8 = require("fs");
3203
+ var import_fs9 = require("fs");
3072
3204
  var import_path10 = require("path");
3073
3205
  var import_crypto = require("crypto");
3074
3206
  var import_pg = require("pg");
@@ -3359,7 +3491,7 @@ function rowOutcomes(rows, resolved, failures) {
3359
3491
  return { done, retry, deadLetter };
3360
3492
  }
3361
3493
  function defaultSleep4(ms) {
3362
- return new Promise((resolve2) => setTimeout(resolve2, ms));
3494
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
3363
3495
  }
3364
3496
  function errorMessage(err) {
3365
3497
  if (!(err instanceof Error)) return String(err);
@@ -3399,7 +3531,7 @@ async function runSyncWorker(opts) {
3399
3531
  batchSize = DEFAULT_BATCH_SIZE,
3400
3532
  stopSignal,
3401
3533
  now = Date.now,
3402
- sleep = defaultSleep4,
3534
+ sleep: sleep2 = defaultSleep4,
3403
3535
  onHeartbeat,
3404
3536
  log = (message) => console.warn(message)
3405
3537
  } = opts;
@@ -3426,7 +3558,7 @@ async function runSyncWorker(opts) {
3426
3558
  log(`[headband] could not record this worker run: ${errorMessage(err)}`);
3427
3559
  }
3428
3560
  };
3429
- const backoff = (attempt) => sleep(Math.min(failureDelayMs * 2 ** (attempt - 1), MAX_FAILURE_DELAY_MS));
3561
+ const backoff = (attempt) => sleep2(Math.min(failureDelayMs * 2 ** (attempt - 1), MAX_FAILURE_DELAY_MS));
3430
3562
  const processBatch = async (batch) => {
3431
3563
  const changes = batch.map((row) => ({
3432
3564
  model: row.model,
@@ -3468,7 +3600,7 @@ async function runSyncWorker(opts) {
3468
3600
  const rows = await outbox.claim(batchSize);
3469
3601
  if (rows.length === 0) {
3470
3602
  if (!loop) break;
3471
- await sleep(idleDelayMs);
3603
+ await sleep2(idleDelayMs);
3472
3604
  continue;
3473
3605
  }
3474
3606
  let outcome;
@@ -3627,8 +3759,8 @@ async function runSyncInit(deps) {
3627
3759
  deps.io.stdout(ddl);
3628
3760
  if (deps.write) {
3629
3761
  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}
3762
+ (0, import_fs9.mkdirSync)((0, import_path10.join)(deps.cwd ?? process.cwd(), "headband"), { recursive: true });
3763
+ (0, import_fs9.writeFileSync)(path2, `${ddl}
3632
3764
  `);
3633
3765
  deps.io.stdout(`Wrote ${path2}`);
3634
3766
  }
@@ -4014,10 +4146,10 @@ async function runSelfHostedSyncStatus(deps) {
4014
4146
  }
4015
4147
  function runSyncScaffoldDocker(deps) {
4016
4148
  const path2 = (0, import_path10.join)(deps.cwd, DOCKERFILE_NAME);
4017
- if ((0, import_fs8.existsSync)(path2) && !deps.force) {
4149
+ if ((0, import_fs9.existsSync)(path2) && !deps.force) {
4018
4150
  throw new CliError(`${DOCKERFILE_NAME} already exists at ${path2}. Pass --force to overwrite.`);
4019
4151
  }
4020
- (0, import_fs8.writeFileSync)(path2, dockerfileContent(deps.cliVersion));
4152
+ (0, import_fs9.writeFileSync)(path2, dockerfileContent(deps.cliVersion));
4021
4153
  deps.io.stdout(`Wrote ${path2}`);
4022
4154
  deps.io.stdout("Add this service to your docker-compose.yml:");
4023
4155
  deps.io.stdout(COMPOSE_SNIPPET);
@@ -4194,10 +4326,10 @@ ${CONFIG_NOTE}`,
4194
4326
  };
4195
4327
 
4196
4328
  // src/sync-indexes.ts
4197
- var import_fs10 = require("fs");
4329
+ var import_fs11 = require("fs");
4198
4330
 
4199
4331
  // src/sync-sources.ts
4200
- var import_fs9 = require("fs");
4332
+ var import_fs10 = require("fs");
4201
4333
  var import_pg2 = require("pg");
4202
4334
 
4203
4335
  // ../../src/lib/sync/copy.ts
@@ -4264,7 +4396,7 @@ var SOURCE_USAGE = [
4264
4396
  function readCaFile(deps, file) {
4265
4397
  let text;
4266
4398
  try {
4267
- text = (deps.readFile ?? ((p) => (0, import_fs9.readFileSync)(p, "utf8")))(file);
4399
+ text = (deps.readFile ?? ((p) => (0, import_fs10.readFileSync)(p, "utf8")))(file);
4268
4400
  } catch {
4269
4401
  throw new CliError("Cannot read the --ca-file.");
4270
4402
  }
@@ -4905,7 +5037,7 @@ var PREVIEW_MAX_IDS = 20;
4905
5037
  var ATTENTION_INDEX = /* @__PURE__ */ new Set(["error", "needs_rebuild", "awaiting_setup", "degraded"]);
4906
5038
  var ATTENTION_SOURCE = /* @__PURE__ */ new Set(["error", "awaiting_setup"]);
4907
5039
  var path = (id, suffix = "") => `/v1/sync/indexes/${encodeURIComponent(id)}${suffix}`;
4908
- function readDefinition(file, readFile = (p) => (0, import_fs10.readFileSync)(p, "utf8")) {
5040
+ function readDefinition(file, readFile = (p) => (0, import_fs11.readFileSync)(p, "utf8")) {
4909
5041
  let text;
4910
5042
  try {
4911
5043
  text = readFile(file);
@@ -5324,6 +5456,8 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
5324
5456
  "--branch",
5325
5457
  "--parent",
5326
5458
  "--expires-in",
5459
+ "--env-out",
5460
+ "--from",
5327
5461
  "--in",
5328
5462
  "--at",
5329
5463
  "--level",
@@ -5453,7 +5587,8 @@ function syncClientFromFlags(flags, cwd, options = {}) {
5453
5587
  return fetch(input, { ...init2, headers: merged });
5454
5588
  } : void 0;
5455
5589
  return new HeadbandClient({
5456
- url: auth.url,
5590
+ // The sync client calls /v1 only: the same API host as every other /v1 call (api.ts).
5591
+ url: apiUrlFor(auth.url),
5457
5592
  adminKey: auth.apiKey,
5458
5593
  ...fetchImpl ? { fetchImpl } : {}
5459
5594
  });
@@ -5496,12 +5631,13 @@ Commands:
5496
5631
  projects create <name> --engine <meilisearch|typesense|elasticsearch|visor> [--json]
5497
5632
  projects delete <id> [--yes] [--json]
5498
5633
  branch list [--project <id>] [--json]
5499
- branch create <name> --empty [--parent <name>] [--expires-in <2h|7d>] [--project <id>] [--json]
5634
+ branch create <name> [--empty] [--parent <name>] [--expires-in <2h|7d>] [--project <id>] [--json]
5635
+ branch ensure <name> [--from <parent>] [--expires-in <7d>] [--env-out <file>] [--project <id>] [--json]
5500
5636
  branch delete <name> [--project <id>] [--yes] [--json]
5501
5637
  branch protect|unprotect <name> [--project <id>] [--json]
5502
5638
  branch expire <name> (--in <2h|7d> | --at <iso> | --clear) [--project <id>] [--json]
5503
5639
  branch checkout <name> [--project <id>] [--json]
5504
- branch reset|rollback|restore|set-primary (coming with bucket storage)
5640
+ branch reset|rollback|restore|set-primary (not available yet)
5505
5641
  keys list [--json]
5506
5642
  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
5643
  keys revoke <id> [--yes] [--json]
@@ -6054,7 +6190,7 @@ async function runIndexes(args, flags, io, confirm, cwd) {
6054
6190
  }
6055
6191
  if (settingsAction === "set") {
6056
6192
  const file = requiredFlag(flags, "--file");
6057
- const settings = JSON.parse((0, import_fs12.readFileSync)(file, "utf-8"));
6193
+ const settings = JSON.parse((0, import_fs13.readFileSync)(file, "utf-8"));
6058
6194
  const response = await client.patch(`/v1/indexes/${encode(uid)}/settings`, settings);
6059
6195
  printJson(io, response);
6060
6196
  return;
@@ -6115,7 +6251,7 @@ async function runDocsExport(flags, context, cwd) {
6115
6251
  return;
6116
6252
  }
6117
6253
  const partial = `${out}.partial`;
6118
- const fd = (0, import_fs11.openSync)(partial, "w", 384);
6254
+ const fd = (0, import_fs12.openSync)(partial, "w", 384);
6119
6255
  let exported;
6120
6256
  try {
6121
6257
  exported = await exportDocuments(client, {
@@ -6123,26 +6259,26 @@ async function runDocsExport(flags, context, cwd) {
6123
6259
  format,
6124
6260
  batchSize,
6125
6261
  write: (line) => {
6126
- (0, import_fs11.writeSync)(fd, `${line}
6262
+ (0, import_fs12.writeSync)(fd, `${line}
6127
6263
  `);
6128
6264
  },
6129
6265
  onPage: (count, total) => io.stderr(`Exported ${count}${total === null ? "" : `/${total}`} documents`),
6130
6266
  finishOnError: true
6131
6267
  });
6132
- (0, import_fs11.closeSync)(fd);
6268
+ (0, import_fs12.closeSync)(fd);
6133
6269
  } catch (error) {
6134
- (0, import_fs11.closeSync)(fd);
6270
+ (0, import_fs12.closeSync)(fd);
6135
6271
  const kept = error instanceof ExportInterruptedError ? error.exported : 0;
6136
6272
  if (kept > 0) {
6137
6273
  io.stderr(
6138
6274
  `Kept the ${kept} ${kept === 1 ? "document" : "documents"} exported before the error in ${partial}. ${out} was not written.`
6139
6275
  );
6140
6276
  } else {
6141
- (0, import_fs11.rmSync)(partial, { force: true });
6277
+ (0, import_fs12.rmSync)(partial, { force: true });
6142
6278
  }
6143
6279
  throw exportFailure(error instanceof ExportInterruptedError ? error.cause : error, index);
6144
6280
  }
6145
- (0, import_fs11.renameSync)(partial, out);
6281
+ (0, import_fs12.renameSync)(partial, out);
6146
6282
  if (boolFlag(flags, "--json")) printJson(io, { index, out, format, exported });
6147
6283
  else io.stdout(`Exported ${exported} documents to ${out}.`);
6148
6284
  }
@@ -6513,7 +6649,7 @@ async function runSync(args, flags, io, confirm, cwd) {
6513
6649
  printCommandHelp("sync", io);
6514
6650
  throw new UsageError("Unknown sync command");
6515
6651
  }
6516
- var CLI_VERSION = true ? "0.6.1" : "dev";
6652
+ var CLI_VERSION = true ? "0.7.1" : "dev";
6517
6653
  async function run(argv, context = { io: defaultIO }) {
6518
6654
  if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v" || argv[0] === "version")) {
6519
6655
  context.io.stdout(CLI_VERSION);
@@ -6615,7 +6751,7 @@ async function main(argv, context = { io: defaultIO }) {
6615
6751
  function stdoutLineWriter(line) {
6616
6752
  if (process.stdout.write(`${line}
6617
6753
  `)) return;
6618
- return new Promise((resolve2) => process.stdout.once("drain", () => resolve2()));
6754
+ return new Promise((resolve3) => process.stdout.once("drain", () => resolve3()));
6619
6755
  }
6620
6756
  function isDirectRun() {
6621
6757
  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.1",
4
4
  "description": "CLI tool for setting up Headband search in any project",
5
5
  "bin": {
6
6
  "headband": "dist/index.js"