@headband/cli 0.3.0 → 0.6.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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +282 -53
  3. package/dist/index.js +2344 -527
  4. package/package.json +1 -1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ZVN DEV
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @headband/cli
2
2
 
3
3
  Command-line tools for [Headband](https://headband.dev): project setup, login,
4
- API keys, indexes, documents, search, tasks, dedicated engines, schema
4
+ branches, API keys, indexes, documents, search, tasks, dedicated engines, schema
5
5
  migrations and Sync. Every command works for a person at a terminal and for a
6
6
  script or coding agent with no terminal at all.
7
7
 
@@ -26,6 +26,9 @@ npx @headband/cli login
26
26
  --token <key> API key for this call. Wins over HEADBAND_API_KEY and stored logins.
27
27
  --profile <name> Stored login to use (else HEADBAND_PROFILE, else the current profile)
28
28
  --url <url> Headband URL (else HEADBAND_URL, else the profile's, else https://headband.dev)
29
+ --branch <name> Work on this branch for one call, in the project from --project <id>,
30
+ else the checkout's (see Branches). A branch id (br_...) is sent as it is.
31
+ Without --branch or a checkout, calls reach the primary branches.
29
32
  --json Print one JSON document on stdout; errors print one JSON object on stderr
30
33
  --yes, -y Answer yes to confirmations (required when stdin is not a terminal)
31
34
  --help, -h Show help for the CLI or a command
@@ -102,9 +105,10 @@ headband indexes list --json
102
105
  ```
103
106
 
104
107
  Which key to use: an admin key from `headband keys create <name> --type admin`
105
- covers indexes, documents, search, engines and Sync. Managing keys, projects
106
- and schema state needs a key minted by `headband login`, because only those
107
- carry `keys.manage`, `projects.manage` and `schema.manage`.
108
+ covers indexes, documents, search, engines and Sync. Managing keys, projects,
109
+ branches and schema state needs a key minted by `headband login`, because only
110
+ those carry `keys.manage`, `projects.manage`, `branches.manage` and
111
+ `schema.manage`.
108
112
 
109
113
  ## Scripts and agents
110
114
 
@@ -128,6 +132,11 @@ The CLI never waits for input it cannot get.
128
132
  `network_error`. `status` is the HTTP status, or `null` when no request was
129
133
  made.
130
134
 
135
+ - **Rate limits.** A `429` whose `Retry-After` is 10 seconds or less is
136
+ retried up to twice after that wait (CLI 0.6.0). If it still fails, the
137
+ command exits `7` and the message keeps the server's hint, such as
138
+ "Retry after 1s.". A write where some documents were rejected names the
139
+ first rejected document and the engine's reason.
131
140
  - **Branch on exit codes**, not on message text. See the table below.
132
141
 
133
142
  ## Exit codes
@@ -135,7 +144,7 @@ The CLI never waits for input it cannot get.
135
144
  | Code | Meaning | Typical cause |
136
145
  | ----- | ---------------- | --------------------------------------------------------------------------------------------------------------- |
137
146
  | `0` | Success | |
138
- | `1` | Error | Anything not listed below; also `schema diff` when changes are pending |
147
+ | `1` | Error | Anything not listed below; also `schema diff` when changes are pending, `sync reconcile` when drift is left |
139
148
  | `2` | Usage | Unknown command or option, missing or malformed value, HTTP 400/413/422, confirmation needed without a terminal |
140
149
  | `3` | Authentication | No key found, a `--profile` that does not exist, HTTP 401 |
141
150
  | `4` | Permission | HTTP 403, or 402 (the plan does not allow it) |
@@ -163,27 +172,144 @@ uses defaults. Running `headband` with no command runs `init`.
163
172
  ```bash
164
173
  headband projects list [--json]
165
174
  headband projects create <name> --engine <meilisearch|typesense|elasticsearch|visor> [--json]
166
- headband projects delete <id> [--force] [--yes] [--json]
175
+ headband projects delete <id> [--yes] [--json]
167
176
  ```
168
177
 
169
178
  `projects delete` refuses a project that still has indexes (exit `6`, code
170
- `project_has_indexes`). `--force` deletes the project's indexes on its shared
171
- engine along with it. Even with `--force`, a project with an index on a
172
- dedicated engine, or an index being moved to one, is refused: delete or move
173
- those indexes first. Deleting a project also deletes the API keys scoped to it.
179
+ `project_not_empty`). Delete its indexes first. Deleting a project
180
+ also deletes the API keys scoped to it.
181
+
182
+ ### Branches
183
+
184
+ A branch is one environment of a project: its primary branch `main`
185
+ (production), plus others such as `staging` or `preview/pr-123`. Each branch
186
+ has its own indexes and its own keys.
187
+
188
+ ```bash
189
+ headband branch list [--project <id>] [--json]
190
+ headband branch create <name> --empty [--parent <name>] [--expires-in <2h|7d>] [--project <id>] [--json]
191
+ headband branch delete <name> [--project <id>] [--yes] [--json]
192
+ headband branch protect <name> [--project <id>] [--json]
193
+ headband branch unprotect <name> [--project <id>] [--json]
194
+ headband branch expire <name> (--in <2h|7d> | --at <iso> | --clear) [--project <id>] [--json]
195
+ headband branch checkout <name> [--project <id>] [--json]
196
+ ```
197
+
198
+ - The project comes from `--project`, else the directory's checkout (below).
199
+ 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.
207
+ - `delete` removes the branch with its indexes, keys and engines. It asks
208
+ first. The primary branch, a protected branch, a branch with live child
209
+ branches and a branch with Sync-owned indexes are refused (exit `6`).
210
+ - `protect` blocks delete and expiry; `unprotect` lifts it. The primary
211
+ branch (`main`) serves production and is always protected: `unprotect`,
212
+ `expire` and `delete` on it are refused (exit `6`, code `branch_is_primary`).
213
+ - `expire` takes exactly one of `--in <duration>`, `--at <ISO time>` (in the
214
+ future) or `--clear`.
215
+ - Durations are a number and a unit: `s`, `m`, `h`, `d` or `w`, such as `30m`,
216
+ `2h` or `7d`.
217
+ - Creating, deleting, protecting and expiring need the key from
218
+ `headband login`. `branch list` works with any admin key that reaches the
219
+ project.
220
+
221
+ **Working on a branch.** `checkout` makes a directory work on one branch:
222
+
223
+ ```bash
224
+ headband branch checkout staging --project <project id>
225
+ headband indexes list # staging's indexes
226
+ headband docs push products.json --index products
227
+ headband search products shoes # searches staging
228
+ ```
229
+
230
+ `checkout` writes `.headband/project.json`: `{ "orgId", "projectId", "branch" }`,
231
+ with file mode `0600`. It holds no key. Every later command in that directory,
232
+ or in any directory below it, sends `Headband-Branch: <project id>/<branch>`
233
+ with your login key. That key reaches every branch of your organization, so no
234
+ key is minted. Checking out `main` writes the file too. `checkout` needs a
235
+ stored login (`headband login`), because the file records your login's
236
+ organization. `--token` and `HEADBAND_API_KEY` take the place of a stored
237
+ login, so unset them to check out.
238
+
239
+ Like git, the CLI uses the nearest `.headband/project.json` in the working
240
+ directory or a parent, up to your home directory (or the filesystem root,
241
+ outside it). `checkout` rewrites that nearest file, or writes one in the
242
+ working directory when there is none. As in git, a file another user owns, or
243
+ one in a `.headband` directory another user owns, is an error: every command
244
+ that would send `Headband-Branch` stops and names the file, and the search goes
245
+ no further. So a file planted in a shared directory such as `/tmp` never steers
246
+ your commands. A symlink is judged by its own owner, as in git, so another
247
+ user's link to your `.headband` is theirs. `checkout` then writes your own file
248
+ in the working directory, and never rewrites the other user's. When the other
249
+ user's file is in the working directory itself, remove it before you check out
250
+ there.
251
+
252
+ A checkout never falls back to production silently. Every command that would
253
+ send `Headband-Branch` stops with an error when the nearest
254
+ `.headband/project.json` cannot be read, holds a value that is not a valid
255
+ project id, branch name or branch id, or was written for another organization
256
+ than your stored login's. Run `headband branch checkout <name>` again to rewrite
257
+ the file, or delete it to work on production. A token (`--token`,
258
+ `HEADBAND_API_KEY`) names no organization, so the organization check applies
259
+ only to stored logins.
260
+
261
+ For one command, `--project <id> --branch <name>` picks another branch, and
262
+ `--branch <name>` alone takes the checkout's project. A branch id
263
+ (`--branch br_...`) names its own project, so it is sent as it is and needs no
264
+ `--project`. With no checkout and no `--branch`, commands reach each project's
265
+ primary branch, as before. A branch name with no project at all is a usage
266
+ error, so a name is never matched across projects by accident.
267
+
268
+ `--project` without `--branch` is a usage error, because these commands send no
269
+ project of their own and would ignore it. The exception is `keys create`, which
270
+ sends the project in its body (see Keys).
271
+
272
+ Coming with bucket storage: `branch create` without `--empty` (a copy of its
273
+ parent), `reset`, `rollback`, `restore` and `set-primary`.
174
274
 
175
275
  ### Keys
176
276
 
177
277
  ```bash
178
278
  headband keys list [--json]
179
- headband keys create <name> --type <admin|search> [--project <id>]
180
- [--permissions <a,b,...>] [--expires <30d|ISO date>] [--json]
279
+ headband keys create <name> --type <admin|search> [--project <id>] [--branch <name>]
280
+ [--instances <id,id>] [--permissions <a,b,...>] [--expires <30d|ISO date>] [--json]
181
281
  headband keys revoke <id> [--yes] [--json]
182
282
  ```
183
283
 
284
+ A key belongs to one branch and reaches only that branch's indexes, for an app
285
+ such as a preview deployment. `keys create` picks the branch in this order:
286
+
287
+ 1. `--branch <name>`, in the project from `--project`, else the checkout's. A
288
+ branch id (`--branch br_...`) needs no project.
289
+ 2. The checked-out branch, unless `--project` names another project.
290
+ 3. The primary branch of `--project`.
291
+
292
+ With none of these, `--type admin` mints an org key, which reaches every branch
293
+ of every project, and `--type search` is refused (exit `2`, code
294
+ `branch_required`), because search keys ship in browsers. A key on the primary
295
+ branch is a live key (`hb_adm_live_`, `hb_src_live_`); a key on any other
296
+ branch is a test key (`hb_adm_test_`, `hb_src_test_`). `keys create` and
297
+ `keys list` show each key's branch and environment.
298
+
184
299
  `keys create` is the only command that prints a raw API key. Store it when it is
185
300
  shown: Headband returns raw keys only once.
186
301
 
302
+ `--instances <id,id>` also limits the key to those dedicated engines. It takes
303
+ one comma-separated list of 1 to 50 ids (from `headband instances list`);
304
+ repeating the flag does not add engines, only the last one counts. An org key then reaches only them, on every
305
+ branch. A branch key reaches its branch's engines among them, and every id must
306
+ sit on that branch. Any other index answers as if it did not exist, and
307
+ endpoints that are not about those engines refuse the key. The list is fixed:
308
+ to change it, create a new key and revoke the old one. `keys list` and
309
+ `keys create` show each key's engines: `all` when it is not limited, the engine
310
+ ids when it is, `none` when its list reaches nothing, and `unknown` when the
311
+ API did not report them (never shown as `all`).
312
+
187
313
  `--permissions` limits the new key to the permissions you list. You can grant
188
314
  only permissions your own key holds, and only ones that fit the key type:
189
315
 
@@ -194,8 +320,8 @@ only permissions your own key holds, and only ones that fit the key type:
194
320
 
195
321
  Anything else is refused with exit `4` and the names it refused. Without
196
322
  `--permissions`, the key gets the type's defaults, cut down to what your key
197
- holds. `keys.manage`, `projects.manage` and `schema.manage` are never
198
- grantable here; only `headband login` mints them.
323
+ holds. `keys.manage`, `projects.manage`, `branches.manage` and
324
+ `schema.manage` are never grantable here; only `headband login` mints them.
199
325
 
200
326
  `--expires` sets when the key stops working: a duration from now (`12h`,
201
327
  `30d`, `2w`, `1y`) or an ISO date (`2027-01-31`). It must be in the future and
@@ -216,8 +342,18 @@ headband indexes settings set <uid> --file <settings.json>
216
342
  headband indexes stats <uid>
217
343
  ```
218
344
 
345
+ `index` is an alias of `indexes` for every command here: `headband index list`
346
+ and `headband index switch-engine <uid>` work the same.
347
+
219
348
  `settings get`, `settings set` and `stats` always print JSON.
220
349
 
350
+ `indexes create` prints the project and branch the index went to (CLI 0.6.0).
351
+ With the key from `headband login` and no `--project`, `--branch` or
352
+ checkout, that is the org's Default project, main branch.
353
+
354
+ Deleting an index that a `schema apply` reindex moved to `<uid>__v2` also
355
+ removes that build's own index row, so nothing is left behind.
356
+
221
357
  `--instance <id>` creates the index on one of your dedicated engines (ids from
222
358
  `headband instances list`). The index takes that engine's type, so `--engine` is
223
359
  optional; if you pass it, it must match.
@@ -226,18 +362,43 @@ On the engine, the index has a longer name (`engineIndexUid`, such as
226
362
  `hbo_<org>_products`). `indexes create` and `indexes list` show it. Use that name,
227
363
  not the uid, with the engine's own client on the instance endpoint.
228
364
 
365
+ #### Change an index's engine
366
+
367
+ ```bash
368
+ headband indexes switch-engine <uid> --engine <engine> [--instance <id>] [--accept-drops] [--declare-key] [--dry-run] [--wait] [--json]
369
+ headband indexes switch-engine <uid> --status [--json]
370
+ ```
371
+
372
+ Moves the index to another engine. The
373
+ uid, keys and URLs stay the same, and searches keep working until the switch.
374
+ Writes are held for about a minute at the end (they answer 503 with `Retry-After`).
375
+
376
+ - `--dry-run` shows how many documents would be copied, where the new index goes,
377
+ and which settings would not carry over. It changes nothing.
378
+ - A setting the new engine cannot apply stops the switch with exit code 6 and a
379
+ list of what would not carry over, with a reason for each. `--accept-drops`
380
+ switches anyway.
381
+ - `--wait` follows the switch until it ends and prints each phase. Without it the
382
+ command returns once the switch is queued; `--status` shows how it went.
383
+ - `--declare-key` is for a Visor index over 1000 documents whose primary key is
384
+ not filterable and sortable: the key is declared while the index is read, which
385
+ rebuilds it.
386
+
229
387
  ### Documents
230
388
 
231
389
  ```bash
232
390
  headband docs push <file.json|file.jsonl> --index <uid> [--json]
233
391
  headband docs export --index <uid> [--out <file>] [--format <jsonl|json>] [--batch <n>] [--json]
234
- headband docs delete <docId> --index <uid> [--json]
392
+ headband docs delete <docId>... --index <uid> [--json]
235
393
  ```
236
394
 
395
+ `docs delete` removes one or more documents by id, in one call to
396
+ `DELETE /v1/documents?index=<uid>`.
397
+
237
398
  `docs push` accepts a JSON array or a JSONL file and sends documents to
238
399
  `/v1/bulk` in batches of 1000.
239
400
 
240
- `docs export` pages through `GET /v1/documents` and writes every document in
401
+ `docs export` walks `GET /v1/documents` by cursor and writes every document in
241
402
  the index, one page in memory at a time:
242
403
 
243
404
  ```bash
@@ -251,37 +412,62 @@ headband docs export --index products | head -5
251
412
  - `--format` is `jsonl` (one document per line, the default) or `json` (one
252
413
  array). With `--out`, a `.json` file name picks `json`.
253
414
  - `--out` writes to `<file>.partial` and renames it when the export finishes,
254
- so a failed export never leaves a truncated file under the real name.
255
- `--json` with `--out` prints a summary: `{index, out, format, exported}`.
415
+ so a failed export never leaves a truncated file under the real name. If the
416
+ export stops on an error after some documents were written (CLI 0.6.0), they
417
+ stay in `<file>.partial`, as a whole JSONL file or a closed JSON array, and
418
+ stderr says how many. `--json` with `--out` prints a summary:
419
+ `{index, out, format, exported}`.
256
420
  - `--batch` is the page size. The default is 1000, or 250 for an index on
257
- Typesense, which refuses larger pages.
258
- - Elasticsearch serves at most the first 10,000 documents by paging, so a
259
- larger Elasticsearch index fails partway with exit `8`.
421
+ Typesense, which refuses larger pages. The server may answer fewer a page.
422
+ - The cursor reaches every document on every engine: Visor past its first
423
+ 1000 and Elasticsearch past its first 10,000. A server without cursors is
424
+ paged by offset, as before CLI 0.5.0.
425
+ - A Visor index created before 2026-10-21 with more than 1000 documents may
426
+ not have its primary key declared filterable and sortable. The server then
427
+ refuses the walk (exit `2`, code `feature_not_supported`), and the CLI prints
428
+ the fix: `headband indexes settings get <uid> > settings.json`, add the key
429
+ to the lists it names, `headband indexes settings set <uid> --file
430
+ settings.json` (Visor rebuilds the index), then export again. New Visor
431
+ indexes declare the key on their own.
260
432
  - Export needs an admin key with `documents.read`.
261
433
 
262
434
  ### Search
263
435
 
264
436
  ```bash
265
- headband search <index> <query> [--limit N] [--filter <expr>] [--json]
437
+ headband search <index> [<query>] [--limit N] [--filter <expr>] [--mode <instant|retrieval>] [--json]
266
438
  ```
267
439
 
440
+ Without a query, or with `""`, search browses the index (CLI 0.6.0): every
441
+ document matches, and `--filter` and `--limit` pick what comes back.
442
+
443
+ `--mode` picks Visor's ranking: `instant` (the default, typo-tolerant,
444
+ as-you-type) or `retrieval` (BM25F, for longer queries). Indexes on the other
445
+ engines ignore it.
446
+
268
447
  Plain output shows a compact table. `--json` prints the raw API response.
269
448
 
270
449
  ### Tasks
271
450
 
272
451
  ```bash
273
- headband tasks list [--json]
274
- headband tasks get <id>
452
+ headband tasks list [--index <uid>] [--limit <n>] [--json]
453
+ headband tasks get <id> [--index <uid>] [--json]
275
454
  ```
276
455
 
277
456
  `tasks list` prints a table, or the raw response with `--json`. `tasks get`
278
457
  prints JSON.
279
458
 
459
+ Tasks live on the engine that holds the index. The key `headband login`
460
+ creates is org-wide, so it must name the index with `--index`. So must a key
461
+ of a branch other than production. A project key of the production branch may
462
+ leave it out.
463
+
280
464
  ### Engines
281
465
 
282
- Dedicated engines are private Meilisearch, Elasticsearch or Typesense servers for
283
- your organization, reachable with each engine's own client. They are rolling out
284
- gradually. Until your organization has access, these commands say so.
466
+ Dedicated engines are private Meilisearch, Elasticsearch, Typesense or Visor
467
+ servers for your organization, reachable with each engine's own client (Visor over
468
+ its HTTP API). They are rolling out gradually. Until your organization has access,
469
+ these commands say so. Visor is offered in a region where a dedicated engine host
470
+ runs it; elsewhere `instances create visor` answers `engine_not_available`.
285
471
 
286
472
  ```bash
287
473
  headband instances list [--json]
@@ -308,23 +494,29 @@ woke, the outcome and how long it took to be ready. `--limit` takes up to 200;
308
494
  the default is 50.
309
495
 
310
496
  `connect` prints the endpoint and a snippet for the engine's own client.
311
- Authenticate with an organization-wide key. The snippets create an index and add
497
+ Authenticate with an organization-wide key, a branch key of the engine's branch, or a key limited to that engine. The snippets create an index and add
312
498
  a document, so run them with an admin key (`headband keys create <name> --type
313
- admin`); a search key can only search. Project keys do not work on engine
314
- endpoints. Serverless engines are available for Meilisearch, and for Typesense
499
+ admin`); a search key can only search. A key of another branch does not work on the engine. Serverless engines are available for Meilisearch, and for Typesense
315
500
  in early access; Elasticsearch runs always on. If a command says the key cannot
316
501
  manage engines, run `headband login` again.
317
502
 
318
503
  ### Schema migrations
319
504
 
320
505
  For projects that define their indexes in `headband.config.ts` with
321
- `@headband/sync`. These commands read that file from the current directory.
506
+ `@headband/sync`. These commands read that file from the current directory:
507
+ its default export is `defineConfig({ url, projectId?, indexes })`, and `apply`
508
+ also needs a named `adapter` export, such as `drizzleAdapter(...)` from
509
+ `@headband/drizzle`, in the same file. When the adapter is exported, the
510
+ config is checked against your tables first, and every misspelled table,
511
+ field, relation or settings attribute is listed with its index.
512
+ `headband schema <command> --help` describes each command.
322
513
 
323
514
  ```bash
324
515
  headband schema diff [--allow-destructive] [--json]
325
516
  headband schema generate [--slug <slug>] [--dir <dir>] [--allow-destructive] [--json]
326
- headband schema apply [--allow-destructive] [--dir <dir>] [--yes] [--json]
517
+ headband schema apply [--project <id>] [--allow-destructive] [--dir <dir>] [--yes] [--json]
327
518
  headband schema status [--dir <dir>] [--json]
519
+ headband schema forget <index> [--yes] [--json]
328
520
  ```
329
521
 
330
522
  - `diff` compares the config with the schema state on the server. Like
@@ -333,15 +525,30 @@ headband schema status [--dir <dir>] [--json]
333
525
  - `generate` writes a migration file for the pending changes into `--dir`
334
526
  (default `headband/migrations`). `--json` prints `{path}`, with `null` when
335
527
  there was nothing to generate.
336
- - `apply` runs the migration files not yet applied, in order, backfilling
337
- documents through your config's `adapter` when a change needs a reindex.
338
- Removing an index needs `--allow-destructive`. `--json` prints
339
- `{appliedFiles, appliedOps}`.
340
- - `status` shows each index's applied version and any drift. `--json` prints
528
+ - `apply` runs the migration files the server does not have yet, in order. A
529
+ new index is created and backfilled from every source row (rows
530
+ `shouldIndex` rejects are left out). A schema change builds a new index
531
+ beside the live one, backfills it and switches the name over. Removing an
532
+ index needs `--allow-destructive`. `--json` prints
533
+ `{appliedFiles, appliedOps, projectId}`.
534
+ - `apply` recognises migrations the server already has from its schema state,
535
+ so a fresh checkout or CI applies without `headband/migrations/.applied.json`.
536
+ That file is only a local cache: commit it or ignore it.
537
+ - `status` shows each index's applied version and any drift, including
538
+ `index missing` for an index deleted outside `apply`. `--json` prints
341
539
  `{indexes, localApplied, localHead}`.
540
+ - `forget` removes the recorded schema state of an index deleted outside
541
+ `apply`, so the next `generate` plans it as new. `headband indexes delete`
542
+ clears it for you.
342
543
 
343
544
  Schema commands need a key with `schema.manage`, which only `headband login`
344
- mints.
545
+ mints. That key is org-wide: new indexes go to the project named by
546
+ `--project <id>` or the config's `projectId`, else to your org's Default
547
+ project. Your app's own project admin key reaches only its project's indexes,
548
+ so name that project.
549
+
550
+ The URL comes from `--url`, else `HEADBAND_URL`, else the config's `url`, else
551
+ your login's.
345
552
 
346
553
  ### Sync
347
554
 
@@ -382,35 +589,57 @@ Headband's setup SQL on your machine as the tables' owner, in one transaction;
382
589
  the owner URL never leaves your machine. Run `headband sync source --help` and
383
590
  `headband sync index --help` for the details.
384
591
 
592
+ Managed Sync is in early access: until your organization has it, these
593
+ commands say "Sync is not enabled for this organization yet."
594
+
385
595
  **Self-hosted Sync** runs a worker in your own infrastructure. Your app writes
386
- changes to an outbox table, and the worker pushes them to Headband.
596
+ changes to an outbox table, and the worker pushes them to Headband. The
597
+ `@headband/drizzle` README walks through the whole setup.
387
598
 
388
599
  ```bash
389
- headband sync init [--table <name>] [--write] [--json]
390
- headband sync worker [--loop] [--budget-ms <ms>] [--batch-size <n>] [--reconcile-every <n>] [--json]
600
+ headband sync init [--table <name>] [--write] [--apply] [--json]
601
+ headband sync worker [--loop] [--budget-ms <ms>] [--batch-size <n>] [--reconcile-every <n>] [--project <id>] [--json]
391
602
  headband sync worker --scaffold docker [--force] [--json]
392
603
  headband sync reconcile [--fix] [--json]
393
604
  headband sync retry-dead-letters [--json]
605
+ headband sync status [--json]
394
606
  ```
395
607
 
396
608
  - `init` prints the outbox table's DDL; `--write` also saves it to
397
- `headband/outbox.sql`. `--json` prints `{ddl, path}`.
609
+ `headband/outbox.sql`, and `--apply` runs it against `DATABASE_URL` (it is
610
+ `CREATE TABLE IF NOT EXISTS`). `--json` prints `{ddl, path, applied}`.
398
611
  - `worker` drains the outbox. Without `--budget-ms` it loops until stopped
399
- (Ctrl+C or SIGTERM finish the current batch first). With `--budget-ms` it
400
- stops after that long; `--json` then prints the run report. In a loop,
612
+ (Ctrl+C or SIGTERM finish the current batch first), so `--loop` changes
613
+ nothing on its own. With `--budget-ms` it stops after that long, or earlier
614
+ once the outbox is empty; add `--loop` to keep waiting for changes until the
615
+ budget ends. `--json` then prints the run report. Each batch that drained
616
+ rows prints one line to stderr (CLI 0.6.0). In a loop,
401
617
  `--json` sends progress to stderr and prints the report when the worker
402
- stops.
403
- - `worker --scaffold docker` writes `Dockerfile.headband-sync` and prints a
404
- compose service for it.
405
- - `reconcile` compares each index's document count with its source table and
406
- reports drift; `--fix` resends a drifted index in full. `--json` prints
407
- `{indexes}`.
618
+ stops. A row that fails is printed with its model, ids and error; a batch
619
+ that fails is retried row by row, so one bad row never fails the others.
620
+ Rows that keep failing move to the dead letters. Each invocation is recorded
621
+ as one run under the connector `orm-sync` (`GET /v1/connectors/runs`); with
622
+ the org-wide login key that needs `--project <id>` or the config's
623
+ `projectId`. `--reconcile-every <n>` runs `reconcile` after every n-th batch.
624
+ - `worker --scaffold docker` writes `Dockerfile.headband-sync`, which runs
625
+ `npx --yes @headband/cli@<version> sync worker --loop`, and prints a compose
626
+ service for it. The container needs `DATABASE_URL` and `HEADBAND_API_KEY`.
627
+ - `reconcile` compares the source rows that belong in each index (every row
628
+ `shouldIndex` keeps) with the documents in it, and reports how many are
629
+ missing and how many are extra (counts, not ids). `--fix` pushes the missing
630
+ and deletes the extra. It exits
631
+ `1` when drift is left. `--json` prints `{indexes, drift}`. The index's ids
632
+ are read by cursor, so a Visor index past 1000 documents is compared id by
633
+ id too.
408
634
  - `retry-dead-letters` requeues rows that failed too many times. `--json`
409
635
  prints `{requeued}`.
636
+ - `status`, beside a `headband.config.ts`, shows the outbox's pending rows,
637
+ dead letters and oldest pending row. Elsewhere it shows managed Sync.
410
638
 
411
- The worker and `retry-dead-letters` read the outbox database URL from
412
- `DATABASE_URL`, falling back to `HEADBAND_DATABASE_URL`. The worker and
413
- `reconcile` need `headband.config.ts` to export an `adapter`.
639
+ The worker, `retry-dead-letters`, `status` and `init --apply` read the outbox
640
+ database URL from `DATABASE_URL`, falling back to `HEADBAND_DATABASE_URL`. The
641
+ worker and `reconcile` need `headband.config.ts` to export an `adapter`. A
642
+ missing outbox table is reported by name, with how to create it.
414
643
 
415
644
  ## What `init` creates
416
645