@blotout/qinto-cli 0.1.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.
package/README.md ADDED
@@ -0,0 +1,373 @@
1
+ # @blotout/qinto-cli
2
+
3
+ The Qinto platform CLI. Authenticate against the Qinto platform and operate it from the terminal — or from an AI agent.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm i -g @blotout/qinto-cli
9
+ ```
10
+
11
+ Needs Node.js 24 or newer. The command is `qinto`.
12
+
13
+ ## Log in
14
+
15
+ ```sh
16
+ qinto login
17
+ ```
18
+
19
+ `login` opens your browser on the Qinto consent screen. Authorize one, several, or all of your current teams; each team receives
20
+ the strongest access your role allows there. By default the CLI requests full access (every scope the platform supports, plus
21
+ `offline_access` for silent renewal and `profile` for your account identity); pass `--scopes` to narrow the request.
22
+
23
+ ```sh
24
+ qinto whoami # who am I, with my teams, organizations, and scopes
25
+ qinto whoami --json # the same identity as JSON
26
+ qinto env # choose which Qinto every command talks to
27
+ qinto team # list authorized teams and mark the active one
28
+ qinto team active # choose the local default team
29
+ qinto team active <teamId>
30
+ qinto logout # revoke the session on the server, then clear local credentials
31
+ ```
32
+
33
+ The active team is a local default, not an authorization boundary. Use `--team <teamId>` on a command for a one-off override.
34
+ `qinto login --team <teamId>` preselects that team during consent and prefers it as the active team afterward; use
35
+ `--reauthorize` to force the consent screen. A plain login preserves the active team while it remains authorized. The CLI
36
+ revokes the login it replaces; if that revocation fails, it is remembered and retried on the next login or logout.
37
+
38
+ ## Onboard a domain
39
+
40
+ ```sh
41
+ qinto domain onboard example.com
42
+ qinto domain dns shop.example.com
43
+ ```
44
+
45
+ `domain onboard` guides a human through team confirmation, managed or self-hosted infrastructure, advanced placement settings,
46
+ DNS records, optional email sharing, delivery, deployment, and the final integration snippet. Switching teams in this flow also
47
+ updates the saved active team. DNS can remain pending without making onboarding fail; resume with the secret-free command printed
48
+ by the CLI, or force a fresh check later with `qinto domain dns [siteId-or-hostname]`.
49
+
50
+ Every prompt has a flag, so a full onboarding can be supplied up front. `qinto describe domain onboard` prints the same list as a
51
+ machine-readable spec.
52
+
53
+ | Flag | What it does |
54
+ | --- | --- |
55
+ | `--team <teamId>` | Use this authorized team without changing the active team. |
56
+ | `--mode <mode>` | Choose the infrastructure: `managed` or `self`. |
57
+ | `--name <name>` | Site display name. |
58
+ | `--subdomain <subdomain>` | Site subdomain; also matched against existing sites to detect a resumable one. |
59
+ | `--jurisdiction <jurisdiction>` | Choose the EU (`eu`) or US (`us`) data jurisdiction. |
60
+ | `--location-hint <locationHint>` | Place site state near a Cloudflare region: `wnam`, `enam`, `weur`, `eeur`, `apac`, or `oc`. |
61
+ | `--delivery <delivery>` | Serve from the customer domain (`domain`) or a development URL (`development-url`), even while DNS is pending. |
62
+ | `--site <siteId>` | Resume onboarding an existing site instead of creating one. |
63
+ | `--share-dns <emails>` | Email DNS instructions to 1–10 comma-separated recipients. |
64
+ | `--share-submission-id <submissionId>` | UUID used to retry DNS sharing idempotently. |
65
+ | `--host-id <hostId>` | Use an existing self-host (requires `--mode self`). |
66
+ | `--host-name <name>` | Display name for a new self-host (requires `--mode self`). |
67
+ | `--cloudflare-account-id <accountId>` | Cloudflare account ID for a new self-host (requires `--mode self`). |
68
+ | `--host-zone <zone>` | Cloudflare zone to use for the self-host. |
69
+ | `--workers-for-platforms` | Enable Workers for Platforms on a new self-host. |
70
+ | `--no-input` | Disable all interactive prompts. |
71
+ | `--json` | Print a structured onboarding state as JSON. |
72
+
73
+ Re-running onboarding after an abort tries not to create a duplicate site. The CLI canonicalizes the given domain the way the
74
+ server does (`https://www.example.com/` and `example.com` name the same site) and looks for existing sites on it: interactively
75
+ it offers to resume one or create a new site, and in machine mode it fails with the candidate ids so a script passes `--site
76
+ <siteId>` to resume or `--subdomain` to force a new site. Detection is best-effort — it needs `team:read` to list the team's
77
+ sites, so a credential that only carries `domain:write` skips the check and creates a new site on every run. Resume by id
78
+ instead: right after a site is created the CLI prints its id and a resume command to stderr, and if onboarding fails after that
79
+ the error names the site and the same command. A resume command is `qinto domain onboard --site <siteId> --team <teamId>`,
80
+ followed by whatever names the environment it ran against (`--env sandbox`, or `--api-url <origin>`, and nothing for
81
+ production); when it is produced for a machine it also carries `--no-input --json`.
82
+
83
+ For automation or an AI agent, use `--json`, `--no-input`, or a non-interactive terminal. Supply every conditional input up front:
84
+
85
+ ```sh
86
+ qinto domain onboard example.com --mode managed --team <teamId> --no-input --json
87
+ qinto domain onboard example.com \
88
+ --mode self \
89
+ --host-name production \
90
+ --cloudflare-account-id <accountId> \
91
+ --workers-for-platforms \
92
+ --no-input \
93
+ --json
94
+ qinto domain dns <siteId> --team <teamId> --json
95
+ ```
96
+
97
+ Set `QINTO_CLOUDFLARE_API_TOKEN` when connecting a new self-host non-interactively. The token has no command-line flag and is never
98
+ included in JSON, output, or resume commands. Structured onboarding results use `host_setup_pending`, `dns_pending`, or `complete`;
99
+ all obtained pending states exit `0`, operational failures exit `1`, and invalid or incomplete invocations exit `2`.
100
+
101
+ ## Build an app
102
+
103
+ An app is code you write that runs on the Qinto platform: a **transform** rewrites, enriches, or drops events before destinations
104
+ see them, and a **destination** receives events after transforms ran and forwards them onward. An app belongs to an
105
+ **organization** and is published as immutable **versions**; a team then installs a version on one of its **sites**.
106
+
107
+ The whole loop is a handful of commands:
108
+
109
+ ```sh
110
+ qinto app create my-app # scaffold, install, and offer to register
111
+ qinto app dev # run it against a local Qinto
112
+ qinto app publish # build, upload, and publish a version
113
+ qinto app publish --site example.com # …and install it on a site
114
+ qinto app status my-app --site example.com # desired vs deployed state
115
+ ```
116
+
117
+ ### Scaffold — `qinto app create [dir]`
118
+
119
+ Asks for the app name, proposes a slug derived from it that you confirm or replace, and asks which template to start from. It
120
+ then writes the scaffold, installs its dependencies, and offers to register the app under one of your organizations so you do
121
+ not have to call `qinto app register` yourself; answer no, or pass `--no-register`, to scaffold only. In machine mode nothing is
122
+ registered unless you pass `--register`.
123
+
124
+ Two templates:
125
+
126
+ | Template | What you get |
127
+ | --- | --- |
128
+ | `minimal` | `qinto.app.jsonc`, `package.json`, `tsconfig.json`, and a `src/server.ts` with one `track` hook. |
129
+ | `full` | The same, plus a `transform` hook, a `src/browser.ts` bundle, a D1 database that records every `Purchase` (order id, value, and the SHA-256 of the visitor's email), three install variables, and the Astro UI panel under `ui/`, wired to the platform through `@blotout/qinto-sdk-app/ui`. |
130
+
131
+ The `full` app forwards events to an endpoint the site manager supplies. Both that endpoint and its API key are `required`
132
+ variables, so the install stays inactive — and the app sends nothing anywhere — until they are filled in.
133
+
134
+ The install runs `npm install` in the app directory — and in `ui/` when a panel was scaffolded. A failed install keeps the
135
+ scaffold and names the command to rerun, but exits `1`, so a script does not publish an app with no dependencies.
136
+ `--no-install` skips the install and exits `0`.
137
+
138
+ The panel imports `@blotout/qinto-sdk-app/ui` and resolves it from the app's own dependency one directory up, so `ui/package.json`
139
+ declares only its build tooling.
140
+
141
+ | Flag | What it does |
142
+ | --- | --- |
143
+ | `--name <name>` | App display name. |
144
+ | `--slug <slug>` | Immutable app slug: 3–30 lowercase letters, digits, and hyphens, starting with a letter. Derived from the name when omitted. |
145
+ | `--template <template>` | `minimal` or `full`. Prompted when omitted; `minimal` without prompts. |
146
+ | `--ui` | Also scaffold the Astro UI panel under `ui/` and wire `ui` into the manifest. The `full` template always has one. |
147
+ | `--no-install` | Skip installing the scaffold dependencies. |
148
+ | `--register` / `--no-register` | Register the app after scaffolding, or skip the offer entirely. |
149
+ | `--org <organization>` | Owning organization id or name when registering (prompted when ambiguous). |
150
+ | `--no-input` | Disable all interactive prompts. |
151
+ | `--json` | Print the scaffolded files — and the registered app, when one was created — as JSON. |
152
+
153
+ `qinto app add ui [dir]` adds the same UI panel to an app that was scaffolded without one, and installs it.
154
+
155
+ ### Register — `qinto app register`
156
+
157
+ Registers the slug under an organization. `create --register` calls it for you; run it directly to register an app you scaffolded
158
+ earlier or wrote by hand. The slug is immutable and unique inside the organization.
159
+
160
+ ```sh
161
+ qinto app register --slug my-app --name "My App" --org Acme --description "What it does."
162
+ qinto app list # every app across your organizations
163
+ qinto app list --json
164
+ ```
165
+
166
+ ### Develop locally — `qinto app dev [dir]`
167
+
168
+ `dev` runs your app against a local Qinto. It boots the **released site worker and app runtime** — the same bundles a deploy
169
+ uploads — inside workerd, so the transform chain, the consent gate, the fan-out envelope, the app runtime and `/q.js` assembly are
170
+ the real ones rather than a simulation. Nothing is uploaded, no Cloudflare account is involved, and no login is needed: the CLI
171
+ ships the platform bundles and prints the version it is running.
172
+
173
+ ```sh
174
+ qinto app dev # from inside the app directory
175
+ qinto app dev ./apps/my-app
176
+ ```
177
+
178
+ ```
179
+ My App (my-app) — local Qinto on platform 0.1.0
180
+
181
+ Console http://127.0.0.1:4300
182
+ Demo site http://127.0.0.1:4301
183
+ Site worker http://127.0.0.1:4310
184
+ Dev state ~/.qinto/dev/8f3c1a2b4d5e6f70
185
+ Site token qwat_dev_…
186
+
187
+ The app is not installed yet.
188
+ ```
189
+
190
+ Three surfaces, one per side of the product:
191
+
192
+ - **The console** is the platform's side. A team and the site `dev.localhost` are seeded for you; install the app there, turn it
193
+ on and off, manage its installs, and fill in the variables the manifest declares. It renders the same form the Qinto platform
194
+ does — text, toggle, and write-only secrets — and an install stays `inactive`, receiving nothing, until every `required`
195
+ variable is filled in. Adding an install is offered only for an app whose manifest sets `install.multiple`, and the last
196
+ install is never removable — uninstall the app instead.
197
+ - **The demo site** is the customer's page. It loads `q.js` from the local site worker and drives every method the tag exposes —
198
+ `track`, `identify`, `consent`, `alias`, `getSession` — then shows which app bundles `q.js` carried and which installs
199
+ activated. Its **Events** panel has a button for each of the eleven standard events in
200
+ [the standard events reference](https://docs.qinto.io): selecting one loads its documented payload into an editable box,
201
+ and **Track it** sends whatever is in it, so you can fire the real shape or your own. Every event reaches your hooks;
202
+ only one carrying a numeric `value` and a non-empty `orderId` — `Purchase`, among the standard ones — is also recorded as a
203
+ conversion in the identity graph.
204
+ - **The terminal** streams the trace of every request, your app's own `ctx.observability` lines included: they arrive in the same
205
+ trace as the site worker's spans, exactly as they do in the Qinto platform's real-time logs.
206
+
207
+ `dev` builds the app the way `publish` does — the same esbuild bundles, and the same cross-check that event/lifecycle hooks,
208
+ backfill handlers, and workflows match the manifest — so a mismatch fails here instead of at dispatch. A `d1` resource's phased
209
+ `migrations` use the same statement ledger and backfill checkpoints as a deployed app: rerunning `dev` applies only what is new,
210
+ and an entry changed after it was applied is refused by id and checksum.
211
+
212
+ Everything the session persists — the install, its variables and secrets, and every Durable Object, D1, KV and R2 the two workers
213
+ wrote — lives in `~/.qinto/dev/<project>/`, outside your app directory, so a restart resumes where you left off and the plaintext
214
+ secrets file can never be committed. `--reset` discards all of it.
215
+
216
+ | Flag | What it does |
217
+ | --- | --- |
218
+ | `--console-port <port>` | Port the console is served from (default `4300`). |
219
+ | `--demo-port <port>` | Port the demo site is served from (default `4301`). |
220
+ | `--worker-port <port>` | Port the local site worker listens on (default `4310`). |
221
+ | `--reset` | Discard this project's local install, variables, secrets and stored data first. |
222
+
223
+ All three listeners bind `127.0.0.1` only.
224
+
225
+ One thing behaves differently from a deployed site, and `dev` says so rather than letting you discover it:
226
+
227
+ - **`ai`, `vectorize`, and a `hyperdrive` with no local database URL are not bound.** Each needs a Cloudflare account or a
228
+ database only you can name, so the app boots without those bindings and the start-up banner lists them. Give a hyperdrive its
229
+ local URL in the console to bind it — the origin in your manifest is the *deployed* database and is never used here.
230
+
231
+ **The UI panel.** When your manifest declares a `ui` block, `dev` runs its build with the same base path a published panel is
232
+ served under — `/<appId>/<version>/` — and serves the result on its own port (`--ui-port`, default 4302). **Apps → Panel** in
233
+ the console frames it there. That port is deliberately not the console's: the panel is cross-origin, exactly as it is in
234
+ production, so the `postMessage` origin and `event.source` checks on both sides are the real ones. The console mints a real
235
+ app-UI token, so the panel's calls to `${ctx.siteOrigin}/apps/<slug>/ui/*` go through the site worker's own claim checks.
236
+
237
+ ### Publish — `qinto app publish [dir]`
238
+
239
+ `publish` reads `qinto.app.jsonc`, allocates a draft version, builds, uploads, and finalizes it. Each step is recorded in
240
+ `.qinto/publish.json`, so an interrupted publish resumes where it stopped when you rerun it with the same arguments; `--restart`
241
+ discards that state. A directory-level lock (`.qinto/publish.lock`) stops two publishes racing in the same checkout.
242
+
243
+ What a publish does, in order:
244
+
245
+ 1. **Allocate** a version — `--bump patch|minor|major` (default `patch`) from the highest ready version, or `--version x.y.z`.
246
+ 2. **Build** `server.entry` with esbuild, and `browser.entry` too when the manifest declares one. The built server bundle is
247
+ imported; its event/lifecycle hooks, backfill handlers, and workflows are compared against the manifest before anything is
248
+ uploaded.
249
+ 3. **Build the UI panel**, when `ui` is declared, with the panel's own base path injected, then check that no asset in the
250
+ built HTML points outside that prefix.
251
+ 4. **Declare and upload** the artifacts. Each upload is presigned against the declared SHA-256, so what lands in storage is
252
+ exactly what was built. Redeclaring replaces the previous declaration, so a failed upload is fixed by publishing again.
253
+ 5. **Finalize** the version. `artifacts_stale` means the manifest changed after the build; the CLI rebuilds and redeclares once
254
+ by itself.
255
+ 6. **Install**, when `--site` is given.
256
+
257
+ | Flag | What it does |
258
+ | --- | --- |
259
+ | `--bump <level>` | `patch` (default), `minor`, or `major`, from the highest ready version. |
260
+ | `--version <version>` | Allocate this exact `x.y.z` instead of bumping. |
261
+ | `--org <organization>` | Owning organization id or name, when the slug exists in several. |
262
+ | `--site <site>` | Install the finalized version on this site (hostname or domain id). |
263
+ | `--team <teamId>` | Authorized team to resolve `--site` in. |
264
+ | `--accept-capabilities` | Accept the extra capabilities a newer version asks for. |
265
+ | `--acknowledge-necessary` | Record that this app runs without visitor consent. |
266
+ | `--dry-run` | Build and validate locally; allocate nothing and upload nothing. |
267
+ | `--restart` | Discard `.qinto/publish.json` and start a fresh publish. |
268
+ | `--no-input` | Disable all interactive prompts. |
269
+ | `--json` | Print the published version, and the install when `--site` was given, as JSON. |
270
+
271
+ Installing a version the visitor's consent or the capability set does not already cover fails until you accept it. Interactively
272
+ the CLI asks; in machine mode it exits `2` and names the flag to pass.
273
+
274
+ ### Inspect — `status`, `versions`, `version cancel`
275
+
276
+ ```sh
277
+ qinto app status my-app --site example.com # desired vs deployed version, installs, endpoints, grants
278
+ qinto app versions my-app # every version, drafts included
279
+ qinto app versions my-app --limit 20 --json
280
+ qinto app version cancel my-app 1.4.0 # release a draft this checkout holds
281
+ ```
282
+
283
+ `status` is the one command that answers "did my publish land": `desired` is what you published and installed, `deployed` is what
284
+ the site's worker is actually running, and an install is `inactive` until every required variable is filled in. A **draft** is a
285
+ version that was allocated but never finalized; cancelling one frees its version number. Only the checkout that holds the draft
286
+ can cancel it — an organization owner unwedges an abandoned draft with `--force`.
287
+
288
+ ### The manifest — `qinto.app.jsonc`
289
+
290
+ `qinto.app.jsonc` is the app: it is JSONC (comments and trailing commas allowed), it is validated before every publish, and its
291
+ digest is what the version is finalized against. Every field and option is documented in
292
+ [the manifest reference](https://docs.qinto.io); the summary:
293
+
294
+ | Field | What it declares |
295
+ | --- | --- |
296
+ | `$schema` | The JSON Schema your editor completes and validates the file against. The scaffold points it at `./node_modules/@blotout/qinto-sdk-app/app.schema.json`, which the SDK ships, so completion works offline and matches the SDK version you installed. |
297
+ | `slug`, `name`, `description` | Identity. `slug` is immutable and must match the registered app. |
298
+ | `flows` | `transform`, `destination`, or both. |
299
+ | `consent` | The single consent category a visitor must have granted before events reach the app. |
300
+ | `legal` | `termsUrl` and `privacyUrl` — HTTPS, and your own documents, never a `qinto.io` URL. |
301
+ | `server` | `entry` plus the event and advisory lifecycle hooks the entry exports. |
302
+ | `browser` | `entry` for the bundle inside the site's `q.js`, optional capture names, and action types whose browser handlers return a same-event server result. |
303
+ | `capabilities` | The platform `api` calls and the event `data` the app is allowed to read. |
304
+ | `resources` | Cloudflare bindings the app worker gets: KV, D1, R2, Durable Objects, Vectorize, Hyperdrive, and more. D1 uses immutable named `expand`, resumable `backfill`, and post-convergence `contract` entries. |
305
+ | `variables` | Up to 32 per-install settings a team fills in — `plain` or `secret`, `text`/`password`/`toggle`. |
306
+ | `appVariables` | Up to 16 settings you fill in once for the whole app with `qinto app var set` / `qinto app secret set`. |
307
+ | `endpoints` | Up to 16 HTTP routes served at `https://<site>/app/<slug><path>`. |
308
+ | `rpc` | Functions this app `exposes` to other apps, and the ones it `calls` on them. |
309
+ | `events` | The push topics this app broadcasts to every app on the site. |
310
+ | `ui` | `dir` holding the built panel, and the `build` command that produces it. |
311
+ | `install` | `multiple: true` to let a site install the app more than once, plus a `labelHint`. |
312
+
313
+ Limits worth knowing before a publish: the server bundle is capped at 10 MB, the browser bundle at 2 MB (and 3 MB for all apps on
314
+ one site together), the UI panel at 200 files and 25 MB, and an app at 15 installs per site. A variable value is capped at 256
315
+ serialized bytes, and one variable's values across every install of that app on a site must fit in 5 KB. Every other cap — the
316
+ manifest, the publish, the install, and the runtime envelope — is listed in
317
+ [Qinto app limits](https://docs.qinto.io).
318
+
319
+ ## Built for humans and agents alike
320
+
321
+ Every interactive path has a non-interactive equivalent. This is a standing rule for every current and future command:
322
+
323
+ | Surface | What it does |
324
+ | --- | --- |
325
+ | `qinto describe [command ...]` | Prints a versioned, machine-readable manifest of nested command paths, flags, environment inputs, and structured result states. |
326
+ | `--json` | On data and onboarding commands, prints the typed result as JSON on stdout — nothing else lands on stdout. |
327
+ | `--no-browser` | `login` prints the authorize URL instead of opening a browser; open it in a browser on the same host so its loopback callback reaches the CLI. |
328
+ | `QINTO_API_TOKEN` | A bearer token that takes precedence over stored logins. Works with team API tokens (`qapi_`, from Team → API tokens in the Qinto platform) and OAuth access tokens (`qoa_`). No browser needed. |
329
+ | `QINTO_ENV` | `production` or `sandbox` for every command in the shell — the non-interactive stand-in for `qinto env`, which stores the same choice. |
330
+ | Exit codes | `0` success, `1` operational failure, `2` usage error, `130` aborted prompt (Ctrl-C). Errors go to stderr; with `--json` they are printed as `{"error": "…"}`. |
331
+
332
+ ## Environments
333
+
334
+ Qinto runs in two environments, and every command talks to exactly one of them:
335
+
336
+ | Environment | API origin | Platform |
337
+ | --- | --- | --- |
338
+ | `production` | `https://api.qinto.io` | `https://portal.qinto.io` |
339
+ | `sandbox` | `https://api.qinto.tech` | `https://portal.qinto.tech` |
340
+
341
+ `qinto env` picks one and remembers it, so nothing after it needs a flag:
342
+
343
+ ```sh
344
+ qinto env # pick from a list; prints the current one when nothing can be prompted
345
+ qinto env sandbox # the same choice, non-interactively
346
+ qinto env --json # {"environment":"sandbox","apiOrigin":"https://api.qinto.tech"}
347
+ ```
348
+
349
+ The choice is written to `~/.qinto/settings.json` and applies to every later command, `login` included — so the usual flow is
350
+ `qinto env sandbox`, then `qinto login`, then work as normal.
351
+
352
+ **Each environment keeps its own login.** Sessions are stored per API origin, so switching does not log you out of anywhere:
353
+ log in to production and to sandbox once each, and from then on `qinto env` moves between them with no further login. Each
354
+ environment also remembers its own active team, and renews its own access token from its own refresh token. `qinto env` says so
355
+ when you land on an environment you have no session on; `qinto logout` clears one environment — the one it is pointed at.
356
+
357
+ Four things override it, strongest first:
358
+
359
+ | Override | Scope |
360
+ | --- | --- |
361
+ | `--api-url <origin>` | One command, any origin — a local stack. |
362
+ | `QINTO_API_URL` | Every command in that shell, any origin. |
363
+ | `--env <name>` | One command. |
364
+ | `QINTO_ENV` | Every command in that shell. |
365
+
366
+ With nothing saved and nothing overriding, commands talk to production.
367
+
368
+ Credentials are stored per API origin in `~/.qinto/credentials.json` (file mode 0600), so logins to different environments
369
+ coexist and each keeps its own active team. When a command finds no login for the environment it was pointed at, its error names
370
+ the login command for that same environment. Access tokens renew themselves through the refresh token; when a session can no
371
+ longer be renewed the CLI asks you to log in again. `qinto logout`
372
+ clears local credentials only after the server confirms the revocation — if the API is unreachable, nothing is cleared, so a
373
+ retry can still revoke the session (or revoke it under User → Authorized apps in the Qinto platform).
package/bin/qinto.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/index.js'