@headband/cli 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +412 -24
  2. package/dist/index.js +4279 -71
  3. package/package.json +8 -4
package/README.md CHANGED
@@ -1,33 +1,421 @@
1
1
  # @headband/cli
2
2
 
3
- Interactive CLI for setting up [Headband](https://headband.dev) search in any project.
4
-
5
- ## Usage
3
+ Command-line tools for [Headband](https://headband.dev): project setup, login,
4
+ 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.
6
7
 
7
8
  ```bash
9
+ npx @headband/cli --help
8
10
  npx @headband/cli init
11
+ npx @headband/cli login
12
+ ```
13
+
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
+ --json Print one JSON document on stdout; errors print one JSON object on stderr
30
+ --yes, -y Answer yes to confirmations (required when stdin is not a terminal)
31
+ --help, -h Show help for the CLI or a command
32
+ ```
33
+
34
+ ## Authentication
35
+
36
+ ### Interactive login
37
+
38
+ ```bash
39
+ headband login [--url <url>] [--profile <name>] [--json]
40
+ headband logout [--profile <name>] [--yes] [--no-revoke] [--json]
41
+ headband whoami [--json]
42
+ headband profiles list [--json]
43
+ headband profiles use <name>
44
+ ```
45
+
46
+ `login` runs Headband's device flow: it prints a code and a link, opens your
47
+ browser, and waits while you approve it in the dashboard. It saves the key it
48
+ receives as a named profile and makes that profile current. The name defaults
49
+ to `default` for headband.dev and to the host name for any other URL, such as
50
+ `localhost-3000`. The full key is never printed; output shows only its prefix.
51
+
52
+ Log in to several Headband URLs or organizations and switch between them:
53
+
54
+ ```bash
55
+ headband login --profile prod
56
+ headband login --url http://localhost:3000 --profile local
57
+ headband profiles list
58
+ headband profiles use prod
59
+ headband indexes list --profile local # one command, without switching
60
+ ```
61
+
62
+ `logout` revokes the profile's key on the server, then removes it from this
63
+ machine. It asks first at a terminal. `--yes` revokes without asking.
64
+ `--no-revoke` removes the local copy and leaves the key working. If the key is
65
+ already gone on the server, logout still succeeds; any other failure keeps the
66
+ local login so you can retry.
67
+
68
+ Credentials live in `~/.config/headband/credentials.json`, or
69
+ `$XDG_CONFIG_HOME/headband/credentials.json`, with file mode `0600`. A file
70
+ written by CLI 0.2.0 or earlier (profiles keyed by URL) still works: its
71
+ profiles get names on first read, and the file is saved in the new format the
72
+ next time the CLI writes it.
73
+
74
+ ### Tokens and environment variables
75
+
76
+ A script, CI job or agent does not need to log in. Give it a key:
77
+
78
+ | Variable | Flag | Meaning |
79
+ | ------------------ | ----------- | ------------------------------------------------------ |
80
+ | `HEADBAND_API_KEY` | `--token` | The API key to send. Used instead of any stored login. |
81
+ | `HEADBAND_URL` | `--url` | The Headband URL. |
82
+ | `HEADBAND_PROFILE` | `--profile` | Which stored login to use. |
83
+
84
+ Precedence, highest first:
85
+
86
+ - **Key:** `--token`, then `HEADBAND_API_KEY`, then the stored profile.
87
+ - **URL:** `--url`, then `HEADBAND_URL`, then the profile's URL, then
88
+ `https://headband.dev`. With a token, the profile's URL counts only when you
89
+ name the profile with `--profile`.
90
+ - **Profile:** `--profile`, then `HEADBAND_PROFILE`, then the current profile,
91
+ then the only stored profile when there is one.
92
+
93
+ A stored key is only ever sent to the URL it was issued for. `--url` with no
94
+ `--profile` picks the stored profile for that URL; `--url` with a `--profile`
95
+ for a different URL is a usage error. A `--profile` name that does not exist is
96
+ an error, never a silent fallback.
97
+
98
+ ```bash
99
+ export HEADBAND_API_KEY=hb_adm_...
100
+ export HEADBAND_URL=https://headband.dev
101
+ headband indexes list --json
102
+ ```
103
+
104
+ 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
+
109
+ ## Scripts and agents
110
+
111
+ The CLI never waits for input it cannot get.
112
+
113
+ - **No hanging prompts.** A command that would ask for confirmation fails at
114
+ once with exit code `2` and code `confirmation_required` when stdin is not a
115
+ terminal. Pass `--yes` to confirm. `headband init` does the same without
116
+ `--yes`.
117
+ - **One JSON document.** With `--json`, stdout carries exactly one JSON
118
+ document and progress lines go to stderr, so `headband ... --json | jq` is
119
+ safe. `docs export` is the exception by design: its output is the documents.
120
+ - **JSON errors.** With `--json`, a failure prints one line to stderr:
121
+
122
+ ```json
123
+ { "error": "Index not found", "code": "index_not_found", "status": 404, "exitCode": 5 }
124
+ ```
125
+
126
+ `code` is the API's error code when there is one, else a CLI code such as
127
+ `usage_error`, `not_authenticated`, `confirmation_required` or
128
+ `network_error`. `status` is the HTTP status, or `null` when no request was
129
+ made.
130
+
131
+ - **Branch on exit codes**, not on message text. See the table below.
132
+
133
+ ## Exit codes
134
+
135
+ | Code | Meaning | Typical cause |
136
+ | ----- | ---------------- | --------------------------------------------------------------------------------------------------------------- |
137
+ | `0` | Success | |
138
+ | `1` | Error | Anything not listed below; also `schema diff` when changes are pending |
139
+ | `2` | Usage | Unknown command or option, missing or malformed value, HTTP 400/413/422, confirmation needed without a terminal |
140
+ | `3` | Authentication | No key found, a `--profile` that does not exist, HTTP 401 |
141
+ | `4` | Permission | HTTP 403, or 402 (the plan does not allow it) |
142
+ | `5` | Not found | HTTP 404 or 410, `profiles use` with an unknown name |
143
+ | `6` | Conflict | HTTP 409, such as deleting a project that still has indexes |
144
+ | `7` | Rate limited | HTTP 429 |
145
+ | `8` | Server or engine | HTTP 5xx, or the URL could not be reached |
146
+ | `130` | Interrupted | Ctrl+C |
147
+
148
+ ## Command reference
149
+
150
+ ### Setup
151
+
152
+ ```bash
153
+ headband init [--yes]
154
+ ```
155
+
156
+ Asks for your Headband URL, a browser-safe search key (`hb_src_...` only) and
157
+ a default index, installs `@headband/react`, and writes `headband.config.ts`
158
+ plus an example `<HeadbandSearch />` component. `--yes` skips the questions and
159
+ uses defaults. Running `headband` with no command runs `init`.
160
+
161
+ ### Projects
162
+
163
+ ```bash
164
+ headband projects list [--json]
165
+ headband projects create <name> --engine <meilisearch|typesense|elasticsearch|visor> [--json]
166
+ headband projects delete <id> [--force] [--yes] [--json]
167
+ ```
168
+
169
+ `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.
174
+
175
+ ### Keys
176
+
177
+ ```bash
178
+ headband keys list [--json]
179
+ headband keys create <name> --type <admin|search> [--project <id>]
180
+ [--permissions <a,b,...>] [--expires <30d|ISO date>] [--json]
181
+ headband keys revoke <id> [--yes] [--json]
9
182
  ```
10
183
 
11
- This will:
184
+ `keys create` is the only command that prints a raw API key. Store it when it is
185
+ shown: Headband returns raw keys only once.
186
+
187
+ `--permissions` limits the new key to the permissions you list. You can grant
188
+ only permissions your own key holds, and only ones that fit the key type:
189
+
190
+ | Type | Grantable permissions |
191
+ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
192
+ | `search` | `search`, `analytics.write` |
193
+ | `admin` | `search`, `documents.read`, `documents.add`, `documents.delete`, `indexes.create`, `indexes.delete`, `settings.update`, `analytics.write`, `connectors.write`, `instances.manage`, `sync.manage` |
12
194
 
13
- 1. Ask for your Headband URL (default: `https://headband.dev`)
14
- 2. Ask for your search API key (`hb_src_...` or `hb_adm_...`)
15
- 3. Ask for your default search index (e.g. `products`, `movies`)
16
- 4. Detect your package manager (npm, pnpm, yarn, or bun)
17
- 5. Install `@headband/react`
18
- 6. Create `headband.config.ts` with your settings
19
- 7. Create an example `<HeadbandSearch />` component
195
+ Anything else is refused with exit `4` and the names it refused. Without
196
+ `--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.
20
199
 
21
- ## Options
200
+ `--expires` sets when the key stops working: a duration from now (`12h`,
201
+ `30d`, `2w`, `1y`) or an ISO date (`2027-01-31`). It must be in the future and
202
+ at most one year away. Without it, the key does not expire.
22
203
 
23
204
  ```bash
24
- npx @headband/cli init --yes # Skip prompts, use defaults
25
- npx @headband/cli --help # Show help
205
+ headband keys create ci-reader --type admin --permissions search,documents.read --expires 30d --json
26
206
  ```
27
207
 
28
- ## What gets created
208
+ ### Indexes
209
+
210
+ ```bash
211
+ headband indexes list [--json]
212
+ headband indexes create <uid> [--primary-key <pk>] [--engine <engine>] [--instance <id>] [--json]
213
+ headband indexes delete <uid> [--yes] [--json]
214
+ headband indexes settings get <uid>
215
+ headband indexes settings set <uid> --file <settings.json>
216
+ headband indexes stats <uid>
217
+ ```
29
218
 
30
- **`headband.config.ts`** -- Search configuration that reads from `NEXT_PUBLIC_HEADBAND_KEY` env var with a fallback:
219
+ `settings get`, `settings set` and `stats` always print JSON.
220
+
221
+ `--instance <id>` creates the index on one of your dedicated engines (ids from
222
+ `headband instances list`). The index takes that engine's type, so `--engine` is
223
+ optional; if you pass it, it must match.
224
+
225
+ On the engine, the index has a longer name (`engineIndexUid`, such as
226
+ `hbo_<org>_products`). `indexes create` and `indexes list` show it. Use that name,
227
+ not the uid, with the engine's own client on the instance endpoint.
228
+
229
+ ### Documents
230
+
231
+ ```bash
232
+ headband docs push <file.json|file.jsonl> --index <uid> [--json]
233
+ headband docs export --index <uid> [--out <file>] [--format <jsonl|json>] [--batch <n>] [--json]
234
+ headband docs delete <docId> --index <uid> [--json]
235
+ ```
236
+
237
+ `docs push` accepts a JSON array or a JSONL file and sends documents to
238
+ `/v1/bulk` in batches of 1000.
239
+
240
+ `docs export` pages through `GET /v1/documents` and writes every document in
241
+ the index, one page in memory at a time:
242
+
243
+ ```bash
244
+ headband docs export --index products > products.jsonl
245
+ headband docs export --index products --out products.json
246
+ headband docs export --index products | head -5
247
+ ```
248
+
249
+ - Without `--out`, documents stream to stdout and nothing else is printed
250
+ there. Progress for `--out` goes to stderr.
251
+ - `--format` is `jsonl` (one document per line, the default) or `json` (one
252
+ array). With `--out`, a `.json` file name picks `json`.
253
+ - `--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}`.
256
+ - `--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`.
260
+ - Export needs an admin key with `documents.read`.
261
+
262
+ ### Search
263
+
264
+ ```bash
265
+ headband search <index> <query> [--limit N] [--filter <expr>] [--json]
266
+ ```
267
+
268
+ Plain output shows a compact table. `--json` prints the raw API response.
269
+
270
+ ### Tasks
271
+
272
+ ```bash
273
+ headband tasks list [--json]
274
+ headband tasks get <id>
275
+ ```
276
+
277
+ `tasks list` prints a table, or the raw response with `--json`. `tasks get`
278
+ prints JSON.
279
+
280
+ ### Engines
281
+
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.
285
+
286
+ ```bash
287
+ headband instances list [--json]
288
+ headband instances create <meilisearch|elasticsearch|typesense|visor> [--name <name>]
289
+ [--lane <serverless|always_on>] [--memory <512m|1g|...>]
290
+ [--region <region>] [--idle-timeout <seconds>] [--json]
291
+ headband instances show <id> [--json]
292
+ headband instances update <id> [--name <name>] [--lane <serverless|always_on>]
293
+ [--memory <512m|1g|...>] [--idle-timeout <seconds>] [--json]
294
+ headband instances wake <id> [--json]
295
+ headband instances park <id> [--json]
296
+ headband instances wakes <id> [--limit <n>] [--json]
297
+ headband instances delete <id> [--yes] [--json]
298
+ headband instances connect <id> [--client <node|curl>] [--json]
299
+ ```
300
+
301
+ `update` changes only the settings you pass; `--size` is an alias for
302
+ `--memory`. The engine and region cannot change. A resize applies in the
303
+ background: `instances show` reports the state as "applying changes" until it
304
+ is done. Always-on engines need a paid plan (exit `4` otherwise).
305
+
306
+ `wakes` lists recent wake-ups, newest first: when each was requested, how it
307
+ woke, the outcome and how long it took to be ready. `--limit` takes up to 200;
308
+ the default is 50.
309
+
310
+ `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
312
+ 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
315
+ in early access; Elasticsearch runs always on. If a command says the key cannot
316
+ manage engines, run `headband login` again.
317
+
318
+ ### Schema migrations
319
+
320
+ For projects that define their indexes in `headband.config.ts` with
321
+ `@headband/sync`. These commands read that file from the current directory.
322
+
323
+ ```bash
324
+ headband schema diff [--allow-destructive] [--json]
325
+ headband schema generate [--slug <slug>] [--dir <dir>] [--allow-destructive] [--json]
326
+ headband schema apply [--allow-destructive] [--dir <dir>] [--yes] [--json]
327
+ headband schema status [--dir <dir>] [--json]
328
+ ```
329
+
330
+ - `diff` compares the config with the schema state on the server. Like
331
+ `git diff --exit-code`, it exits `1` when changes are pending and `0` when
332
+ in sync. `--json` prints `{inSync, ops}`.
333
+ - `generate` writes a migration file for the pending changes into `--dir`
334
+ (default `headband/migrations`). `--json` prints `{path}`, with `null` when
335
+ 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
341
+ `{indexes, localApplied, localHead}`.
342
+
343
+ Schema commands need a key with `schema.manage`, which only `headband login`
344
+ mints.
345
+
346
+ ### Sync
347
+
348
+ Sync keeps search indexes in step with a Postgres database. There are two ways
349
+ to run it.
350
+
351
+ **Managed Sync** runs on Headband. You register a source database and define
352
+ synced indexes; Headband reads the database's changes and keeps the indexes
353
+ current.
354
+
355
+ ```bash
356
+ headband sync source add <name> [--db-url <url>] [--tls-mode verify-full|require] [--ca-file <pem>] [--json]
357
+ headband sync source add <name> --kind batadb --batadb-project <id> --batadb-branch <id> --batadb-compute <id> --accept-broad-role
358
+ headband sync source list [--json]
359
+ headband sync source show <id> [--json]
360
+ headband sync source test <id> [--json]
361
+ headband sync source setup-sql <id> [--provider generic|rds|batadb] [--json]
362
+ headband sync source setup <id> [--owner-url <url>] [--provider generic|rds|batadb] [--yes] [--json]
363
+ headband sync source set-ca <id> (--ca-file <pem> | --none) [--tls-mode verify-full|require] [--json]
364
+ headband sync source remove <id> [--yes] [--json]
365
+
366
+ headband sync index create -f <file.sync.json> [--json]
367
+ headband sync index list [--source <id>] [--instance <id>] [--json]
368
+ headband sync index show <id> [--version <n>] [--json]
369
+ headband sync index update <id> -f <file.sync.json> [--rebuild] [--json]
370
+ headband sync index validate -f <file.sync.json> [--index <id>] [--json]
371
+ headband sync index preview <id> [--ids <id,id,...>] [-f <file.sync.json>] [--json]
372
+ headband sync index rebuild|pause|resume|check <id> [--yes] [--json]
373
+ headband sync index remove <id> [--drop-index] [--yes] [--json]
374
+
375
+ headband sync status [--json]
376
+ ```
377
+
378
+ Keep connection strings out of shell history: the source URL can come from
379
+ `HEADBAND_SYNC_DB_URL`, the owner URL from `HEADBAND_SYNC_OWNER_URL`, and a
380
+ BataDB API key from `HEADBAND_BATADB_API_KEY` only. `sync source setup` runs
381
+ Headband's setup SQL on your machine as the tables' owner, in one transaction;
382
+ the owner URL never leaves your machine. Run `headband sync source --help` and
383
+ `headband sync index --help` for the details.
384
+
385
+ **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.
387
+
388
+ ```bash
389
+ headband sync init [--table <name>] [--write] [--json]
390
+ headband sync worker [--loop] [--budget-ms <ms>] [--batch-size <n>] [--reconcile-every <n>] [--json]
391
+ headband sync worker --scaffold docker [--force] [--json]
392
+ headband sync reconcile [--fix] [--json]
393
+ headband sync retry-dead-letters [--json]
394
+ ```
395
+
396
+ - `init` prints the outbox table's DDL; `--write` also saves it to
397
+ `headband/outbox.sql`. `--json` prints `{ddl, path}`.
398
+ - `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,
401
+ `--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}`.
408
+ - `retry-dead-letters` requeues rows that failed too many times. `--json`
409
+ prints `{requeued}`.
410
+
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`.
414
+
415
+ ## What `init` creates
416
+
417
+ **`headband.config.ts`**: search configuration that reads from the
418
+ `NEXT_PUBLIC_HEADBAND_KEY` env var with a fallback:
31
419
 
32
420
  ```typescript
33
421
  export const headbandConfig = {
@@ -37,18 +425,18 @@ export const headbandConfig = {
37
425
  };
38
426
  ```
39
427
 
40
- **`components/headband-search.tsx`** (or `src/components/` if your project uses a `src/` directory) -- A ready-to-use search component:
428
+ Do not paste an `hb_adm_` admin key into this flow. The generated config is used
429
+ by a browser component, so the CLI only accepts scoped `hb_src_` search keys.
430
+ Keep admin keys in server-only environments for indexing, settings, and sync
431
+ jobs.
432
+
433
+ **`components/headband-search.tsx`** (or `src/components/` if your project uses
434
+ a `src/` directory): a ready-to-use search component:
41
435
 
42
436
  ```tsx
43
437
  "use client";
44
438
 
45
- import {
46
- headband,
47
- HeadbandProvider,
48
- SearchBox,
49
- Hits,
50
- Stats,
51
- } from "@headband/react";
439
+ import { headband, HeadbandProvider, SearchBox, Hits, Stats } from "@headband/react";
52
440
  import { headbandConfig } from "../headband.config";
53
441
 
54
442
  const client = headband(headbandConfig.url, headbandConfig.searchKey);