@quatronic/qore 0.1.1 → 0.1.3

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
@@ -140,20 +140,145 @@ qore run -- pnpm dev
140
140
  no worse than a `.env.local` on disk, but worth knowing before running an
141
141
  untrusted command this way.
142
142
 
143
+ #### `qore dev`
144
+
145
+ Starts this module's dev server, from a trusted repository:
146
+
147
+ ```sh
148
+ qore dev
149
+ ```
150
+
151
+ - **Context + trust**, like `qore run`/`qore env pull` — the same
152
+ `--module`/`--app` resolution, the same trust-on-first-use prompt
153
+ (`--yes` to skip it non-interactively).
154
+ - **Install**, if needed: the package manager comes from package.json's
155
+ `"packageManager": "<pnpm|npm|yarn>@<version>"` field, or (absent that)
156
+ whichever lockfile is present. `node_modules` missing, or older than the
157
+ lockfile, triggers an install; `--no-install` skips this step entirely.
158
+ - **Env merge**: this module's local secrets are pulled and merged into its
159
+ env file on disk (same target and conflict rules as `qore env pull`) —
160
+ unlike `qore run`, which only injects them into the child's environment.
161
+ - **`db:up`**, if the repository declares one: run automatically (unless
162
+ `--no-services`), then `qore dev` waits for `DB_PORT` to accept
163
+ connections before continuing — bounded by `--wait-timeout <s>` (default
164
+ `60`), which also bounds a workspace's provider-port wait (below). A
165
+ service that needs Docker and finds it missing or not running fails
166
+ fast, naming which.
167
+ - **Port check**: `PORT` from the merged env must be free; qore never
168
+ picks, shifts or suggests a port (see "Ports never move" below).
169
+ - **Runs `dev` exactly as `qore run` would** — same package manager, same
170
+ child-process semantics, same exit-code passthrough. The script name is
171
+ `"dev"` by default, or `dev.script` in `.qore.json` (below).
172
+ - `qore dev` in a module repository has no `--json`: the dev server's own
173
+ output passes straight through untouched.
174
+
175
+ A legacy backend `dev` script (from older templates, which need a
176
+ build and a `.env` first) fails fast with `QORE_DEV_UNSUPPORTED`, naming
177
+ the old script and the fix — the same code a repository with no `dev`
178
+ script at all, or an unsupported package manager, gets.
179
+
180
+ **Offline start**: if the portal is unreachable, `qore dev`
181
+ still starts — but only when the repository is already trusted **and** its
182
+ env file already exists on disk (from a previous `qore dev`/`env pull`).
183
+ It starts from that file as last written, with a warning that values may
184
+ be stale. This is the one place `qore` starts without confirming secrets
185
+ are current; every other command still requires the portal. An untrusted
186
+ repository, or one with no env file yet, surfaces the original
187
+ "portal unreachable" error instead.
188
+
189
+ **In a workspace root** (see "Workspaces" below), `qore dev` starts every
190
+ present module of the app, in dependency order, from one process — unless
191
+ `--module <slug>` is passed, which takes the module flow above instead
192
+ (run it from inside that module's own repository, not the workspace root
193
+ itself, or it fails with "Not inside a module repository"). `--app` is
194
+ refused in a workspace root: an app is already implied by
195
+ `qore.workspace.json`.
196
+
197
+ **Ports never move.** If `PORT` (or, for a workspace, a `db:up`'s
198
+ `DB_PORT`) is already taken, `qore dev` fails with `QORE_PORT_IN_USE`
199
+ naming the port and, when it can determine one, the process already
200
+ holding it — it never picks a different port, shifts one, or suggests an
201
+ alternative. Ports are part of the local contract between modules
202
+ (`CONN_*_URL` values point at fixed ports), so moving one silently would
203
+ break every other module that depends on it.
204
+
205
+ #### What qore diagnoses
206
+
207
+ Beyond the checks above, `qore dev` recognises common failure shapes and adds
208
+ a hint rather than leaving a bare error (spec §14.2):
209
+
210
+ - **Docker not running**: in a TTY, waits up to 120 s for it (`Ctrl+C` to
211
+ stop) instead of failing immediately; non-interactive or `--json` fails at
212
+ once.
213
+ - **A leftover `qore dev` holds the port**: in a TTY, offers to stop it
214
+ (`Stop the previous qore dev (pid N)?`); outside a TTY it's just
215
+ `QORE_PORT_IN_USE`.
216
+ - **The dev server exits within seconds of starting**: its tail output is
217
+ classified into a hint — a missing dependency (`Cannot find module`), an
218
+ invalid/missing env var (a `ZodError`'s keys, never values), a port taken
219
+ after the free-port check (`EADDRINUSE`), or a Linux file-watcher limit —
220
+ printed after the exit, never rewriting the server's own output. An
221
+ unrecognised failure falls back to the tool's last lines plus the command
222
+ that ran, never a bare "failed".
223
+ - **The portal is unreachable behind a corporate proxy**: the connect error
224
+ names `HTTPS_PROXY`/`NODE_EXTRA_CA_CERTS` as things to check.
225
+
226
+ `QORE_DEV_EXITED` is not a thrown code: when none of this applies, `qore dev`
227
+ just passes the child process's own exit code through.
228
+
229
+ #### `qore doctor`
230
+
231
+ Runs the same checks `qore` makes before other commands, on demand, without
232
+ doing anything else — the first thing to run when something's wrong, and
233
+ `--json` makes it attachable to a support ticket:
234
+
235
+ ```sh
236
+ qore doctor
237
+ ```
238
+
239
+ - Prints one line per check: `✓` ok, `!` warning, `✗` failing.
240
+ - Checks qore's own version, Node, git, GitHub SSH, Docker + compose, every
241
+ logged-in portal's session, clock skew, and — inside a workspace or module
242
+ repository — each module's port, access and database.
243
+ - **Never fetches the local env and never starts anything.** Ports are read
244
+ from the env file already on disk; access comes from the ordinary
245
+ `GET /api/cli/v1/me` and `.../modules` routes.
246
+ - Exit code `0` only when every check passes; a `!` warning never fails it,
247
+ only a `✗` does.
248
+ - No login profile at all is a warning, not a failure.
249
+ - `--json` shape: success is `{"ok":true,"data":{"results":[...],"context":{...}}}`;
250
+ on failure it's `{"ok":false,"error":{...,"details":{"results":[...],"context":{...}}}}`
251
+ — the full report is always in `details`, even though the top-level error
252
+ names only the first failing check. A check that throws still lets the
253
+ rest run; it just becomes a `fail` with that error attached.
254
+
143
255
  #### `.qore.json`
144
256
 
145
257
  An optional file at the repository root that configures `qore env pull`
146
- for that repository. Schema:
258
+ and `qore dev` for that repository. Schema:
147
259
  [`schema/qore.schema.json`](./schema/qore.schema.json).
148
260
 
149
261
  ```json
150
262
  {
151
263
  "$schema": "https://unpkg.com/@quatronic/qore/schema/qore.schema.json",
152
264
  "envFile": ".env.local",
153
- "localSecrets": ["MY_LOCAL_OVERRIDE"]
265
+ "localSecrets": ["MY_LOCAL_OVERRIDE"],
266
+ "dev": {
267
+ "install": true,
268
+ "services": ["db:up"],
269
+ "script": "dev"
270
+ }
154
271
  }
155
272
  ```
156
273
 
274
+ - **`dev.install`** — run the package manager's install when needed
275
+ (default `true`).
276
+ - **`dev.services`** — package.json scripts to run before the dev server,
277
+ in order (default: `["db:up"]` if that script exists, else none — each
278
+ name must be a real script in package.json).
279
+ - **`dev.script`** — the package.json script `qore dev` runs (default
280
+ `"dev"`).
281
+
157
282
  It never holds a portal URL, tenant, app or module id — those are always
158
283
  resolved from the git remote or `--module`/`--app`, never committed.
159
284
 
@@ -176,6 +301,154 @@ If a repository resolves to no module on any portal you're logged in to,
176
301
  the error lists the portals it tried and, when a staging repository looks
177
302
  likely, suggests `qore login --portal staging`.
178
303
 
304
+ ### Workspaces
305
+
306
+ `qore clone` sets up a local directory holding one or more of an app's
307
+ modules, each in its own repository; `qore sync` keeps that directory in
308
+ step with the app afterwards.
309
+
310
+ ```sh
311
+ qore clone <app> # clones every module of <app> into ./<app-slug>
312
+ qore clone <app> <dir> # ...into <dir> instead
313
+ cd <app-slug>
314
+ qore status # app-level table, one row per module/environment
315
+ cd <module>
316
+ qore env pull # no trust prompt — a clone made by qore is already trusted
317
+ cd ..
318
+ qore sync # clone any module that's new, fetch the rest
319
+ qore sync --pull # ...and fast-forward every clean repository with an upstream
320
+ ```
321
+
322
+ - **`--protocol https|ssh`** picks the clone URL (default `https`). `ssh`
323
+ rewrites `https://github.com/o/r` to `git@github.com:o/r.git`; your own
324
+ git `insteadOf` config still applies on top of either.
325
+ - **`--tenant <slug>`** on `qore clone` disambiguates an app slug that
326
+ exists in more than one tenant you have access to.
327
+ - **The workspace file** is `qore.workspace.json` at the directory `qore
328
+ clone` created, holding `{"schemaVersion": 1, "portal": "<apiOrigin>",
329
+ "appId": "<uuid>"}`. It never lists modules — those always come from the
330
+ portal, so a module added to the app later shows up on the next `qore
331
+ sync` with no local change needed. `qore` finds it by walking up from the
332
+ current directory (stopping at your home directory). The module commands
333
+ (`env pull`, `run`, …), `qore status` and `qore sync` pick up its portal
334
+ (and, for a module shared by several apps, its app) automatically when run
335
+ from inside the workspace — including from a module's own repository;
336
+ `qore clone`, `qore whoami` and `qore tenant` do not read it. An explicit
337
+ `--portal` flag always wins over it. `qore status` shows the app-level
338
+ table only when run in the directory that holds `qore.workspace.json`
339
+ itself (not inside one of its module repositories, which shows that
340
+ module).
341
+ - **Nothing is ever deleted, moved or overwritten.** A present module
342
+ directory is left exactly as it is; `sync` only fetches into it, and
343
+ only fast-forwards with `--pull`, and only when the tree is clean and has
344
+ an upstream — a dirty tree, one with no upstream, or one that would need
345
+ a merge is reported, never touched. `sync` runs a plain `git fetch` (no
346
+ `--prune`). A module's directory whose `origin` doesn't point at that
347
+ module's repository is reported as re-pointed, and one that isn't a git
348
+ repository at all is reported too — neither is repaired; move it aside
349
+ and run `qore sync` again. A repository directory in the workspace that
350
+ isn't one of the app's modules (typically a module that left the app) is
351
+ reported as removed, and left on disk.
352
+ - **A target directory that already holds something else** — someone
353
+ else's files, or another app's or portal's workspace — makes `qore
354
+ clone` fail with `QORE_WORKSPACE_CONFLICT` before writing anything,
355
+ including `qore.workspace.json`. Pick a different directory
356
+ (`qore clone <app> <dir>`).
357
+ - **Per-module clone failures** are diagnosed individually and don't stop
358
+ the rest of the run:
359
+ - `QORE_GITHUB_NOT_LINKED` — your GitHub account isn't linked in Qore;
360
+ link it at the account page, then `qore sync`.
361
+ - `QORE_REPO_NO_ACCESS` — you have no seat on the repository; the error
362
+ names the dev team(s) that build the module and their maintainers to ask.
363
+ - `QORE_REPO_ACCESS_PENDING` — access was granted recently, or your
364
+ GitHub organisation invitation isn't accepted yet; the error links the
365
+ invitation page.
366
+ - `QORE_GIT_AUTH` — git itself couldn't authenticate (no SSH key,
367
+ GitHub's SSH host key not accepted yet, no `gh auth login`, or SSO
368
+ authorisation needed for the org); the error names the fix.
369
+
370
+ `clone`/`sync` exits `0` when at least one module cloned or was already
371
+ present and every failure is one of the first three above
372
+ (`QORE_GITHUB_NOT_LINKED`, `QORE_REPO_NO_ACCESS`,
373
+ `QORE_REPO_ACCESS_PENDING`) — so a developer missing a seat on one module
374
+ of a multi-module app still gets a usable workspace for the rest.
375
+ `QORE_GIT_AUTH` is not an access failure in this sense: it fails the run
376
+ (exit `5`). Any other failure, or a run where nothing succeeded at all,
377
+ exits with that failure's own code. With `--json`, once the per-module
378
+ loop has run its results are under `data.results` (or
379
+ `error.details.results` on failure); an error before that (not logged in,
380
+ no workspace, a conflicting target directory) is an ordinary error
381
+ document without results.
382
+
383
+ #### `qore dev` (workspace root)
384
+
385
+ Run from the directory holding `qore.workspace.json` (not inside one of its
386
+ module repositories — that takes the single-module flow above), starts
387
+ every present module of the app as one supervised run:
388
+
389
+ ```sh
390
+ qore dev # every present, writable module, in dependency order
391
+ qore dev --only api,web # just these (comma-separated)
392
+ qore dev --skip docs # all but these
393
+ qore dev --kill-others # the first module to exit stops the rest
394
+ ```
395
+
396
+ - Only **present** modules run (cloned into the workspace); one not cloned
397
+ yet is skipped with "not cloned — run `qore sync`". A module without
398
+ write access is skipped too (local secrets need write) naming who to
399
+ ask; its dependents still start, with a warning that the dependency
400
+ they point at (`CONN_*_URL`) won't be running — qore never falls back to
401
+ staging. `--only`/`--skip` select by slug; a skip is never fatal by
402
+ itself, only when it leaves nothing to start.
403
+ - **Every module's install, package-manager check and port check run
404
+ first, for every module, before anything starts.** Problems are
405
+ collected per module and all reported together — one module's install
406
+ failure never hides another's, and nothing starts if any of them failed.
407
+ Installs run with bounded parallelism (4 at a time).
408
+ - **Start order follows `dependsOn`**: providers before their consumers.
409
+ A consumer waits (up to `--wait-timeout`, default `60` s) for each
410
+ dependency's `PORT` to accept connections before its own `dev` script
411
+ runs; a dependency that doesn't come up in time (or that exited) logs a
412
+ warning and the consumer starts anyway. Modules that depend on each
413
+ other (a cycle) start together, without waiting, and the cycle is
414
+ reported once. A backend under `node --watch` that crashes waits for a
415
+ file change instead of exiting, so no exit line appears and its
416
+ consumers wait up to `--wait-timeout`.
417
+ - **`--kill-others`**: the first module to exit on its own (success or
418
+ failure) stops every other module; the others' own exit is then reported
419
+ quietly, not as a failure. Without it, `qore dev` keeps every surviving
420
+ module running until you stop it yourself.
421
+ - **Ctrl+C / SIGTERM/SIGHUP** stops every running module in reverse start
422
+ order, `SIGKILL` after a 10 s grace if one doesn't exit on its own; a
423
+ second Ctrl+C escalates to `SIGKILL` immediately. A `db:up` service a
424
+ module started is left running — `qore dev` only stops the dev servers
425
+ it launched directly. During the checks, Ctrl+C / SIGTERM / SIGHUP
426
+ stops a running install or `db:up` before `qore dev` exits.
427
+ - **Offline start** (same rule as module-level): only when every
428
+ module that would start is already trusted and has an env file on disk;
429
+ one exception fails the whole run with the original error.
430
+
431
+ **Output.** Every line of a dev server's own stdout/stderr is prefixed per
432
+ module and column-aligned:
433
+
434
+ ```
435
+ api │ listening on 8000
436
+ web │ listening on 3001
437
+ ```
438
+
439
+ qore's own start, ready and exit lines (`▶ api (pid 4121)`,
440
+ `✓ api is listening on 8000`, `✗ web exited with code 1`) are not prefixed
441
+ and go to stderr, so `--json`'s stdout stays pure NDJSON. A module's own
442
+ stdout goes to stdout and its stderr to stderr. Colour is disabled with `--json`, `--no-color` or `NO_COLOR`.
443
+
444
+ With `--json` — the only qore command that streams instead of printing one
445
+ document — stdout is newline-delimited JSON, one event object per line
446
+ (`{"type":"start"|"ready"|"line"|"exit"|"warning"|"skip"|...}`), instead of
447
+ the usual single `{"ok":...}` document. An `exit` event carries an optional
448
+ `hint` (`{"hint":"..."}`, the same early-exit hint the human renderer prints
449
+ as ` → ...`) when the module crashed early with a non-zero code and no
450
+ signal — never on a signalled or graceful exit.
451
+
179
452
  ### `--json`
180
453
 
181
454
  Every command accepts `--json`, which prints exactly one JSON document on
@@ -192,8 +465,8 @@ qore whoami --json
192
465
  | Code | Meaning |
193
466
  |------|---------|
194
467
  | 0 | Success |
195
- | 1 | Internal error, `git` is not installed or not on `PATH`, or the OS keychain is unavailable/access was denied |
196
- | 2 | Missing or invalid context (e.g. a bad `--portal` value, or no module selected), or the module is part of several apps and no `--app` was given |
468
+ | 1 | Internal error, `git` is not installed or not on `PATH`, the OS keychain is unavailable/access was denied, or qore itself needs a newer Node (`QORE_NODE_VERSION`, checked by `qore doctor`'s own `node` check against qore's own `process.version`); `qore dev`: its package manager isn't installed (`QORE_PACKAGE_MANAGER`), its install failed (`QORE_INSTALL_FAILED`), Docker is missing (`QORE_DOCKER_MISSING`), not running (`QORE_DOCKER_NOT_RUNNING`) or its compose v2 plugin is missing (`QORE_DOCKER_COMPOSE_MISSING`), a `db:up`/service failed, never became healthy (`QORE_SERVICE_UNHEALTHY`) or its data is incompatible (`QORE_SERVICE_DATA_INCOMPATIBLE`), or the repository's own `engines.node` doesn't match the `node` on `PATH` (`QORE_NODE_VERSION`, a different check from qore's own — `qore doctor`'s `node:<module>` check, run only in a trusted repository) |
469
+ | 2 | Missing or invalid context (e.g. a bad `--portal` value, or no module selected), or the module is part of several apps and no `--app` was given; `qore dev`: a port is already in use (`QORE_PORT_IN_USE`) or the repository's dev setup isn't supported (`QORE_DEV_UNSUPPORTED`) |
197
470
  | 4 | Not logged in, session/login expired, login denied, or clock skew |
198
471
  | 5 | Access denied — lost tenant access, resource not found, no developer access |
199
472
  | 6 | Auth or API temporarily unreachable/unavailable |