@headband/cli 0.2.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 +566 -70
  3. package/dist/index.js +5250 -1698
  4. package/package.json +3 -3
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # @headband/cli
2
2
 
3
- Command-line tools for [Headband](https://headband.dev): project setup, device login,
4
- API key management, index operations, document pushes, search, and task inspection.
5
-
6
- ## Usage
3
+ Command-line tools for [Headband](https://headband.dev): project setup, login,
4
+ branches, API keys, indexes, documents, search, tasks, dedicated engines, schema
5
+ migrations and Sync. Every command works for a person at a terminal and for a
6
+ script or coding agent with no terminal at all.
7
7
 
8
8
  ```bash
9
9
  npx @headband/cli --help
@@ -11,85 +11,349 @@ npx @headband/cli init
11
11
  npx @headband/cli login
12
12
  ```
13
13
 
14
- Global flags:
14
+ ## Contents
15
+
16
+ - [Global options](#global-options)
17
+ - [Authentication](#authentication)
18
+ - [Scripts and agents](#scripts-and-agents)
19
+ - [Exit codes](#exit-codes)
20
+ - [Command reference](#command-reference)
21
+ - [What `init` creates](#what-init-creates)
22
+
23
+ ## Global options
24
+
25
+ ```text
26
+ --token <key> API key for this call. Wins over HEADBAND_API_KEY and stored logins.
27
+ --profile <name> Stored login to use (else HEADBAND_PROFILE, else the current profile)
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.
32
+ --json Print one JSON document on stdout; errors print one JSON object on stderr
33
+ --yes, -y Answer yes to confirmations (required when stdin is not a terminal)
34
+ --help, -h Show help for the CLI or a command
35
+ ```
36
+
37
+ ## Authentication
38
+
39
+ ### Interactive login
15
40
 
16
41
  ```bash
17
- --url <url> Select a Headband profile URL
18
- --json Print JSON for supported list/get commands
19
- --yes Skip confirmation prompts
20
- --help Show help
42
+ headband login [--url <url>] [--profile <name>] [--json]
43
+ headband logout [--profile <name>] [--yes] [--no-revoke] [--json]
44
+ headband whoami [--json]
45
+ headband profiles list [--json]
46
+ headband profiles use <name>
21
47
  ```
22
48
 
23
- Credentials are stored in `~/.config/headband/credentials.json` or
24
- `$XDG_CONFIG_HOME/headband/credentials.json` with file mode `0600`.
25
-
26
- ## Commands
49
+ `login` runs Headband's device flow: it prints a code and a link, opens your
50
+ browser, and waits while you approve it in the dashboard. It saves the key it
51
+ receives as a named profile and makes that profile current. The name defaults
52
+ to `default` for headband.dev and to the host name for any other URL, such as
53
+ `localhost-3000`. The full key is never printed; output shows only its prefix.
27
54
 
28
- ### Setup
55
+ Log in to several Headband URLs or organizations and switch between them:
29
56
 
30
57
  ```bash
31
- headband init [--yes]
58
+ headband login --profile prod
59
+ headband login --url http://localhost:3000 --profile local
60
+ headband profiles list
61
+ headband profiles use prod
62
+ headband indexes list --profile local # one command, without switching
32
63
  ```
33
64
 
34
- This will:
65
+ `logout` revokes the profile's key on the server, then removes it from this
66
+ machine. It asks first at a terminal. `--yes` revokes without asking.
67
+ `--no-revoke` removes the local copy and leaves the key working. If the key is
68
+ already gone on the server, logout still succeeds; any other failure keeps the
69
+ local login so you can retry.
70
+
71
+ Credentials live in `~/.config/headband/credentials.json`, or
72
+ `$XDG_CONFIG_HOME/headband/credentials.json`, with file mode `0600`. A file
73
+ written by CLI 0.2.0 or earlier (profiles keyed by URL) still works: its
74
+ profiles get names on first read, and the file is saved in the new format the
75
+ next time the CLI writes it.
76
+
77
+ ### Tokens and environment variables
78
+
79
+ A script, CI job or agent does not need to log in. Give it a key:
35
80
 
36
- 1. Ask for your Headband URL (default: `https://headband.dev`)
37
- 2. Ask for your browser-safe search API key (`hb_src_...` only)
38
- 3. Ask for your default search index (e.g. `products`, `movies`)
39
- 4. Detect your package manager (npm, pnpm, yarn, or bun)
40
- 5. Install `@headband/react`
41
- 6. Create `headband.config.ts` with your settings
42
- 7. Create an example `<HeadbandSearch />` component
81
+ | Variable | Flag | Meaning |
82
+ | ------------------ | ----------- | ------------------------------------------------------ |
83
+ | `HEADBAND_API_KEY` | `--token` | The API key to send. Used instead of any stored login. |
84
+ | `HEADBAND_URL` | `--url` | The Headband URL. |
85
+ | `HEADBAND_PROFILE` | `--profile` | Which stored login to use. |
43
86
 
44
- ## Options
87
+ Precedence, highest first:
88
+
89
+ - **Key:** `--token`, then `HEADBAND_API_KEY`, then the stored profile.
90
+ - **URL:** `--url`, then `HEADBAND_URL`, then the profile's URL, then
91
+ `https://headband.dev`. With a token, the profile's URL counts only when you
92
+ name the profile with `--profile`.
93
+ - **Profile:** `--profile`, then `HEADBAND_PROFILE`, then the current profile,
94
+ then the only stored profile when there is one.
95
+
96
+ A stored key is only ever sent to the URL it was issued for. `--url` with no
97
+ `--profile` picks the stored profile for that URL; `--url` with a `--profile`
98
+ for a different URL is a usage error. A `--profile` name that does not exist is
99
+ an error, never a silent fallback.
45
100
 
46
101
  ```bash
47
- npx @headband/cli init --yes # Skip prompts, use defaults
48
- npx @headband/cli --help # Show help
102
+ export HEADBAND_API_KEY=hb_adm_...
103
+ export HEADBAND_URL=https://headband.dev
104
+ headband indexes list --json
49
105
  ```
50
106
 
51
- ### Auth
107
+ Which key to use: an admin key from `headband keys create <name> --type admin`
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`.
112
+
113
+ ## Scripts and agents
114
+
115
+ The CLI never waits for input it cannot get.
116
+
117
+ - **No hanging prompts.** A command that would ask for confirmation fails at
118
+ once with exit code `2` and code `confirmation_required` when stdin is not a
119
+ terminal. Pass `--yes` to confirm. `headband init` does the same without
120
+ `--yes`.
121
+ - **One JSON document.** With `--json`, stdout carries exactly one JSON
122
+ document and progress lines go to stderr, so `headband ... --json | jq` is
123
+ safe. `docs export` is the exception by design: its output is the documents.
124
+ - **JSON errors.** With `--json`, a failure prints one line to stderr:
125
+
126
+ ```json
127
+ { "error": "Index not found", "code": "index_not_found", "status": 404, "exitCode": 5 }
128
+ ```
129
+
130
+ `code` is the API's error code when there is one, else a CLI code such as
131
+ `usage_error`, `not_authenticated`, `confirmation_required` or
132
+ `network_error`. `status` is the HTTP status, or `null` when no request was
133
+ made.
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.
140
+ - **Branch on exit codes**, not on message text. See the table below.
141
+
142
+ ## Exit codes
143
+
144
+ | Code | Meaning | Typical cause |
145
+ | ----- | ---------------- | --------------------------------------------------------------------------------------------------------------- |
146
+ | `0` | Success | |
147
+ | `1` | Error | Anything not listed below; also `schema diff` when changes are pending, `sync reconcile` when drift is left |
148
+ | `2` | Usage | Unknown command or option, missing or malformed value, HTTP 400/413/422, confirmation needed without a terminal |
149
+ | `3` | Authentication | No key found, a `--profile` that does not exist, HTTP 401 |
150
+ | `4` | Permission | HTTP 403, or 402 (the plan does not allow it) |
151
+ | `5` | Not found | HTTP 404 or 410, `profiles use` with an unknown name |
152
+ | `6` | Conflict | HTTP 409, such as deleting a project that still has indexes |
153
+ | `7` | Rate limited | HTTP 429 |
154
+ | `8` | Server or engine | HTTP 5xx, or the URL could not be reached |
155
+ | `130` | Interrupted | Ctrl+C |
156
+
157
+ ## Command reference
158
+
159
+ ### Setup
52
160
 
53
161
  ```bash
54
- headband login [--url <url>]
55
- headband logout [--url <url>] [--yes]
56
- headband whoami [--url <url>] [--json]
162
+ headband init [--yes]
57
163
  ```
58
164
 
59
- `login` uses Headband's device flow and stores only the API key profile locally.
60
- The full key is never printed during login; success output shows only the key
61
- prefix. `logout` removes local credentials and can offer to revoke the CLI key
62
- server-side first.
165
+ Asks for your Headband URL, a browser-safe search key (`hb_src_...` only) and
166
+ a default index, installs `@headband/react`, and writes `headband.config.ts`
167
+ plus an example `<HeadbandSearch />` component. `--yes` skips the questions and
168
+ uses defaults. Running `headband` with no command runs `init`.
63
169
 
64
170
  ### Projects
65
171
 
66
172
  ```bash
67
173
  headband projects list [--json]
68
- headband projects create <name> --engine <meilisearch|typesense|elasticsearch>
174
+ headband projects create <name> --engine <meilisearch|typesense|elasticsearch|visor> [--json]
175
+ headband projects delete <id> [--yes] [--json]
176
+ ```
177
+
178
+ `projects delete` refuses a project that still has indexes (exit `6`, code
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
69
228
  ```
70
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`.
274
+
71
275
  ### Keys
72
276
 
73
277
  ```bash
74
278
  headband keys list [--json]
75
- headband keys create <name> --type <admin|search> [--project <id>] [--json]
76
- headband keys revoke <id> [--yes]
279
+ headband keys create <name> --type <admin|search> [--project <id>] [--branch <name>]
280
+ [--instances <id,id>] [--permissions <a,b,...>] [--expires <30d|ISO date>] [--json]
281
+ headband keys revoke <id> [--yes] [--json]
77
282
  ```
78
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
+
79
299
  `keys create` is the only command that prints a raw API key. Store it when it is
80
- shown because Headband returns raw keys only once.
300
+ shown: Headband returns raw keys only once.
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
+
313
+ `--permissions` limits the new key to the permissions you list. You can grant
314
+ only permissions your own key holds, and only ones that fit the key type:
315
+
316
+ | Type | Grantable permissions |
317
+ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
318
+ | `search` | `search`, `analytics.write` |
319
+ | `admin` | `search`, `documents.read`, `documents.add`, `documents.delete`, `indexes.create`, `indexes.delete`, `settings.update`, `analytics.write`, `connectors.write`, `instances.manage`, `sync.manage` |
320
+
321
+ Anything else is refused with exit `4` and the names it refused. Without
322
+ `--permissions`, the key gets the type's defaults, cut down to what your key
323
+ holds. `keys.manage`, `projects.manage`, `branches.manage` and
324
+ `schema.manage` are never grantable here; only `headband login` mints them.
325
+
326
+ `--expires` sets when the key stops working: a duration from now (`12h`,
327
+ `30d`, `2w`, `1y`) or an ISO date (`2027-01-31`). It must be in the future and
328
+ at most one year away. Without it, the key does not expire.
329
+
330
+ ```bash
331
+ headband keys create ci-reader --type admin --permissions search,documents.read --expires 30d --json
332
+ ```
81
333
 
82
334
  ### Indexes
83
335
 
84
336
  ```bash
85
337
  headband indexes list [--json]
86
338
  headband indexes create <uid> [--primary-key <pk>] [--engine <engine>] [--instance <id>] [--json]
87
- headband indexes delete <uid> [--yes]
88
- headband indexes settings get <uid> [--json]
89
- headband indexes settings set <uid> --file <settings.json> [--json]
90
- headband indexes stats <uid> [--json]
339
+ headband indexes delete <uid> [--yes] [--json]
340
+ headband indexes settings get <uid>
341
+ headband indexes settings set <uid> --file <settings.json>
342
+ headband indexes stats <uid>
91
343
  ```
92
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
+
348
+ `settings get`, `settings set` and `stats` always print JSON.
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
+
93
357
  `--instance <id>` creates the index on one of your dedicated engines (ids from
94
358
  `headband instances list`). The index takes that engine's type, so `--engine` is
95
359
  optional; if you pass it, it must match.
@@ -98,58 +362,289 @@ On the engine, the index has a longer name (`engineIndexUid`, such as
98
362
  `hbo_<org>_products`). `indexes create` and `indexes list` show it. Use that name,
99
363
  not the uid, with the engine's own client on the instance endpoint.
100
364
 
101
- ### Engines
102
-
103
- Dedicated engines are private Meilisearch, Elasticsearch or Typesense servers for
104
- your organization, reachable with each engine's own client. They are rolling out
105
- gradually. Until your organization has access, these commands say so.
365
+ #### Change an index's engine
106
366
 
107
367
  ```bash
108
- headband instances list [--json]
109
- headband instances create <meilisearch|elasticsearch|typesense> [--name <name>] [--lane <serverless|always_on>] [--memory <512m|1g|...>] [--region <region>] [--idle-timeout <seconds>] [--json]
110
- headband instances show <id> [--json]
111
- headband instances wake <id> [--json]
112
- headband instances park <id> [--json]
113
- headband instances delete <id> [--yes]
114
- headband instances connect <id> [--client <node|curl>]
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]
115
370
  ```
116
371
 
117
- `connect` prints the endpoint and a snippet for the engine's own client. Authenticate
118
- with an organization-wide key. The snippets create an index and add a document, so
119
- run them with an admin key (`headband keys create <name> --type admin`); a search
120
- key can only search.
121
- Project keys do not work on engine endpoints. Serverless engines are available for
122
- Meilisearch only for now. If a command says the key cannot manage engines, run
123
- `headband login` again.
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.
124
386
 
125
387
  ### Documents
126
388
 
127
389
  ```bash
128
- headband docs push <file.json|file.jsonl> --index <uid>
129
- headband docs delete <docId> --index <uid>
390
+ headband docs push <file.json|file.jsonl> --index <uid> [--json]
391
+ headband docs export --index <uid> [--out <file>] [--format <jsonl|json>] [--batch <n>] [--json]
392
+ headband docs delete <docId>... --index <uid> [--json]
130
393
  ```
131
394
 
132
- `docs push` accepts a JSON array or JSONL file and sends documents to
395
+ `docs delete` removes one or more documents by id, in one call to
396
+ `DELETE /v1/documents?index=<uid>`.
397
+
398
+ `docs push` accepts a JSON array or a JSONL file and sends documents to
133
399
  `/v1/bulk` in batches of 1000.
134
400
 
401
+ `docs export` walks `GET /v1/documents` by cursor and writes every document in
402
+ the index, one page in memory at a time:
403
+
404
+ ```bash
405
+ headband docs export --index products > products.jsonl
406
+ headband docs export --index products --out products.json
407
+ headband docs export --index products | head -5
408
+ ```
409
+
410
+ - Without `--out`, documents stream to stdout and nothing else is printed
411
+ there. Progress for `--out` goes to stderr.
412
+ - `--format` is `jsonl` (one document per line, the default) or `json` (one
413
+ array). With `--out`, a `.json` file name picks `json`.
414
+ - `--out` writes to `<file>.partial` and renames it when the export finishes,
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}`.
420
+ - `--batch` is the page size. The default is 1000, or 250 for an index on
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.
432
+ - Export needs an admin key with `documents.read`.
433
+
135
434
  ### Search
136
435
 
137
436
  ```bash
138
- headband search <index> <query> [--limit N] [--filter <expr>] [--json]
437
+ headband search <index> [<query>] [--limit N] [--filter <expr>] [--mode <instant|retrieval>] [--json]
139
438
  ```
140
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
+
141
447
  Plain output shows a compact table. `--json` prints the raw API response.
142
448
 
143
449
  ### Tasks
144
450
 
145
451
  ```bash
146
- headband tasks list [--json]
147
- headband tasks get <id> [--json]
452
+ headband tasks list [--index <uid>] [--limit <n>] [--json]
453
+ headband tasks get <id> [--index <uid>] [--json]
148
454
  ```
149
455
 
150
- ## What gets created
456
+ `tasks list` prints a table, or the raw response with `--json`. `tasks get`
457
+ prints JSON.
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
+
464
+ ### Engines
465
+
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`.
471
+
472
+ ```bash
473
+ headband instances list [--json]
474
+ headband instances create <meilisearch|elasticsearch|typesense|visor> [--name <name>]
475
+ [--lane <serverless|always_on>] [--memory <512m|1g|...>]
476
+ [--region <region>] [--idle-timeout <seconds>] [--json]
477
+ headband instances show <id> [--json]
478
+ headband instances update <id> [--name <name>] [--lane <serverless|always_on>]
479
+ [--memory <512m|1g|...>] [--idle-timeout <seconds>] [--json]
480
+ headband instances wake <id> [--json]
481
+ headband instances park <id> [--json]
482
+ headband instances wakes <id> [--limit <n>] [--json]
483
+ headband instances delete <id> [--yes] [--json]
484
+ headband instances connect <id> [--client <node|curl>] [--json]
485
+ ```
486
+
487
+ `update` changes only the settings you pass; `--size` is an alias for
488
+ `--memory`. The engine and region cannot change. A resize applies in the
489
+ background: `instances show` reports the state as "applying changes" until it
490
+ is done. Always-on engines need a paid plan (exit `4` otherwise).
491
+
492
+ `wakes` lists recent wake-ups, newest first: when each was requested, how it
493
+ woke, the outcome and how long it took to be ready. `--limit` takes up to 200;
494
+ the default is 50.
495
+
496
+ `connect` prints the endpoint and a snippet for the engine's own client.
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
498
+ a document, so run them with an admin key (`headband keys create <name> --type
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
500
+ in early access; Elasticsearch runs always on. If a command says the key cannot
501
+ manage engines, run `headband login` again.
502
+
503
+ ### Schema migrations
504
+
505
+ For projects that define their indexes in `headband.config.ts` with
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.
513
+
514
+ ```bash
515
+ headband schema diff [--allow-destructive] [--json]
516
+ headband schema generate [--slug <slug>] [--dir <dir>] [--allow-destructive] [--json]
517
+ headband schema apply [--project <id>] [--allow-destructive] [--dir <dir>] [--yes] [--json]
518
+ headband schema status [--dir <dir>] [--json]
519
+ headband schema forget <index> [--yes] [--json]
520
+ ```
521
+
522
+ - `diff` compares the config with the schema state on the server. Like
523
+ `git diff --exit-code`, it exits `1` when changes are pending and `0` when
524
+ in sync. `--json` prints `{inSync, ops}`.
525
+ - `generate` writes a migration file for the pending changes into `--dir`
526
+ (default `headband/migrations`). `--json` prints `{path}`, with `null` when
527
+ there was nothing to generate.
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
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.
543
+
544
+ Schema commands need a key with `schema.manage`, which only `headband login`
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.
552
+
553
+ ### Sync
554
+
555
+ Sync keeps search indexes in step with a Postgres database. There are two ways
556
+ to run it.
557
+
558
+ **Managed Sync** runs on Headband. You register a source database and define
559
+ synced indexes; Headband reads the database's changes and keeps the indexes
560
+ current.
561
+
562
+ ```bash
563
+ headband sync source add <name> [--db-url <url>] [--tls-mode verify-full|require] [--ca-file <pem>] [--json]
564
+ headband sync source add <name> --kind batadb --batadb-project <id> --batadb-branch <id> --batadb-compute <id> --accept-broad-role
565
+ headband sync source list [--json]
566
+ headband sync source show <id> [--json]
567
+ headband sync source test <id> [--json]
568
+ headband sync source setup-sql <id> [--provider generic|rds|batadb] [--json]
569
+ headband sync source setup <id> [--owner-url <url>] [--provider generic|rds|batadb] [--yes] [--json]
570
+ headband sync source set-ca <id> (--ca-file <pem> | --none) [--tls-mode verify-full|require] [--json]
571
+ headband sync source remove <id> [--yes] [--json]
572
+
573
+ headband sync index create -f <file.sync.json> [--json]
574
+ headband sync index list [--source <id>] [--instance <id>] [--json]
575
+ headband sync index show <id> [--version <n>] [--json]
576
+ headband sync index update <id> -f <file.sync.json> [--rebuild] [--json]
577
+ headband sync index validate -f <file.sync.json> [--index <id>] [--json]
578
+ headband sync index preview <id> [--ids <id,id,...>] [-f <file.sync.json>] [--json]
579
+ headband sync index rebuild|pause|resume|check <id> [--yes] [--json]
580
+ headband sync index remove <id> [--drop-index] [--yes] [--json]
581
+
582
+ headband sync status [--json]
583
+ ```
584
+
585
+ Keep connection strings out of shell history: the source URL can come from
586
+ `HEADBAND_SYNC_DB_URL`, the owner URL from `HEADBAND_SYNC_OWNER_URL`, and a
587
+ BataDB API key from `HEADBAND_BATADB_API_KEY` only. `sync source setup` runs
588
+ Headband's setup SQL on your machine as the tables' owner, in one transaction;
589
+ the owner URL never leaves your machine. Run `headband sync source --help` and
590
+ `headband sync index --help` for the details.
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
+
595
+ **Self-hosted Sync** runs a worker in your own infrastructure. Your app writes
596
+ changes to an outbox table, and the worker pushes them to Headband. The
597
+ `@headband/drizzle` README walks through the whole setup.
598
+
599
+ ```bash
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]
602
+ headband sync worker --scaffold docker [--force] [--json]
603
+ headband sync reconcile [--fix] [--json]
604
+ headband sync retry-dead-letters [--json]
605
+ headband sync status [--json]
606
+ ```
151
607
 
152
- **`headband.config.ts`** -- Search configuration that reads from `NEXT_PUBLIC_HEADBAND_KEY` env var with a fallback:
608
+ - `init` prints the outbox table's DDL; `--write` also saves it to
609
+ `headband/outbox.sql`, and `--apply` runs it against `DATABASE_URL` (it is
610
+ `CREATE TABLE IF NOT EXISTS`). `--json` prints `{ddl, path, applied}`.
611
+ - `worker` drains the outbox. Without `--budget-ms` it loops until stopped
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,
617
+ `--json` sends progress to stderr and prints the report when the worker
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.
634
+ - `retry-dead-letters` requeues rows that failed too many times. `--json`
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.
638
+
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.
643
+
644
+ ## What `init` creates
645
+
646
+ **`headband.config.ts`**: search configuration that reads from the
647
+ `NEXT_PUBLIC_HEADBAND_KEY` env var with a fallback:
153
648
 
154
649
  ```typescript
155
650
  export const headbandConfig = {
@@ -164,7 +659,8 @@ by a browser component, so the CLI only accepts scoped `hb_src_` search keys.
164
659
  Keep admin keys in server-only environments for indexing, settings, and sync
165
660
  jobs.
166
661
 
167
- **`components/headband-search.tsx`** (or `src/components/` if your project uses a `src/` directory) -- A ready-to-use search component:
662
+ **`components/headband-search.tsx`** (or `src/components/` if your project uses
663
+ a `src/` directory): a ready-to-use search component:
168
664
 
169
665
  ```tsx
170
666
  "use client";