naro-cli 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -92,9 +92,55 @@ naro db restore --bookmark <id> Time Travel, last 30 days (or --timestamp <ti
92
92
  asks first, prints the command that undoes it
93
93
  naro db pull Existing database, no migrations yet: writes its schema
94
94
  as migrations/0001_remote_schema.sql, recorded as applied
95
+ naro db reset [--dry-run] Drop every table, view and trigger, clear the ledger and
96
+ apply ./migrations again (asks first; --yes in CI)
97
+ naro db lint [--strict] Check the schema for what D1 actually trips on; exits 1
98
+ on an error, and on a warning too with --strict
99
+ naro db table create posts --column "id:integer:pk:autoincrement" --column "title:text:notnull" --column "created_at:datetime:notnull:default=current_timestamp" --owner user_id
100
+ A table without SQL (--if-not-exists too); --owner adds
101
+ user_id and an owner policy
102
+ naro db table add-column posts --column "published:boolean:notnull:default=false"
103
+ naro db table rename-column posts title headline | rename posts articles
104
+ naro db table drop-column posts published | drop posts (ask first; --yes in CI)
105
+ naro db index create posts user_id,created_at [--unique] [--name <n>] | drop <name>
106
+ Each is one migration in d1_migrations, written to
107
+ ./migrations (--no-write skips it; --dry-run shows the SQL)
95
108
  naro migration new create_users Creates migrations/0001_create_users.sql
96
109
  naro migration list Local files vs the d1_migrations ledger
97
- naro gen types --output src/db.d.ts TypeScript types for the schema
110
+ naro migration repair 0001_init.sql --status applied
111
+ Fix the ledger alone — mark it applied, or --status
112
+ reverted to drop its row. Its SQL is never run
113
+ naro gen types --output src/database.types.ts
114
+ TypeScript types for the schema, for
115
+ createClient<Database>() from naro-js
116
+
117
+ naro backend status On or off, status, URL and anon key (service key hidden)
118
+ naro backend enable Turn the backend on: D1, sign-in and the Data API; safe
119
+ to re-run, and a failed run resumes at the step that
120
+ stopped it (provision is the same command)
121
+ naro backend disable Turn it off: /_naro/* answers 404; data, users and
122
+ keys are kept (asks first; --yes to skip)
123
+ naro backend keys --reveal Print the service key (a secret; needs database write)
124
+ naro backend keys rotate --service Replace a key (--anon or --service); the old one stops
125
+ working at once (asks first)
126
+ naro backend auth Sign-in settings: email, Google, GitHub, redirects,
127
+ and the callback URL to register for each production host
128
+ naro backend auth email set --provider resend --from "App <no-reply@example.com>" --api-key-stdin < resend-key.txt
129
+ Confirmation and password reset emails from your Resend account
130
+ naro backend auth oauth set github --client-id Iv1.abc --client-secret-stdin < github-secret.txt
131
+ naro backend auth redirects add https://app.example.com/auth/
132
+ Where sign-in may send the browser back to: an
133
+ origin, or only the pages under a path
134
+ naro backend auth redirects remove https://app.example.com/auth/
135
+ Exits 1, changing nothing, when it is not in the list
136
+ naro backend auth require-verification on
137
+ Refuse sign-in until the email is confirmed (needs email)
138
+ naro policy list Per-table rules and conditions of the Data API
139
+ naro policy set todos --select owner --insert owner --update owner --delete owner --owner user_id
140
+ naro policy set posts --select-when 'or(is_public.eq.true,user_id.eq.auth.uid())' --insert-when 'user_id.eq.auth.uid()'
141
+ naro policy set posts --from-file policy.json
142
+ The policy as JSON, conditions and all
143
+ naro policy remove todos Back to deny (asks first)
98
144
 
99
145
  naro promote <deployment-id> Publish a build to production
100
146
  naro rollback [deployment-id] Back to the previous production build
@@ -122,6 +168,253 @@ the dump reflects; `naro db restore --bookmark <that>` brings the database back
122
168
  to it. A restore rolls back everything written after that point — the
123
169
  `d1_migrations` ledger included — and prints the bookmark that undoes it.
124
170
 
171
+ `naro db reset` never deletes the database itself: it drops every table, view
172
+ and trigger the project owns, empties the `d1_migrations` ledger and applies
173
+ the local migration files again, so the schema is what those files say. It
174
+ reads the bookmark from just before the drops and prints it, so
175
+ `naro db restore --bookmark <that>` puts the data back. `--dry-run` lists what
176
+ it would drop and re-apply without touching anything.
177
+
178
+ `naro db lint` reads the schema and runs the checks D1 allows. Its rules are
179
+ the ones D1 actually trips on — orphaned rows, a failed `quick_check`, a
180
+ missing or nullable primary key, a foreign key no index starts with, a trigger
181
+ the REST API cannot recreate, a virtual table that makes `db dump` refuse the
182
+ whole database. There is no "unused index" guess: D1 keeps no execution
183
+ statistics to base one on. An error exits 1, a warning exits 0 — `--strict`
184
+ makes warnings fail too, so CI can gate on it:
185
+
186
+ ```bash
187
+ naro db lint --strict --json || exit 1
188
+ ```
189
+
190
+ `db lint` changes nothing, but it needs **write** access to the database, not
191
+ read: its two checks (`foreign_key_check`, `quick_check`) are whole-database
192
+ scans, and naro classifies those as writes on purpose so a read-only role
193
+ cannot start them. That is also the cost warning — both walk every row that
194
+ has a foreign key, and `quick_check` walks the file, so on a large database
195
+ the lint is billed rows-read and can run into the 30-second query limit. Run
196
+ it in CI on a branch's own data, not on a busy production database.
197
+
198
+ `naro db table` and `naro db index` change the schema without SQL. naro
199
+ renders the DDL from checked input — every name quoted, types and defaults
200
+ from a fixed list — and applies it as one migration, recorded in
201
+ `d1_migrations` and written to `./migrations` as `NNNN_schema_<op>_<table>.sql`,
202
+ numbered after your local files and the ledger. `naro migration list`,
203
+ `db pull` and `db reset` see it like any other migration; with `--no-write`
204
+ there is no file, so `db reset` will not re-apply it. `--dry-run` prints the
205
+ SQL and changes nothing. While a local migration is not in the ledger yet, a
206
+ change is refused — `naro db push` it first, or the change would run before it.
207
+
208
+ A column is `name:type[:modifier…]`:
209
+
210
+ | Part | Values |
211
+ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
212
+ | type | `integer`, `real`, `text`, `blob`, `numeric`, `boolean`, `datetime`, `json` (keep JSON in `text`: a `json` column turns `'123'` into a number) |
213
+ | modifier | `pk`, `autoincrement`, `notnull`, `unique`, `default=<value>`, `references=<table>.<column>`, `ondelete=cascade\|set-null\|set-default\|restrict\|no-action` |
214
+ | default | `'text'` (quotes keep `:` and `''` is a quote), `null`, `true`, `false`, `current_timestamp`, `current_date`, `current_time`, a number, or a bare word as text |
215
+
216
+ Every primary key but a single `integer` one is `NOT NULL`; a foreign key must
217
+ point at its table's primary key or a unique column. `--owner user_id` adds
218
+ `user_id TEXT NOT NULL` and, when the backend is on and ready, an owner policy for all
219
+ four actions; any other new table is closed to the Data API until you run
220
+ `naro policy set` (the command says so). Policies are kept by table name: a
221
+ drop removes the table's policy whenever the project has a backend, on or off
222
+ (the command says so), and a rename leaves the old one behind — the command
223
+ prints what to run.
224
+
225
+ SQLite cannot add a PRIMARY KEY, UNIQUE or NOT-NULL-without-default column, or
226
+ drop a primary key, a unique or indexed column, or a table another table
227
+ references; naro refuses those before it tries and says why. It also reads the
228
+ SQL of your triggers and views, and refuses to drop or rename a table or
229
+ column one of them uses (it warns when the SQL leaves that open) — drop or
230
+ recreate the trigger or view with a migration first. Changing a
231
+ column's type needs a new table — write that migration with
232
+ `naro migration new` (create the new table, `INSERT … SELECT`, drop the old
233
+ one, rename the new one) and `naro db push` it.
234
+
235
+ `naro migration repair <name> --status applied|reverted` edits the
236
+ `d1_migrations` ledger and nothing else — it never runs the migration's SQL.
237
+ It is for a ledger that stopped matching the database: a migration whose file
238
+ ran but whose ledger row was never written (a request that timed out between
239
+ the two), or a `db pull` baseline you want to take back. If the ledger already
240
+ says what you asked for, it says so and writes nothing.
241
+
242
+ `naro gen types` writes TypeScript for the schema: a `…Row` interface per
243
+ table and view, and a `Database` type that `createClient<Database>()` from
244
+ `naro-js` reads, with `Tables<"name">`, `TablesInsert<"name">` and
245
+ `TablesUpdate<"name">` to pick one out. `--lang typescript` is the default and
246
+ the only language.
247
+
248
+ ```bash
249
+ naro gen types --output src/database.types.ts
250
+ ```
251
+
252
+ ```ts
253
+ import { createClient } from "naro-js";
254
+ import type { Database } from "./database.types";
255
+
256
+ const naro = createClient<Database>({ url, anonKey });
257
+ const { data } = await naro.from("posts").select("id, title"); // { id: number; title: string }[] | null
258
+ ```
259
+
260
+ `Insert` leaves out generated columns and makes a column optional when it has
261
+ a default, allows NULL, is the `INTEGER PRIMARY KEY`, or is the `--owner`
262
+ column of a table whose insert rule is `owner` (the backend fills it; see
263
+ below); `Update` makes every column optional. A TEXT column also takes an array
264
+ or object (`Json`), which the Data API stores as its `JSON.stringify` text — it
265
+ reads back as that string, so `JSON.parse` it. A BLOB is `number[]` (what D1
266
+ returns) and cannot be written through the Data API; a column with no type, or
267
+ a STRICT table's `ANY`, is `number | string | number[]`. When the backend's
268
+ policies cannot be read (no backend yet, not ready, no permission), the types
269
+ are generated without them and one `⚠` line on stderr says why — `--json`
270
+ carries `policies: "unavailable"` and `policiesReason`.
271
+ Before 2026-09-24 `Database` was a flat map (`Database["users"]` was the row)
272
+ and BLOB was `ArrayBuffer`: use `Tables<"users">` now. The `…Row` interfaces
273
+ are unchanged.
274
+
275
+ `naro backend` is the project's Naro backend: its own D1, email and password
276
+ sign-in, and a Data API that browser code calls through `naro-js` at the
277
+ project's URL. It is off until you turn it on, one project at a time:
278
+ `naro backend enable` (or the Database page of the dashboard). The anon key
279
+ is public — it identifies the project and grants nothing. The service key
280
+ skips every policy, so the CLI prints it only for
281
+ `naro backend keys --reveal` and for a rotation that made a new one; keep it
282
+ on a server. `naro backend enable` exits 1 with `BACKEND_PROVISION_FAILED`
283
+ when a step stopped the run (the server still answers 200), and running it
284
+ again picks up at that step. `naro backend status` calls a run that died
285
+ part-way (still `provisioning`, but nothing holds its lease) stalled; the
286
+ same `enable` picks it up. `provision`, the command's first name, still works.
287
+
288
+ `naro backend disable` turns the backend off: the app's `/_naro/*` requests
289
+ get 404 in up to about 90 seconds, as the routing cache ages out. The D1 with
290
+ its data and users, the keys, the sign-in settings and the policies are kept,
291
+ and `naro backend enable` brings it back with the same keys — the app needs
292
+ no new anon key. It asks first; without a terminal it needs `--yes`, and
293
+ exits 1 with `CONFIRMATION_REQUIRED` otherwise. `naro backend status` prints
294
+ `Backend: off since <time>` (JSON `backend.enabled`, `backend.disabledAt`).
295
+ `Backend: turning off` (JSON `backend.turningOff`) is a turn-off whose routes
296
+ may still answer. While a run holds its lease (`backend.leaseExpiresAt` still
297
+ ahead) it is in progress, and the advice says to check again in a minute;
298
+ otherwise it stopped half-way: run `naro backend disable` again to finish it
299
+ (the control plane retries it too). Another run holding the lease answers
300
+ `BACKEND_BUSY` "Another change to the project backend is in progress. Try
301
+ again in a minute." Both commands are safe to
302
+ re-run and repair what they can: disable removes the routes again, enable
303
+ writes them again. While it is off, key rotation and sign-in changes answer
304
+ `409 BACKEND_OFF`; reads and policy changes still work.
305
+
306
+ A rotation (`naro backend keys rotate --anon|--service`) needs a ready backend;
307
+ a 409 (`BACKEND_NOT_READY`, `BACKEND_BUSY`, `BACKEND_OFF`) means nothing was tried. It
308
+ installs the new key on the Naro worker first and swaps it in the database
309
+ second, and the database decides the outcome: whenever the new key ends up
310
+ live the answer carries it, and the CLI prints it — with a warning to run
311
+ `naro backend enable` if a step after the swap failed. A
312
+ `BACKEND_ROTATION_FAILED` prints no key and usually leaves the old key
313
+ working. If you are not sure — any 5xx, a timeout, a lost connection, or the
314
+ message that the worker may still expect the new key — run
315
+ `naro backend keys --reveal` to see which key is live, then
316
+ `naro backend enable`; the CLI prints that hint (JSON `hint`) and exits 1.
317
+ For a few seconds during a rotation the worker can already refuse the old key.
318
+
319
+ `naro backend auth` sets up email and social sign-in with the project's own
320
+ accounts — Naro runs no shared sender. Email (confirmation and password reset
321
+ links) needs a Resend API key and a `from` address; Google and GitHub need
322
+ the project's own OAuth app. `naro backend auth` prints a callback URL for
323
+ each production host — `https://<project>.naro.sh/_naro/v1/auth/callback/<provider>`
324
+ and the same path on each custom domain. Register, as the app's redirect URI,
325
+ the one for the origin your app passes to `createClient({ url })`: the
326
+ provider sends the browser back to the host the sign-in began on, where its
327
+ cookie is (otherwise `redirect_uri_mismatch`). A GitHub OAuth app takes one
328
+ callback URL, so it serves one of those hosts.
329
+ Secrets are read from stdin only — `--api-key-stdin` and
330
+ `--client-secret-stdin` take a pipe, or ask without echo in a terminal — and
331
+ `--api-key <value>` is refused, so a key never lands in shell history. The
332
+ answers show a secret as its first 7 characters (the email key) or `set`.
333
+
334
+ A sign-in may send the browser back only to the project's production hosts —
335
+ its naro.sh address and its custom domains — and the entries
336
+ `naro backend auth redirects add` allowed: an origin
337
+ (`https://host[:port]`, every page) or an origin and a path ending in `/`
338
+ (`https://app.example.com/auth/`, only the pages under it); `http://` only
339
+ for localhost. The preview host (`<project>-preview.naro.sh`) runs whatever
340
+ branch was pushed against the same accounts, so it is allowed only once it
341
+ is added, and confirmation and reset emails always link to a production
342
+ host. An entry for a production host narrows it too.
343
+ `naro backend auth redirects remove` exits 1 and changes nothing when the
344
+ entry is not in the list, naming the entries that still allow that origin;
345
+ after a removal it names them too. The
346
+ code or reset token rides in the URL fragment, which any script on the page
347
+ — or an open redirect like `/logout?next=…` — can pass on: allow a path of
348
+ sign-in pages rather than a whole origin. Each change uploads
349
+ the Naro worker again; when that upload stops, the change is saved all the
350
+ same and the command exits 1 with `BACKEND_PROVISION_FAILED` — run
351
+ `naro backend enable` to finish. `require-verification on` needs email set
352
+ up (`VERIFICATION_NEEDS_EMAIL`), and email cannot be removed while it is on.
353
+
354
+ Policies are default deny: a table without one refuses every anon and
355
+ signed-in request. Each of `--select`, `--insert`, `--update` and `--delete`
356
+ takes `none`, `public`, `authenticated` or `owner`; `owner` lets a signed-in
357
+ user reach only rows whose `--owner` column holds their user id, and fills
358
+ that column in on insert. What a `policy set` leaves out keeps its stored
359
+ value. A change is live within about 10 seconds. `naro gen types` reads the
360
+ policies too: with `--insert owner --owner user_id`, `user_id` is optional in
361
+ the generated `Insert` even when it is `NOT NULL` with no default — run it
362
+ again after changing a policy.
363
+
364
+ Instead of a rule, an action can hold a condition on the row, like a Supabase
365
+ row-level security policy, written in the filter syntax of `naro-js`'s
366
+ `.or()` (commas are AND) with `auth.uid()` for the signed-in user's id —
367
+ unquoted; `"auth.uid()"` is the text, and `auth.uid` without `()` is refused
368
+ rather than read as text:
369
+
370
+ - `--select-when` and `--delete-when`: the rows reads and deletes reach.
371
+ - `--insert-when`: what every new row must meet.
372
+ - `--update-when`: the rows updates reach; `--update-check`: what every updated
373
+ row must still meet (by default the same as `--update-when`).
374
+ - `--signed-in`: the conditions of this command refuse anon. Without it,
375
+ `auth.uid()` has no value for anon: one comparison with it matches no row,
376
+ and neither does its `not`. Inside a group SQL's three-valued logic applies,
377
+ so `not(and(is_public.eq.1,user_id.eq.auth.uid()))` matches every row that is
378
+ not public, anon included — write what a condition lets in.
379
+
380
+ When any row of an insert or an update misses its condition, nothing is
381
+ written and the Data API answers `403 PERMISSION_DENIED` ("The new row does not
382
+ meet the insert policy of posts."). A check reads a row as it will be stored
383
+ and compares it with the column's collation (`COLLATE NOCASE`, `RTRIM`). So:
384
+
385
+ - a number column it reads takes numbers, booleans and null, not strings, and
386
+ cannot be compared with `like` or `ilike`;
387
+ - a primary key column it reads must be sent, and not as null;
388
+ - any other column it reads may be left out only when it has no default or a
389
+ plain one — `NULL`, `TRUE`, `FALSE`, a number, or a quoted string on a column
390
+ that is not a number column; a column defaulting to `CURRENT_TIMESTAMP` or
391
+ another expression must be sent;
392
+ - it cannot read a generated column or a virtual table's column.
393
+
394
+ One policy's conditions use at most 50 values together (an update's check
395
+ counts each column it reads once more), leaving 50 of D1's 100 per statement
396
+ for the request — 48 for a write with a check, which also binds the table's
397
+ schema to make sure it checks against the table as it is now. `--from-file`
398
+ takes the PUT body, or a policy object from `--json`
399
+ (`naro policy set posts --json | jq .policy > policy.json`), where
400
+ `auth.uid()` is `{"auth":"uid"}`; its `table`, if it has one, must name the
401
+ table being set. It cannot be combined with the other policy options.
402
+ `naro policy list` prints each condition in filter syntax that reads back as
403
+ the same condition, or as JSON where filter syntax cannot say it exactly
404
+ (`deleted_at.eq.null` would be the text `"null"`, and `= NULL` is not
405
+ `IS NULL`); `--json` gives the exact JSON.
406
+
407
+ Two things a condition does not do for you. An update or delete is held to its
408
+ own condition only, not to select's, unless it asks for rows back with
409
+ `.select()`. With an update condition broader than select's, a caller can tell
410
+ rows it cannot select apart without `.select()`: an update that sets a value
411
+ the check refuses answers 403 when a hidden row matches its filters and 200
412
+ when none does, so filter by filter it learns what those rows hold. Keep
413
+ update and delete conditions no broader than select's. And an `--update-check`
414
+ you give replaces the default: without `user_id.eq.auth.uid()` in it, a user
415
+ can give a row away by changing its `user_id`. Write
416
+ `--update-when 'user_id.eq.auth.uid()' --update-check 'user_id.eq.auth.uid(),status.eq.draft'`.
417
+
125
418
  ## Machine-readable output (CI / AI agents)
126
419
 
127
420
  Every command supports `--json`: stdout carries exactly one JSON object,
@@ -142,6 +435,8 @@ $ naro deploy --prod --yes --json
142
435
  Failures carry stable codes (`NOT_LOGGED_IN`, `PROJECT_NOT_LINKED`,
143
436
  `FRAMEWORK_UNSUPPORTED`, `BUILD_FAILED`, `DEPLOYMENT_NOT_FOUND`,
144
437
  `ENV_NOT_FOUND`, `DOMAIN_NOT_FOUND`, `DOMAIN_NOT_READY`,
438
+ `BACKEND_DISABLED`, `BACKEND_NOT_READY`, `BACKEND_BUSY`,
439
+ `BACKEND_ROTATION_FAILED`, `BACKEND_PROVISION_FAILED`, `POLICY_NOT_FOUND`,
145
440
  `VALIDATION_FAILED`, `CONFIRMATION_REQUIRED`, `CANCELED`, …). A destructive
146
441
  command run non-interactively without `--yes` fails with
147
442
  `CONFIRMATION_REQUIRED` instead of silently doing nothing:
@@ -156,8 +451,8 @@ command run non-interactively without `--yes` fails with
156
451
  ```
157
452
 
158
453
  `--yes` (or `-y`) suppresses every prompt — destructive actions
159
- (production rollback/promote, env/domain removal) run unconfirmed, so use it
160
- deliberately in automation.
454
+ (production rollback/promote, env/domain removal, database reset, backend key
455
+ rotation, policy removal) run unconfirmed, so use it deliberately in automation.
161
456
 
162
457
  ## CI authentication
163
458
 
@@ -33,6 +33,7 @@ export interface BuildMetadata {
33
33
  gitCommitSha?: string;
34
34
  gitCommitMessage?: string;
35
35
  artifactKey?: string;
36
+ framework?: string;
36
37
  }
37
38
  /** Mirrors the build observer shape from the internal build package. */
38
39
  export interface BuildObserver {
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __export = (target, all) => {
4
+ for (var name in all)
5
+ __defProp(target, name, { get: all[name], enumerable: true });
6
+ };
7
+
8
+ export {
9
+ __export
10
+ };