@blotout/qinto-cli 0.1.12 → 0.1.14

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
@@ -223,6 +223,10 @@ callee's page, exactly as a site manager does on a deployed site.
223
223
  An app folder added or removed while `dev` is running is not picked up — it says so and asks for a restart. A save inside a
224
224
  mounted app rebuilds that app alone.
225
225
 
226
+ Declared `server.schedules` fire on their own while `dev` runs — at the cron's minute, in UTC, with `scheduledTime` set to that
227
+ instant, exactly as the deployed sweep dispatches them. Each schedule on the app's console page also has a **Run now** button,
228
+ so you never wait for the window.
229
+
226
230
  Three surfaces, one per side of the product:
227
231
 
228
232
  - **The console** is the platform's side. A team and the site `dev.localhost` are seeded for you; every mounted app is listed
@@ -230,10 +234,13 @@ Three surfaces, one per side of the product:
230
234
  declares. It renders the same form the Qinto platform
231
235
  does — text, toggle, and write-only secrets — and an install stays `inactive`, receiving nothing, until every `required`
232
236
  variable is filled in. Adding an install is offered only for an app whose manifest sets `install.multiple`, and the last
233
- install is never removable — uninstall the app instead.
237
+ install is never removable — uninstall the app instead. **Analytics → ID graph** opens the visitor's anon, alias and person
238
+ objects exactly as the platform's page does: paste an anon id, an email or a person id and read its keys and tables as your
239
+ app writes into them.
234
240
  - **The demo site** is the customer's page. It loads `q.js` from the local site worker and drives every method the tag exposes —
235
241
  `track`, `identify`, `consent`, `alias`, `getSession` — then shows which app bundles `q.js` carried and which installs
236
- activated. Its **Events** panel has a button for each of the eleven standard events in
242
+ activated. Its **Visitor** panel shows the browser's anon id, the one to paste into the ID graph page, and the person id
243
+ once `identify` links one. Its **Events** panel has a button for each of the eleven standard events in
237
244
  [the standard events reference](https://docs.qinto.io): selecting one loads its documented payload into an editable box,
238
245
  and **Track it** sends whatever is in it, so you can fire the real shape or your own. Every event reaches your hooks;
239
246
  only one carrying a numeric `value` and a non-empty `orderId` — `Purchase`, among the standard ones — is also recorded as a
@@ -259,11 +266,124 @@ are namespaced by its slug, so two apps declaring a `d1` called `ledger` get a d
259
266
  | `--worker-port <port>` | Port the local site worker listens on (default `4310`). |
260
267
  | `--ui-port <port>` | Port the app panels are served from (default `4302`). |
261
268
  | `--reset` | Discard this directory's local installs, variables, secrets and stored data first. |
269
+ | `--tunnel` | Expose the site worker on a fresh `trycloudflare.com` hostname, so a vendor webhook can reach its endpoints. |
270
+ | `--tunnel-name <name>` | Expose it through this named Cloudflare Tunnel instead, for a hostname that survives a restart. |
262
271
  | `--no-input` | Disable all interactive prompts, mounting every app found. |
263
272
 
264
273
  All four listeners bind `127.0.0.1` only.
265
274
 
266
- Two things behave differently from a deployed site, and `dev` says so rather than letting you discover them:
275
+ **Reaching your endpoints from outside — `--tunnel`.** A vendor webhook cannot call `127.0.0.1`, so `--tunnel` opens a
276
+ Cloudflare Tunnel to the site worker and prints the public origin; the console's Endpoints card then leads with the URL to
277
+ register with the vendor and keeps the loopback one beside it for `curl`.
278
+
279
+ ```
280
+ Anyone who has this URL can:
281
+ · call your app’s ungated endpoints
282
+ · make your app code run against real vendor APIs, with your dev variables and secrets
283
+ · reach whatever the site worker passes through
284
+
285
+ Restrict it with Cloudflare Access on a named tunnel (--tunnel-name) if that matters here.
286
+
287
+ Opening a Cloudflare Tunnel to the site worker — this usually takes 20-40 seconds.
288
+ · downloading cloudflared 2026.8.3…
289
+ · starting cloudflared…
290
+ · connecting to the Cloudflare edge…
291
+ · publishing odd-mule-hits-yet.trycloudflare.com in DNS…
292
+
293
+ Site worker http://127.0.0.1:4310
294
+ Public site https://odd-mule-hits-yet.trycloudflare.com
295
+ ! anyone holding this URL reaches your dev site worker
296
+
297
+ My App (my-app) — installed and active on the seeded site
298
+ POST https://odd-mule-hits-yet.trycloudflare.com/app/my-app/hook
299
+ ```
300
+
301
+ `cloudflared` is downloaded and cached on first use, unless you already have one on `PATH` or at `CLOUDFLARED_PATH`.
302
+
303
+ **The tunnel closes after an hour.** It is a public door onto your machine, and an `app dev` left running overnight would hold it
304
+ open until you noticed. `dev` says how long is left every ten minutes; press `a` then Enter to add an hour, up to three hours from
305
+ now. When it closes, the public URL stops being advertised and the loopback one carries on.
306
+
307
+ **A hostname that survives a restart — `--tunnel-name <name>`.** A quick tunnel is free and needs no account, but its hostname is
308
+ new on every run, so a vendor has to be told the new one each time. A named tunnel keeps one hostname. `dev` reads it from **your
309
+ own Cloudflare account**, never from anything Qinto holds.
310
+
311
+ Run `qinto app dev --tunnel-name <name>`. The first time, `dev` asks for the two things it needs and tells you how to make them:
312
+
313
+ ```
314
+ A named tunnel is read from your own Cloudflare account, and qinto has no credentials for it yet.
315
+
316
+ 1. Open https://dash.cloudflare.com/profile/api-tokens
317
+ 2. Select Create Token, then Create Custom Token.
318
+ 3. Under Permissions, add: Account > Cloudflare Tunnel > Edit
319
+ To let qinto create the tunnel for you, also add: Zone > Zone > Read and Zone > DNS > Edit
320
+ Nothing more: the token is saved on this machine.
321
+ (An account-owned token lists the first row as Cloudflare One Connector: cloudflared, or Argo Tunnel (Legacy).)
322
+ 4. Under Account Resources, select the account that holds the tunnel; under Zone Resources, the zone its hostname is on.
323
+ 5. Create the token and copy it — Cloudflare shows it once.
324
+
325
+ Your account ID is the last part of the dashboard URL:
326
+ https://dash.cloudflare.com/<account-id>
327
+ ```
328
+
329
+ They are saved in **`~/.qinto/cloudflare.json`**, readable only by you, and every later run uses them without asking. They are
330
+ written only **after Cloudflare accepts them**, so a mistyped token is asked for again rather than stored and reported as a tunnel
331
+ fault. Delete that file to enter different ones. `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` in the environment override the
332
+ saved pair and overwrite nothing — that is how CI, or a second account, runs. Set **both or neither**: one on its own is refused
333
+ rather than paired with a credential from somewhere else. `--no-input`, and a run with no terminal, say what to set instead of
334
+ prompting.
335
+
336
+ **A tunnel that does not exist yet is created for you.** When the account has no tunnel under that name, `dev` says so and asks
337
+ whether to create it. Say yes and it asks for the public hostname a vendor should call — a zone on that account or a name one
338
+ level below it, such as `hooks.example.com`; Universal SSL covers no deeper name, so a deeper one is refused before anything is
339
+ made — then makes the tunnel under the name you gave `--tunnel-name`, routes that hostname to
340
+ `http://127.0.0.1:4310` (the port `--worker-port` sets), and adds the proxied CNAME record for it to your zone. The zone is found
341
+ from the hostname, so nothing is created when no zone on the account holds it; a failure after the tunnel exists — a DNS record
342
+ already on that name, or a token without the zone rows — removes the tunnel again, so a fixed token or a different hostname is a
343
+ clean retry. `dev` never deletes a DNS record: when the record's write got no readable answer, it names the record to check in the
344
+ dashboard instead. From then on the tunnel is found by name and nothing is asked. Say no and `dev` prints the dashboard steps
345
+ instead:
346
+
347
+ ```
348
+ To create the tunnel "hooks" yourself:
349
+ 1. Open https://one.dash.cloudflare.com/<account-id>/networks/tunnels and select Create a tunnel.
350
+ 2. Choose Cloudflared and name it "hooks". Skip installing a connector — qinto runs cloudflared for you.
351
+ 3. Under Public Hostnames, add the hostname a vendor should call, with service http://127.0.0.1:4310 (the port --worker-port sets).
352
+ Then run the command again.
353
+ ```
354
+
355
+ `--no-input`, and a run with no terminal, print the same steps rather than creating anything: making a tunnel and a DNS record
356
+ in your account is a question, and a run that promised no prompts cannot ask it.
357
+
358
+ `dev` looks the tunnel up by name and runs it on the token it fetched. cloudflared reads ingress rules top to bottom and stops at
359
+ the first match, so the rule that decides a hostname is often not the rule naming it — an earlier catch-all, wildcard, or
360
+ same-hostname rule takes the request first. A hostname is advertised only when **every rule that can take it points at your worker
361
+ port, ending in one that carries no path**. A rule with a `path` takes some requests and leaves the rest to the rules below it, and
362
+ that path is a regex `dev` cannot evaluate, so one pointing elsewhere is refused rather than guessed at — it may be the rule that
363
+ takes your `/app/...` URLs. Wildcards are never offered: `https://*.example.com` is not something a vendor can call, though it still
364
+ decides the hostnames it covers. Every advertised hostname is then checked in DNS, not just the first. A tunnel that routes nothing
365
+ usable is refused, and the message lists its rules in the order cloudflared reads them. A tunnel created locally
366
+ with `cloudflared tunnel create` has no configuration in the dashboard, so it lands in the same message — configure it there
367
+ instead. `CLOUDFLARE_*` and `TUNNEL_*` are both kept out of cloudflared's own environment: it is given the tunnel token and
368
+ nothing else, so a stray variable cannot redirect the tunnel and your account token is never handed to a process with no use for
369
+ it.
370
+
371
+ **Your own cloudflared setup is left alone, and not used.** `dev` runs cloudflared against a config file of its own, because an
372
+ existing `~/.cloudflared/config.yml` would otherwise win: its `ingress` rules override the `--url` `dev` passes, and the tunnel
373
+ would answer that config's catch-all — a 404 on every webhook — instead of the site worker.
374
+
375
+ **Why it takes 20-40 seconds.** `dev` names each thing it is waiting for, and the wait is deliberate. A fresh
376
+ `trycloudflare.com` hostname is minted about five seconds before DNS publishes it, and `trycloudflare.com` tells resolvers to
377
+ remember a miss for **1800 seconds** — so one lookup a moment too early makes the hostname fail for the next half hour, for you
378
+ and for the vendor you gave it to. `dev` therefore waits for cloudflared to register a connection, then asks the zone's own
379
+ nameservers whether the name exists yet. Those answer from zone data, so the wait itself leaves nothing behind in any cache your
380
+ browser, `curl` or the vendor will read. By the time the URL is printed it works on the first request. The tunnel closes when
381
+ `dev` does.
382
+
383
+ **Only the site worker is exposed** — the same worker, and the same code, that faces the internet on a deployed site. The console
384
+ is never tunneled: it installs apps and shows your variables, and it answers a loopback `Host` and nothing else.
385
+
386
+ Four things behave differently from a deployed site, and `dev` says so rather than letting you discover them:
267
387
 
268
388
  - **`ai`, `vectorize`, and a `hyperdrive` with no local database URL are not bound.** Each needs a Cloudflare account or a
269
389
  database only you can name, so the app boots without those bindings and the start-up banner lists them. Give a hyperdrive its
@@ -271,6 +391,15 @@ Two things behave differently from a deployed site, and `dev` says so rather tha
271
391
  - **Two mounted apps cannot bind the same hyperdrive name to different databases.** workerd names a hyperdrive connection after
272
392
  the binding — your app's own `env.DATABASE` — and keeps the first of a name, so the second app's binding is reported inert
273
393
  instead of silently reaching the first app's database. Rename the binding, or point both at one database.
394
+ - **A `pipeline` writes to a local catalog, not to Cloudflare.** `send()` checks each record against the manifest schema, runs
395
+ the sink `select` on SQLite and appends the rows to `namespace.table` under `~/.qinto/dev/<hash>/catalog/`; `query.catalog`
396
+ reads them back with SQLite plus `to_timestamp_millis`, `date_trunc` and the `json_get_*` functions. The banner prints the
397
+ directory so `sqlite3` can open it, and `--reset` clears it.
398
+ - **An `analytics_engine` dataset is a local SQLite file.** `writeDataPoint` stores the point with the columns Analytics Engine
399
+ gives it, and `query.analytics` reads it back through the same statement gate the platform applies, plus the documented
400
+ Analytics Engine functions — a name outside that list is refused locally rather than answered by SQLite, so a query that runs
401
+ here runs deployed. The one thing it cannot reproduce is the throw: a point that breaks a documented limit is reported on the
402
+ terminal and dropped, where a deployed hook would throw. The banner prints the file, and `--reset` clears it.
274
403
 
275
404
  **The UI panel.** When your manifest declares a `ui` block, `dev` runs its build with the same base path a published panel is
276
405
  served under — `/<appId>/<version>/` — and serves the result on its own port (`--ui-port`, default 4302); every mounted app's