@voltro/cli 0.49.0 → 0.50.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/CHANGELOG.md +114 -0
- package/dist/apiBuild-BZ-plaQN.js +2 -0
- package/dist/{apiBuild-QmYHaCF4.js → apiBuild-D3s7lx57.js} +2 -2
- package/dist/appGraph-CM9wvUmD.js +2 -0
- package/dist/appGraph-Cv2W_wO5.js +111 -0
- package/dist/bin.js +1 -1
- package/dist/{build-Bi4O_g7T.js → build-CFmqF39c.js} +7 -7
- package/dist/{checkCommand-udbQpphy.js → checkCommand-DomvYDi1.js} +1 -1
- package/dist/{checkCommand-D8vjQXQt.js → checkCommand-DsKIjL6k.js} +54 -45
- package/dist/{codegenCommand-DApSH7hK.js → codegenCommand-C0S27nbB.js} +2 -2
- package/dist/{codemodRunner-De1_316I.js → codemodRunner-DwY8Rjbl.js} +696 -481
- package/dist/{commands-a5PxCaIg.js → commands-Brp4DI8u.js} +22 -22
- package/dist/{dashboardCommand-DaoUUqJS.js → dashboardCommand-bbLfc9FW.js} +3 -3
- package/dist/{dataCommand-CJv_OMoS.js → dataCommand-k9mXTXin.js} +511 -333
- package/dist/{dev-C31Nvlbv.js → dev-C0cbqysu.js} +4 -4
- package/dist/{dev-DFVuzOj7.js → dev-Dz9GqA6P.js} +1 -1
- package/dist/doctorCommand-BlciWUJb.js +2 -0
- package/dist/{doctorCommand-B3uraeAe.js → doctorCommand-DyViaaCP.js} +38 -22
- package/dist/{doctorSourceRelations-Dd48LPvO.js → doctorSourceRelations-c5mlN5HI.js} +66 -34
- package/dist/{dormancyCommand-QUXQ9yoz.js → dormancyCommand-DMZPA_HW.js} +1 -1
- package/dist/{embeddingsCommand-Zw1-rYSF.js → embeddingsCommand-CMxRPkg6.js} +1 -1
- package/dist/{envCommand-DtaJ4xEf.js → envCommand-CDLr-yD4.js} +2 -2
- package/dist/{evolveCommand-JDD3hE1i.js → evolveCommand-Ct4NesL3.js} +2 -2
- package/dist/{generateCommand-CcyvH2ve.js → generateCommand-BDdOxhnv.js} +1 -1
- package/dist/index.js +1 -1
- package/dist/{infoCommand-COsUC1DB.js → infoCommand-orsKJKUy.js} +1 -1
- package/dist/{inspect-B7U7Cl_Z.js → inspect-CtL_xTbu.js} +1 -0
- package/dist/inspect-CuGDYES0.js +2 -0
- package/dist/{interruptedReplace-C1ZI84xi.js → interruptedReplace-C3O3M1MM.js} +2 -1
- package/dist/interruptedReplace-CvmiAM9K.js +2 -0
- package/dist/manifestBuild-CPjhvM62.js +2 -0
- package/dist/{manifestBuild-BK42hu0k.js → manifestBuild-CuU1VrSm.js} +2 -2
- package/dist/{runtimeTrace-Di6fcndp.js → runtimeTrace-Bo8jWsNe.js} +1 -1
- package/dist/{sdkgen-CIi5gM17.js → sdkgen-OZhiT2vz.js} +1 -1
- package/dist/serveCommand-Bbgg7Paq.js +2 -0
- package/dist/{serveCommand-N3udT49p.js → serveCommand-YbD5VgfV.js} +8 -8
- package/dist/serveEntry.js +1 -1
- package/dist/{start-DvlYDyAr.js → start-7b_bL8Uc.js} +1 -1
- package/dist/{start-7HcSIKEp.js → start-B1nH9ahf.js} +2 -2
- package/dist/startEntry.js +1 -1
- package/dist/{updateCommand-2BfbeMvp.js → updateCommand-BgH6vTtq.js} +2 -1
- package/dist/updateCommand-BweY17z-.js +2 -0
- package/dist/{webDev-B4IEMB0n.js → webDev-BbdkzMbd.js} +1 -1
- package/dist/webDev-BcRFTsMA.js +2 -0
- package/dist/{webhooksCommand-CwX9IzvJ.js → webhooksCommand-DjE6u0r4.js} +1 -1
- package/package.json +41 -17
- package/templates/AGENTS.md +1 -1
- package/templates/agent-docs/_index.md +1 -1
- package/templates/agent-docs/cli.md +113 -3
- package/templates/agent-docs/data.md +36 -0
- package/templates/agent-docs/whats-new.md +30 -41
- package/templates/apps/api-ai/package.json +7 -7
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-sqlite/package.json +8 -8
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-cms/package.json +10 -10
- package/templates/apps/api-collab/package.json +8 -8
- package/templates/apps/api-data-advanced/package.json +8 -8
- package/templates/apps/api-durable/package.json +8 -8
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/api-saas/package.json +11 -11
- package/templates/apps/api-saas-starter/package.json +10 -10
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-status/package.json +8 -8
- package/templates/apps/api-versioning/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/changelog/package.json +6 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-app/package.json +9 -9
- package/templates/apps/frontend-auth/package.json +8 -8
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-cms/package.json +9 -9
- package/templates/apps/frontend-collab/package.json +10 -10
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-docs/package.json +7 -7
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +7 -7
- package/templates/apps/frontend-portal/package.json +8 -8
- package/templates/apps/frontend-saas/package.json +8 -8
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-static-blog/package.json +6 -6
- package/templates/apps/frontend-status/package.json +8 -8
- package/templates/apps/mobile-app/package.json +4 -4
- package/dist/apiBuild-dZbHdqMg.js +0 -2
- package/dist/appGraph-KGDPTuTy.js +0 -110
- package/dist/appGraph-zuMGKVYX.js +0 -2
- package/dist/doctorCommand-CPL6EhrW.js +0 -2
- package/dist/inspect-DUze25t0.js +0 -2
- package/dist/interruptedReplace-DNJ7MKph.js +0 -2
- package/dist/manifestBuild-j0n109tt.js +0 -2
- package/dist/serveCommand-CzSDngKm.js +0 -2
- package/dist/updateCommand-CtYm7aKH.js +0 -2
- package/dist/webDev-B79ji8M5.js +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,120 @@ _Changes staged for the next release accumulate here (rolled up from
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## [0.50.1] — 2026-08-24
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- **@voltro/cli** — A `0.51.0` upgrade note for the case 0.45.0's typed-`source:` note left open: a source that is COMPUTED but whose names are all known — a generic reader that takes its table from `input` or a registry. Both remedies the original offered are wrong there. There is no misspelled name to fix, and widening to `ReactivitySourceValue` (or casting) compiles instantly while taking that entire set of tables permanently out of the check the narrowing exists to provide.
|
|
47
|
+
|
|
48
|
+
The note carries the recipe that keeps the names — `as const satisfies`, with the key set recovered separately — and the trap under it: an ANNOTATION widens the keys back even when the literal carries `as const`, because it is checked against the literal and then replaces its type. Filed under an unreached version because codemod selection is `from < version <= to`: 0.45.0's note can never fire again for anyone already past it. Documented on the subscriptions page in both languages.
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
|
|
52
|
+
- **@voltro/data-transfer** — The `mariadb-dump`-not-on-PATH hint now also warns that a MariaDB 12.x client requires TLS by default, so running it BY HAND against a server without TLS fails with `TLS/SSL error: SSL is required` (2026) and needs `--skip-ssl`. Our own invocation is unaffected. It is said here because this hint is what sends people to install that client package, and the next thing many of them do is run the tool by hand — where the failure reads like a broken install of the thing we just told them to install.
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- **@voltro/cli, @voltro/mcp** — `voltro check`'s `rbac/unguarded-mutation` no longer fires on `internal: true` procedures. Those are in no rpc group and on no route, so the finding's own sentence — "any caller who can reach the rpc surface can invoke it" — named a surface that does not exist, and neither remedy it offered was available: a guard protects nothing there, and `openAccess` is refused outright on an internal descriptor. An app with 990 internal mutations got 990 warnings and a `FAILED` exit from the same tree on which `voltro doctor` reported zero procedures without an access decision, which made `check` unusable as the CI gate it exists to be.
|
|
57
|
+
|
|
58
|
+
The capability manifest now carries `internal: true` per procedure (present only when true), and the rule reads it through `isWireReachable` — the same predicate the boot access gate and all three rpc-group assemblies use, rather than a fourth spelling of the same flag. A manifest that does not carry the field is read as REACHABLE, so the rule stays loud on an api older than the field rather than going quiet on it.
|
|
59
|
+
- **@voltro/data-transfer, @voltro/cli** — `--assets` aborted the whole capture at the first blob reference the storage provider could not resolve. A dangling reference is a fact about the source data — a row pointing at an object that was deleted, or that never arrived because an earlier import ran without the flag — and no backup can put back bytes that are not there. Aborting made the flag unusable for exactly the deployment that needed it: 178 references, one resolvable, and the run stopped at the second.
|
|
60
|
+
|
|
61
|
+
The phase now records the key, steps over it, and the run reports how many were skipped. Only a genuine not-found (`status === 404`) is treated this way; a 403 from a rotated credential or a 5xx from a backend outage still fails the capture, because calling those "the object is gone" turns a recoverable outage into a backup that quietly contains nothing.
|
|
62
|
+
|
|
63
|
+
The same abort also cost the artefact its provenance: `voltro-backup-stamp.json` was written after the asset phase, so a backup taken WITH `--assets` had no stamp at all, and `restore` then greeted an artefact this tool had written minutes earlier as "an older/handmade backup. Cannot verify dialect or schema version." The stamp is now written on every path — it describes the dump, and the dump is on disk and correct by the time the assets run.
|
|
64
|
+
- **@voltro/cli** — `voltro doctor`'s eager-loaded-relation rule resolves nested relations against their PARENT's target table instead of against the query's roots, so every level is reported in one pass. It flattened the `.with({ … })` tree before resolving, which meant a level-2 relation stayed invisible until level 1's table had been added to `source:` — each fix revealed the next level, and a clean run after the first fix meant nothing. One app ran `doctor → fix → doctor` twice before the output stopped producing new findings.
|
|
65
|
+
|
|
66
|
+
The one case that is still genuinely under-reported is now stated instead of being silent: when the executor's base table is neither declared nor textually readable, nothing resolves, and doctor prints which relation names it could not check rather than reporting clean.
|
|
67
|
+
- **@voltro/data-transfer** — A native restore whose client exited before consuming its input took the whole CLI down with an unhandled `EPIPE`. `createReadStream(dump).pipe(child.stdin)` had no `error` listener, so Node threw on a write to a pipe with no reader:
|
|
68
|
+
|
|
69
|
+
Error: write EPIPE Emitted 'error' event on Socket instance at: at Socket.onerror (node:internal/streams/readable:1045:14)
|
|
70
|
+
|
|
71
|
+
What that cost is the point. The crash landed before `close`, so the step never resumed: the client's stderr — the one place the reason was written down — was discarded, the transfer row was never closed, the in-progress marker was never re-asserted, and the operator got a raw Node stack trace over a database that was now half replaced. A deployment met it on a 64 MB restore and could not diagnose it at all, because the only diagnosis had been thrown away.
|
|
72
|
+
|
|
73
|
+
Every stream in the step now carries a handler. `EPIPE` on the child's stdin is deliberately swallowed — a child closes the pipe by dying, and its exit code and stderr are the actual failure — while any other stream error is carried and reported. The failure line now always includes the tool's stderr (and says so explicitly when the tool printed none), plus how many bytes of the artefact had been fed before it stopped. A source error also ends the child's stdin, which turns a restore that used to hang forever on a vanished dump into one that fails, and a clean exit over a broken input stream is reported as a failure rather than as a truncated success.
|
|
74
|
+
- **@voltro/cli** — `voltro check`'s observed-vs-declared findings advised an op that does not exist. The recorder reports what the store did, and `store.upsert` is one call; a `target:` is `InsertTarget | UpdateTarget | DeleteTarget` and has no `upsert` member. So `observed/undeclared-write` said to add `{ table: '…', op: 'upsert' }` — a `TS2322`, from a line whose whole purpose is to be pasted. It now advises both halves, and the observed → declarable mapping is typed `Record<ObservedWriteOp, ReadonlyArray<TargetSpec['op']>>` so a new recorder op cannot ship without a declarable answer.
|
|
75
|
+
|
|
76
|
+
The remedy no longer produces a finding of its own either: declared targets are now collected per table as a SET. `new Map(targets.map(t => [t.table, t.op]))` kept only the last op, so the correct two-entry declaration read as "declares update" and the insert half came back as a `wrong-op`.
|
|
77
|
+
- **@voltro/data-transfer, @voltro/cli** — The in-progress restore marker did not survive a native restore, on the exact dialects that were documented as safe. The claim — postgres and the mysql family drop only the objects the dump names, so the marker row survives — had a correct argument and a wrong premise: a native dump names the WHOLE database, `_voltro_replace_in_progress` included, and a mysql-family restore writes `DROP TABLE IF EXISTS` in front of each table. The table sorts early, so the guard was removed near the START of the window it exists to cover. Measured downstream as one row before and zero after, twice, once by hand and once through the command — and then a restore died and left a database with a schema, some of its tables, no users and no blobs, with nothing to stop the next boot.
|
|
78
|
+
|
|
79
|
+
Two changes, covering different dumps. `voltro data backup` now excludes the marker table (`--exclude-table` / `--ignore-table`), so an artefact we produce cannot carry the thing that erases the guard on the way back in. And `restore` re-asserts the marker after the tool exits, on the failing path as well as the succeeding one, which covers dumps taken before this change and dumps made by hand: rewritten is a warning, and unwritable is an error, because the guard is then off for that run.
|
|
80
|
+
- **@voltro/plugin-storage, @voltro/cli** — `--assets` could not be refused when no storage was configured, and resolved the wrong backend when it was.
|
|
81
|
+
|
|
82
|
+
`resolveStorageProvider({})` is total: its final `default:` arm returns an in-process `memoryProvider`. So the three `if (!provider)` refusals in `voltro data backup` / `export` / `restore` — whose entire job is to refuse a flag the app cannot honour — were checking a condition that cannot hold. What they let through is worse than an unchecked flag: with no storage configured, `backup --assets` captured from a fresh memory provider, found nothing in it, and wrote an artefact stamped `assets: { count: 0 }` — a rollback story that says the blobs are in there.
|
|
83
|
+
|
|
84
|
+
It also ignored the provider the app REGISTERED. An app on `storagePlugin({ provider: s3(…) })` had its export/import/backup read and write the env-derived default instead, which surfaces later as "no object at key" — the exact drift `appStorageProvider()` exists to prevent, in call sites that never adopted it.
|
|
85
|
+
|
|
86
|
+
`configuredStorageProvider()` is the honest predicate: the app's registered provider, else the env-named one, else `undefined`. A memory provider nobody asked for is not a decision. The CLI now loads the app config BEFORE resolving — `storagePlugin(...)` registers at construction, so asking first answered with the default no matter what the app had configured.
|
|
87
|
+
|
|
88
|
+
### Internal (no consumer-facing effect)
|
|
89
|
+
|
|
90
|
+
- **@voltro/cli** — `voltro data backup` / `restore` / `clear-replace-marker` are now driven end to end, one case per declared option and one per refusal, as real CLI subprocesses against a real database.
|
|
91
|
+
|
|
92
|
+
Every defect this command has shipped lived in the SEQUENCE rather than in a function — an unhandled `EPIPE` that discarded the client's stderr before `close` fired, a marker erased by the restore's own artefact, an asset abort that skipped the provenance stamp, a refusal that could not fire — so none of them was reachable by testing a part. `nativeBackupRestore.e2e.test.ts` covers the whole surface against sqlite (a file copy: no vendor binary, runs anywhere) and `nativeBackupRestoreDialects.integration.test.ts` runs the vendor-tool half against postgres, mariadb and mysql, skipping BY NAME when the server or the client binary is absent — including when the client is present but too old for the server, which a "is it on PATH" check reports as ready.
|
|
93
|
+
|
|
94
|
+
The suite checks itself against `DATA_FLAGS`, the command's own declared option list, so an option cannot be added without a case that drives it.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## [0.50.0] — 2026-08-23
|
|
99
|
+
|
|
100
|
+
### ⚠ BREAKING
|
|
101
|
+
|
|
102
|
+
- **@voltro/cli** — The read-only run-history endpoint moved:
|
|
103
|
+
|
|
104
|
+
GET /_voltro/admin/imports → GET /_voltro/admin/transfers
|
|
105
|
+
|
|
106
|
+
Same secret, same query parameters, same response shape. Only the path changed.
|
|
107
|
+
|
|
108
|
+
It answers for four directions now — import, export, native backup, native restore — and it named one of them. The subcommand and the table were renamed for exactly that reason in 0.49.0 and this path was left behind, which is worse than renaming none of them: a reader who follows a rename tries the matching path, gets a 404, and concludes the instance is too old.
|
|
109
|
+
|
|
110
|
+
`voltro data transfers --target api` uses the new path for you. What needs a hand is anything calling it directly — a monitoring check, an uptime probe, a curl in a runbook, a dashboard datasource. The codemod is `manual` and declares `reach: 'beyond-source'`, so it prints whether or not the path is found in your repository.
|
|
111
|
+
- **@voltro/database, @voltro/voltro** — `InterruptedReplace.tables` is `number | null`, and the interface gains `kind?: 'replace' | 'restore'`.
|
|
112
|
+
|
|
113
|
+
`_voltro_replace_in_progress` records two destructive operations now — an import's `--mode replace` and a native `voltro data restore`. A restore replaces the whole database from an artefact, so "how many tables was this going to empty and refill" has no answer: not known up front, not meaningful after.
|
|
114
|
+
|
|
115
|
+
Writing `0` would have preserved the type and been worse. That number is rendered into the boot refusal, which is read under pressure, and it would have said `began emptying 0 table(s)` — a measurement that was never taken. The framework's own text says "this database" instead; `Number(null)` is `0`, so coercing it reintroduces exactly the sentence being avoided.
|
|
116
|
+
|
|
117
|
+
`DataTransferRun.direction` also widens to name `'backup'` and `'restore'` explicitly. That one is documentary — the union already ended in `| string`, so no assignability changes.
|
|
118
|
+
|
|
119
|
+
**`voltro update` carries you across this** — codemod `0.50.0/03_interrupted-replace-tables-nullable`.
|
|
120
|
+
|
|
121
|
+
### Added
|
|
122
|
+
|
|
123
|
+
- **@voltro/cli, @voltro/data-transfer, @voltro/database** — `voltro data backup --assets` and `voltro data restore --assets` now move the stored blobs alongside the vendor dump, through the same content-addressed phase the logical `export` / `import` path uses — streamed, deduped by sha256, verified on the way back, resumable per key.
|
|
124
|
+
|
|
125
|
+
`--assets` was accepted by `backup` and silently ignored, with the only signal a field in the closing JSON reading `NOT included`; `restore` did not accept it at all. A rows-only backup restores a database whose rows reference objects nothing puts back, and the reference and the object are checked at different times, so that state is discovered by a user rather than by the restore.
|
|
126
|
+
|
|
127
|
+
Three refusals, each for a belief that is otherwise acted on silently: `backup --assets` with no storage provider is refused rather than swallowed; `restore --assets` on a rows-only artefact is refused; a restore WITHOUT `--assets` over an artefact that has them warns and proceeds, because restoring rows without blobs is legitimate and refusing it would push people at `--force`.
|
|
128
|
+
|
|
129
|
+
The dump itself still has no resume — a vendor artefact is one opaque file with no offset to restart from, and the logical path is what exists for that.
|
|
130
|
+
- **@voltro/cli, @voltro/database, @voltro/data-transfer** — A native `backup` / `restore` now writes to the same `_voltro_data_transfers` record `import` and `export` use, so `voltro data transfers` answers "did last night's backup finish" from the instance that ran it. A native run reports blobs rather than rows — a vendor tool reports no row count we can trust, and printing `0 row(s)` over a dump that worked would be a wrong measurement.
|
|
131
|
+
|
|
132
|
+
`restore` also writes the `_voltro_replace_in_progress` marker before its first destructive statement and clears it after the last write, blobs included, so a killed restore refuses the next boot instead of serving a half-loaded database. `--allow-live` guards from the wrong side — it asks you not to — and this guards from the right one. The marker carries a `kind` so the refusal can say something different for a `replace` (re-import the capture) and a `restore` (finish the restore), and `tables` is nullable because a native restore has no table count to claim.
|
|
133
|
+
|
|
134
|
+
The sqlite / turso restore is atomic now (temp file + rename): its marker lives in the very file being replaced, so a plain in-place copy would leave a truncated database with nothing left to catch it.
|
|
135
|
+
|
|
136
|
+
A target with no `_voltro_data_transfers` table still gets its backup; the closing line says it was not recorded rather than implying it was.
|
|
137
|
+
- **@voltro/cli** — A re-issued migration note for the `voltro data imports` → `voltro data transfers` rename that shipped in 0.49.0.
|
|
138
|
+
|
|
139
|
+
Codemods are selected by `from < version <= to`, so the 0.49.0 note fires once, on the jump that crosses 0.49.0, and cannot be corrected for anyone already past it. Its gate searched `.ts` / `.tsx` while the command it is about lives in shell scripts, CI job definitions and runbooks — so a project whose only occurrence sat in `.gitlab-ci.yml` crossed 0.49.0 and was told there was nothing to apply.
|
|
140
|
+
|
|
141
|
+
The re-issue is filed under 0.50.0, prints unconditionally, and says what to grep for. Redundant for anyone already fixed; the alternative is firing for nobody.
|
|
142
|
+
|
|
143
|
+
### Fixed
|
|
144
|
+
|
|
145
|
+
- **@voltro/cli** — A `manual` codemod's `appliesTo` can now search every text file the project owns — `.sh`, `.yml`, `.json`, `.md`, `Makefile`, the `.js` scripts — through a new `ctx.text` on the predicate context, and a codemod may declare `reach: 'beyond-source'` to print its note even when nothing matched.
|
|
146
|
+
|
|
147
|
+
Both halves close the same gap, and it was in the codemod most in need of a gate. A manual codemod exists BECAUSE its subject could not be transformed, which usually means it is not source at all — and `appliesTo` was reading the ts-morph project, which holds `.ts` / `.tsx` and nothing else. Measured with one identical CLI invocation in four files: the `.ts` one printed the note, the `.sh`, `.yml` and `.md` ones printed nothing, and the run reported `codemods: nothing to apply for this jump`. That is an acquittal from a check that never looked at the file, and an acquittal gets acted on.
|
|
148
|
+
|
|
149
|
+
`reach: 'beyond-source'` covers what no scan of one repository can reach — an inline script in a CI runner's own UI, a CronJob spec in another repo, a wiki runbook. The note prints either way; only its framing changes, and an uncertain one says plainly that we could not look there.
|
|
150
|
+
- **@voltro/data-transfer** — When `mariadb-dump` is absent and the mysql-family fallback runs Oracle's `mysqldump` against a MariaDB server, the failure now names the way out.
|
|
151
|
+
|
|
152
|
+
The stderr it prints is the child's own words and is the right first thing to show — and on its own it is a dead end: `Unknown table 'COLUMN_STATISTICS' in information_schema (1109)` names a table nobody asked for, in a schema nobody wrote, about a feature nobody enabled. Everything needed to act on it was known where the fallback was DECIDED: which binary we wanted, which one we took, and why the difference matters. That travels with the step now and prints under the stderr, including the counter-move a reader reaches for on their own (`--column-statistics=0` does not exist on `mariadb-dump`, so it fixes the wrong client and breaks the right one).
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
42
156
|
## [0.49.0] — 2026-08-23
|
|
43
157
|
|
|
44
158
|
### ⚠ BREAKING
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { f as e } from "./dev-
|
|
2
|
-
import { vt as t } from "./webDev-
|
|
1
|
+
import { f as e } from "./dev-C0cbqysu.js";
|
|
2
|
+
import { vt as t } from "./webDev-BbdkzMbd.js";
|
|
3
3
|
import { t as n } from "./dialectDriver-czCHYpeH.js";
|
|
4
4
|
import { dirname as r, isAbsolute as i, join as a, relative as o, resolve as s } from "node:path";
|
|
5
5
|
import { existsSync as c, promises as l } from "node:fs";
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { isWireReachable as e } from "@voltro/protocol";
|
|
2
|
+
//#region src/appGraph.ts
|
|
3
|
+
var t = (e) => e.source === void 0 ? [] : typeof e.source == "string" ? [e.source] : e.source, n = (e) => ({
|
|
4
|
+
tables: new Set(e.tables),
|
|
5
|
+
procedures: new Map(e.procedures.map((e) => [e.tag, e])),
|
|
6
|
+
scopes: new Set(e.scopes ?? []),
|
|
7
|
+
input: e
|
|
8
|
+
}), r = (n) => {
|
|
9
|
+
let r = [], i = /* @__PURE__ */ new Set();
|
|
10
|
+
for (let a of n.input.procedures) {
|
|
11
|
+
for (let e of t(a)) i.add(e), n.tables.has(e) || r.push({
|
|
12
|
+
rule: "reference/dangling-source",
|
|
13
|
+
node: `${a.kind}(${a.tag})`,
|
|
14
|
+
...a.file === void 0 ? {} : { file: a.file },
|
|
15
|
+
breaks: [`reads table '${e}' which does not exist`],
|
|
16
|
+
fix: `declare a '${e}.entity.ts' table or fix the query's source`,
|
|
17
|
+
severity: "error"
|
|
18
|
+
});
|
|
19
|
+
for (let e of a.targets ?? []) i.add(e.table), n.tables.has(e.table) || r.push({
|
|
20
|
+
rule: "reference/dangling-target",
|
|
21
|
+
node: `${a.kind}(${a.tag})`,
|
|
22
|
+
...a.file === void 0 ? {} : { file: a.file },
|
|
23
|
+
breaks: [`targets table '${e.table}' (op '${e.op}') which does not exist`],
|
|
24
|
+
fix: `declare a '${e.table}.entity.ts' table or fix the mutation's target`,
|
|
25
|
+
severity: "error"
|
|
26
|
+
});
|
|
27
|
+
for (let e of a.requiresScopes ?? []) n.scopes.size > 0 && !n.scopes.has(e) && r.push({
|
|
28
|
+
rule: "rbac/unknown-scope",
|
|
29
|
+
node: `${a.kind}(${a.tag})`,
|
|
30
|
+
...a.file === void 0 ? {} : { file: a.file },
|
|
31
|
+
breaks: [`requires scope '${e}' which is not declared`],
|
|
32
|
+
fix: `declare the scope '${e}' or fix the requireScope(...) call`,
|
|
33
|
+
severity: "error"
|
|
34
|
+
});
|
|
35
|
+
a.kind === "mutation" && a.hasGuards === !1 && a.openAccess === void 0 && e(a) && r.push({
|
|
36
|
+
rule: "rbac/unguarded-mutation",
|
|
37
|
+
node: `${a.kind}(${a.tag})`,
|
|
38
|
+
...a.file === void 0 ? {} : { file: a.file },
|
|
39
|
+
breaks: [`'${a.tag}' changes state but declares no guard — any caller who can reach the rpc surface can invoke it`],
|
|
40
|
+
fix: "add a guard (e.g. `.guard(requireScope('…'))`), or confirm it is intentionally public",
|
|
41
|
+
severity: "warn"
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
for (let e of n.input.routes ?? []) for (let t of e.binds) n.procedures.has(t) || r.push({
|
|
45
|
+
rule: "binding/unknown-tag",
|
|
46
|
+
node: `route(${e.pattern})`,
|
|
47
|
+
...e.file === void 0 ? {} : { file: e.file },
|
|
48
|
+
breaks: [`binds rpc tag '${t}' which does not exist`],
|
|
49
|
+
fix: `bind an existing tag, or restore '${t}' (it may have been renamed/removed)`,
|
|
50
|
+
severity: "error"
|
|
51
|
+
});
|
|
52
|
+
for (let e of n.input.procedures) for (let t of e.targets ?? []) i.add(t.table);
|
|
53
|
+
let a = new Set(n.input.frameworkOwnedTables ?? []), o = n.input.procedures.filter((e) => e.kind === "action" && t(e).length === 0 && (e.targets ?? []).length === 0).length;
|
|
54
|
+
for (let e of n.tables) a.has(e) || i.has(e) || r.push({
|
|
55
|
+
rule: "orphan/unread-table",
|
|
56
|
+
node: `table(${e})`,
|
|
57
|
+
breaks: o > 0 ? [`no query reads and no mutation writes '${e}'`, `${o} action(s) declare neither source nor target, so one of them may still use it`] : [`no query reads and no mutation writes '${e}'`],
|
|
58
|
+
fix: o > 0 ? "declare `source`/`target` on your actions so this can be answered, then re-run" : `add a query/mutation for '${e}', or remove the table if it's dead`,
|
|
59
|
+
severity: "warn"
|
|
60
|
+
});
|
|
61
|
+
if (n.scopes.size > 0) {
|
|
62
|
+
let e = /* @__PURE__ */ new Set();
|
|
63
|
+
for (let t of n.input.procedures) for (let n of t.requiresScopes ?? []) e.add(n);
|
|
64
|
+
for (let t of [...n.scopes].sort()) t === "admin:full" || e.has(t) || r.push({
|
|
65
|
+
rule: "rbac/unenforced-scope",
|
|
66
|
+
node: `scope(${t})`,
|
|
67
|
+
breaks: [`'${t}' is granted by a role but NO query/mutation/action/stream guards on it`, "a scope nobody checks grants nothing and withholds nothing — the role map reads stricter than the app is"],
|
|
68
|
+
fix: `add \`.guard(requireScope('${t}'))\` to the handlers it is meant to protect — or, if a plugin route, a REST route, or a UI-only affordance enforces it, say so where the role is declared`,
|
|
69
|
+
severity: "warn"
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return r;
|
|
73
|
+
}, i = (e, n) => {
|
|
74
|
+
let r = [];
|
|
75
|
+
if (n.kind === "removeTable") for (let i of e.input.procedures) {
|
|
76
|
+
let e = t(i).includes(n.name), a = (i.targets ?? []).some((e) => e.table === n.name);
|
|
77
|
+
(e || a) && r.push({
|
|
78
|
+
rule: "blast/table-removed",
|
|
79
|
+
node: `${i.kind}(${i.tag})`,
|
|
80
|
+
...i.file === void 0 ? {} : { file: i.file },
|
|
81
|
+
breaks: [`${e ? "reads" : "writes"} table '${n.name}' — would break if removed`],
|
|
82
|
+
fix: `migrate or remove ${i.tag} before dropping '${n.name}'`,
|
|
83
|
+
severity: "error"
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
else if (n.kind === "removeProcedure") for (let t of e.input.routes ?? []) t.binds.includes(n.tag) && r.push({
|
|
87
|
+
rule: "blast/procedure-removed",
|
|
88
|
+
node: `route(${t.pattern})`,
|
|
89
|
+
...t.file === void 0 ? {} : { file: t.file },
|
|
90
|
+
breaks: [`binds '${n.tag}' — would break if removed`],
|
|
91
|
+
fix: `rebind ${t.pattern} to another tag before removing '${n.tag}'`,
|
|
92
|
+
severity: "error"
|
|
93
|
+
});
|
|
94
|
+
else for (let t of e.input.procedures) (t.requiresScopes ?? []).includes(n.name) && r.push({
|
|
95
|
+
rule: "blast/scope-removed",
|
|
96
|
+
node: `${t.kind}(${t.tag})`,
|
|
97
|
+
...t.file === void 0 ? {} : { file: t.file },
|
|
98
|
+
breaks: [`requires scope '${n.name}' — would become unguarded/dangling if removed`],
|
|
99
|
+
fix: `remove the requireScope('${n.name}') call or keep the scope`,
|
|
100
|
+
severity: "error"
|
|
101
|
+
});
|
|
102
|
+
return r;
|
|
103
|
+
}, a = (e, t) => {
|
|
104
|
+
let a = n(e), o = [...r(a), ...t === void 0 ? [] : i(a, t)];
|
|
105
|
+
return {
|
|
106
|
+
ok: !o.some((e) => e.severity === "error"),
|
|
107
|
+
diagnostics: o
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
//#endregion
|
|
111
|
+
export { i, r as n, a as r, n as t };
|
package/dist/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { r as e } from "./commands-
|
|
2
|
+
import { r as e } from "./commands-Brp4DI8u.js";
|
|
3
3
|
import { a as t } from "./nodeEnvironment-cGFAj1J8.js";
|
|
4
4
|
import { t as n } from "./loadEnv-D9nEOClM.js";
|
|
5
5
|
import { n as r } from "./cliError-BmdYnghb.js";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { gn as e } from "./dev-
|
|
2
|
-
import { $ as t, A as n, F as r, I as i, L as a, Q as o, a as s, b as c, d as l, et as u, f as d, ht as f, k as p, rt as m, s as h, v as g, vt as _, y as v } from "./webDev-
|
|
1
|
+
import { gn as e } from "./dev-C0cbqysu.js";
|
|
2
|
+
import { $ as t, A as n, F as r, I as i, L as a, Q as o, a as s, b as c, d as l, et as u, f as d, ht as f, k as p, rt as m, s as h, v as g, vt as _, y as v } from "./webDev-BbdkzMbd.js";
|
|
3
3
|
import { t as y } from "./cliRuntime-Dh7UDinH.js";
|
|
4
4
|
import { a as b } from "./renderModeScan-D7J1B7Kw.js";
|
|
5
|
-
import { i as x, o as S, s as C } from "./apiBuild-
|
|
5
|
+
import { i as x, o as S, s as C } from "./apiBuild-D3s7lx57.js";
|
|
6
6
|
import { n as w } from "./renderProfile-1OWWAAtx.js";
|
|
7
7
|
import { dirname as T, join as E, resolve as D } from "node:path";
|
|
8
8
|
import { existsSync as O, promises as k } from "node:fs";
|
|
@@ -477,7 +477,7 @@ var N = A({ scope: "voltro:build" }), P = async (e) => {
|
|
|
477
477
|
file: n.file,
|
|
478
478
|
reason: n.reason
|
|
479
479
|
});
|
|
480
|
-
let { regenerateRpcGroup: r } = await import("./codegenCommand-
|
|
480
|
+
let { regenerateRpcGroup: r } = await import("./codegenCommand-C0S27nbB.js"), i = await r(e);
|
|
481
481
|
if (i.kind === "refused") {
|
|
482
482
|
F.warn("could not regenerate the rpc group — building against what is on disk", { reason: i.reason });
|
|
483
483
|
return;
|
|
@@ -489,10 +489,10 @@ var N = A({ scope: "voltro:build" }), P = async (e) => {
|
|
|
489
489
|
}, K = async (e) => {
|
|
490
490
|
let n = await l(e);
|
|
491
491
|
if (!n) {
|
|
492
|
-
let { loadApiConfig: t } = await import("./dev-
|
|
492
|
+
let { loadApiConfig: t } = await import("./dev-Dz9GqA6P.js"), n = await t(e);
|
|
493
493
|
if (n) {
|
|
494
494
|
F.info("building api app", { app: n.name ?? "(unnamed)" }), await G(e);
|
|
495
|
-
let { runApiBuild: t, runServeBundleBuild: r } = await import("./apiBuild-
|
|
495
|
+
let { runApiBuild: t, runServeBundleBuild: r } = await import("./apiBuild-BZ-plaQN.js");
|
|
496
496
|
await t(e);
|
|
497
497
|
try {
|
|
498
498
|
await r(e);
|
|
@@ -711,7 +711,7 @@ var N = A({ scope: "voltro:build" }), P = async (e) => {
|
|
|
711
711
|
return t.ran ? t.exitCode : (yield* j.logError("no web app found at this path", { hint: "looked for app.config.ts with `type: 'web'`" }), 1);
|
|
712
712
|
}), J = async (t) => {
|
|
713
713
|
if (t.includes("--target")) {
|
|
714
|
-
let { runSdkBuildCommand: e } = await import("./sdkgen-
|
|
714
|
+
let { runSdkBuildCommand: e } = await import("./sdkgen-OZhiT2vz.js");
|
|
715
715
|
return e(t);
|
|
716
716
|
}
|
|
717
717
|
let n = t.find((e) => !e.startsWith("-")) ?? process.cwd(), r = await y(q(n), { scope: "voltro:build" }), i = e(n);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as e, r as t, t as n } from "./checkCommand-
|
|
1
|
+
import { n as e, r as t, t as n } from "./checkCommand-DsKIjL6k.js";
|
|
2
2
|
export { n as offlineManifest, e as runCheckCommand, t as toInput };
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { t as e } from "./cliArgs-D4p8n7EE.js";
|
|
2
2
|
import { a as t, l as n } from "./codegen-CrMXs4hb.js";
|
|
3
|
-
import { r } from "./appGraph-
|
|
3
|
+
import { r } from "./appGraph-Cv2W_wO5.js";
|
|
4
4
|
import { n as i } from "./cliOutput-D1tSBoRM.js";
|
|
5
5
|
import { n as a, o, r as s, t as c } from "./inspectFetch-EMuhTG_9.js";
|
|
6
6
|
import { r as l } from "./fileTaxonomy-CJfgOllU.js";
|
|
7
|
-
import { S as u } from "./doctorCommand-
|
|
7
|
+
import { S as u } from "./doctorCommand-DyViaaCP.js";
|
|
8
8
|
import { resolve as d } from "node:path";
|
|
9
9
|
import { readdirSync as f, statSync as p } from "node:fs";
|
|
10
10
|
//#region src/observedDiff.ts
|
|
11
|
-
var m = (e) => e === void 0 ? [] : typeof e == "string" ? [e] : e, h =
|
|
12
|
-
"insert",
|
|
13
|
-
"update",
|
|
14
|
-
"delete",
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
var m = (e) => e === void 0 ? [] : typeof e == "string" ? [e] : e, h = {
|
|
12
|
+
insert: ["insert"],
|
|
13
|
+
update: ["update"],
|
|
14
|
+
delete: ["delete"],
|
|
15
|
+
upsert: ["insert", "update"]
|
|
16
|
+
}, g = (e) => e !== "read", _ = (e, t) => `{ table: '${e}', op: '${t}' }`, v = (e, t) => {
|
|
17
17
|
let n = [], r = new Set(t.exercised), i = /* @__PURE__ */ new Map();
|
|
18
18
|
for (let e of t.edges) {
|
|
19
19
|
let t = i.get(e.tag);
|
|
@@ -25,24 +25,32 @@ var m = (e) => e === void 0 ? [] : typeof e == "string" ? [e] : e, h = /* @__PUR
|
|
|
25
25
|
a.push(t.tag);
|
|
26
26
|
continue;
|
|
27
27
|
}
|
|
28
|
-
let e = `${t.kind}(${t.tag})`, s = t.file, c = i.get(t.tag) ?? [], l = new Set(m(t.source)), u = new Map(
|
|
29
|
-
for (let
|
|
30
|
-
let
|
|
31
|
-
|
|
28
|
+
let e = `${t.kind}(${t.tag})`, s = t.file, c = i.get(t.tag) ?? [], l = new Set(m(t.source)), u = /* @__PURE__ */ new Map();
|
|
29
|
+
for (let e of t.targets ?? []) {
|
|
30
|
+
let t = u.get(e.table) ?? /* @__PURE__ */ new Set();
|
|
31
|
+
t.add(e.op), u.set(e.table, t);
|
|
32
|
+
}
|
|
33
|
+
for (let t of c) if (g(t.op)) {
|
|
34
|
+
let r = h[t.op], i = u.get(t.table);
|
|
35
|
+
if (i === void 0) n.push({
|
|
32
36
|
rule: "observed/undeclared-write",
|
|
33
37
|
node: e,
|
|
34
38
|
...s === void 0 ? {} : { file: s },
|
|
35
39
|
breaks: [`writes '${t.table}' (${t.op}) but declares no target for it`, "subscriptions on that table are not invalidated by this mutation"],
|
|
36
|
-
fix: `add {
|
|
37
|
-
severity: "warn"
|
|
38
|
-
}) : r !== t.op && n.push({
|
|
39
|
-
rule: "observed/wrong-op",
|
|
40
|
-
node: e,
|
|
41
|
-
...s === void 0 ? {} : { file: s },
|
|
42
|
-
breaks: [`declares op '${r}' on '${t.table}' but performed '${t.op}'`, "the synthesized optimistic patch has the wrong shape"],
|
|
43
|
-
fix: `change the target op for '${t.table}' to '${t.op}'`,
|
|
40
|
+
fix: `add ${r.map((e) => _(t.table, e)).join(" and ")} to this procedure's targets`,
|
|
44
41
|
severity: "warn"
|
|
45
42
|
});
|
|
43
|
+
else {
|
|
44
|
+
let a = r.filter((e) => !i.has(e));
|
|
45
|
+
a.length > 0 && n.push({
|
|
46
|
+
rule: "observed/wrong-op",
|
|
47
|
+
node: e,
|
|
48
|
+
...s === void 0 ? {} : { file: s },
|
|
49
|
+
breaks: [`declares ${[...i].map((e) => `'${e}'`).join(" + ")} on '${t.table}' but performed '${t.op}'`, "the synthesized optimistic patch has the wrong shape"],
|
|
50
|
+
fix: r.length > 1 ? `an upsert is both halves — declare ${r.map((e) => _(t.table, e)).join(" and ")}` : `change the target op for '${t.table}' to '${a[0]}'`,
|
|
51
|
+
severity: "warn"
|
|
52
|
+
});
|
|
53
|
+
}
|
|
46
54
|
} else !l.has(t.table) && !u.has(t.table) && n.push({
|
|
47
55
|
rule: "observed/undeclared-read",
|
|
48
56
|
node: e,
|
|
@@ -82,7 +90,7 @@ var m = (e) => e === void 0 ? [] : typeof e == "string" ? [e] : e, h = /* @__PUR
|
|
|
82
90
|
total: e.length
|
|
83
91
|
}
|
|
84
92
|
};
|
|
85
|
-
},
|
|
93
|
+
}, y = (e, n = [], r = 0) => {
|
|
86
94
|
if (r > 12) return n;
|
|
87
95
|
let i;
|
|
88
96
|
try {
|
|
@@ -106,24 +114,24 @@ var m = (e) => e === void 0 ? [] : typeof e == "string" ? [e] : e, h = /* @__PUR
|
|
|
106
114
|
} catch {
|
|
107
115
|
continue;
|
|
108
116
|
}
|
|
109
|
-
o.isDirectory() ?
|
|
117
|
+
o.isDirectory() ? y(i, n, r + 1) : o.isFile() && t.test(a) && n.push(i);
|
|
110
118
|
}
|
|
111
119
|
return n;
|
|
112
|
-
},
|
|
120
|
+
}, b = /* @__PURE__ */ new Set(["actors", "tenants"]), x = async (e) => {
|
|
113
121
|
try {
|
|
114
122
|
let { readFile: t } = await import("node:fs/promises"), { join: n } = await import("node:path"), r = await t(n(e, "app.graph.observed.generated.json"), "utf8"), i = JSON.parse(r);
|
|
115
123
|
return i?.version !== 1 || !Array.isArray(i.edges) || !Array.isArray(i.exercised) ? null : i;
|
|
116
124
|
} catch {
|
|
117
125
|
return null;
|
|
118
126
|
}
|
|
119
|
-
},
|
|
127
|
+
}, S = (e) => {
|
|
120
128
|
let { exercised: t, total: n } = e.coverage, r = `\nobserved: ${t}/${n} procedures exercised (${n === 0 ? 0 : Math.round(t / n * 100)}%)`;
|
|
121
129
|
if (e.diagnostics.length === 0) return `${r}\n no declared/observed mismatches${e.unexercised.length > 0 ? `\n ${e.unexercised.length} unexercised — no recording, so nothing is claimed about them` : ""}`;
|
|
122
130
|
let i = e.diagnostics.map((e) => ` ${e.node}${e.file === void 0 ? "" : ` (${e.file})`}\n` + e.breaks.map((e) => ` ${e}`).join("\n") + `\n fix: ${e.fix}`), a = e.unexercised.length > 0 ? `\n ${e.unexercised.length} unexercised — no recording, so nothing is claimed about them` : "";
|
|
123
131
|
return `${r}\n${i.join("\n")}${a}`;
|
|
124
|
-
},
|
|
132
|
+
}, C = (e, t) => ({
|
|
125
133
|
tables: (t.tables ?? []).map((e) => e.name),
|
|
126
|
-
frameworkOwnedTables: (t.tables ?? []).filter((e) => e.framework === !0 || e.name.startsWith("_voltro_") ||
|
|
134
|
+
frameworkOwnedTables: (t.tables ?? []).filter((e) => e.framework === !0 || e.name.startsWith("_voltro_") || b.has(e.name)).map((e) => e.name),
|
|
127
135
|
scopes: e.scopes ?? [],
|
|
128
136
|
procedures: (e.procedures ?? []).map((e) => {
|
|
129
137
|
let t = (e.guards ?? []).flatMap((e) => e.kind === "scope" ? e.scope ?? [] : []), n = (e.guards ?? []).find((e) => e.kind === "open")?.reason;
|
|
@@ -134,14 +142,15 @@ var m = (e) => e === void 0 ? [] : typeof e == "string" ? [e] : e, h = /* @__PUR
|
|
|
134
142
|
...e.targets === void 0 ? {} : { targets: e.targets },
|
|
135
143
|
...t.length > 0 ? { requiresScopes: [...new Set(t)] } : {},
|
|
136
144
|
...e.guards === void 0 ? {} : { hasGuards: e.guards.some((e) => e.kind !== "open") },
|
|
137
|
-
...n === void 0 ? {} : { openAccess: n }
|
|
145
|
+
...n === void 0 ? {} : { openAccess: n },
|
|
146
|
+
...e.internal === !0 ? { internal: !0 } : {}
|
|
138
147
|
};
|
|
139
148
|
})
|
|
140
|
-
}),
|
|
149
|
+
}), w = (e) => e.length === 0 ? "✓ voltro check: no issues" : e.map((e) => {
|
|
141
150
|
let t = e.severity === "error" ? "✗ error" : "⚠ warn", n = e.file ? ` (${e.file})` : "";
|
|
142
151
|
return `${t} ${e.rule} ${e.node}${n}\n ${e.breaks.join("; ")}\n fix: ${e.fix}`;
|
|
143
|
-
}).join("\n"),
|
|
144
|
-
let { walk: t, loadDiscovered: n, loadApiConfig: r } = await import("./dev-
|
|
152
|
+
}).join("\n"), T = async (e) => {
|
|
153
|
+
let { walk: t, loadDiscovered: n, loadApiConfig: r } = await import("./dev-Dz9GqA6P.js"), { buildRpcEntry: i } = await import("./manifestBuild-CPjhvM62.js"), { buildCapabilityManifest: a } = await import("./inspect-CuGDYES0.js"), { relative: o } = await import("node:path"), s = await n(await t(e)), c = (t, n, r) => i(t, n, o(e, r)), l = [
|
|
145
154
|
...s.queries.map((e) => c(e.descriptor, "query", e.file)),
|
|
146
155
|
...s.mutations.map((e) => c(e.descriptor, "mutation", e.file)),
|
|
147
156
|
...s.actions.map((e) => c(e.descriptor, "action", e.file)),
|
|
@@ -157,31 +166,31 @@ var m = (e) => e === void 0 ? [] : typeof e == "string" ? [e] : e, h = /* @__PUR
|
|
|
157
166
|
})), [], m),
|
|
158
167
|
tables: { tables: p }
|
|
159
168
|
};
|
|
160
|
-
},
|
|
169
|
+
}, E = async (t) => {
|
|
161
170
|
let f = t.flatMap((e) => e === "--json" ? ["--format", "json"] : [e]), p = a(f);
|
|
162
171
|
if ("error" in p) return console.error(`voltro check: ${p.error}`), 1;
|
|
163
|
-
let { process: m, target: h, rest:
|
|
164
|
-
for (let e = 0; e <
|
|
165
|
-
let [t, n] = (
|
|
166
|
-
if (t === "removeTable" && n)
|
|
172
|
+
let { process: m, target: h, rest: g } = s(p.rest), _;
|
|
173
|
+
for (let e = 0; e < g.length; e++) if (g[e] === "--diff") {
|
|
174
|
+
let [t, n] = (g[++e] ?? "").split(":");
|
|
175
|
+
if (t === "removeTable" && n) _ = {
|
|
167
176
|
kind: "removeTable",
|
|
168
177
|
name: n
|
|
169
178
|
};
|
|
170
|
-
else if (t === "removeProcedure" && n)
|
|
179
|
+
else if (t === "removeProcedure" && n) _ = {
|
|
171
180
|
kind: "removeProcedure",
|
|
172
181
|
tag: n
|
|
173
182
|
};
|
|
174
|
-
else if (t === "removeScope" && n)
|
|
183
|
+
else if (t === "removeScope" && n) _ = {
|
|
175
184
|
kind: "removeScope",
|
|
176
185
|
name: n
|
|
177
186
|
};
|
|
178
187
|
else return console.error("voltro check --diff: expected removeTable:<name> | removeProcedure:<tag> | removeScope:<name>"), 1;
|
|
179
188
|
}
|
|
180
|
-
let
|
|
189
|
+
let b = e(g, ["--diff"]) ?? process.cwd(), E = g.includes("--offline"), D = E ? [] : await o(m, h), O, k;
|
|
181
190
|
if (D.length === 0) {
|
|
182
191
|
if (!E && h !== void 0) return console.error("voltro check: --url given but no api reachable there."), 1;
|
|
183
192
|
try {
|
|
184
|
-
let e = await
|
|
193
|
+
let e = await T(d(b));
|
|
185
194
|
O = e.manifest, k = e.tables;
|
|
186
195
|
} catch (e) {
|
|
187
196
|
return console.error(`voltro check: could not assemble the graph from source: ${e.message}`), console.error(" Start `voltro dev` to check against a running api instead."), 1;
|
|
@@ -191,21 +200,21 @@ var m = (e) => e === void 0 ? [] : typeof e == "string" ? [e] : e, h = /* @__PUR
|
|
|
191
200
|
if (t.kind === "err") return console.error(`voltro check: manifest fetch failed: ${t.error}`), 1;
|
|
192
201
|
O = t.data, k = n.kind === "ok" ? n.data : { tables: [] };
|
|
193
202
|
}
|
|
194
|
-
let A =
|
|
203
|
+
let A = C(O, k), j = r(A, _), M = [...l(b).findings.map((e) => ({
|
|
195
204
|
rule: e.rule,
|
|
196
205
|
node: e.file,
|
|
197
206
|
file: e.file,
|
|
198
207
|
breaks: [e.detail],
|
|
199
208
|
fix: e.fix,
|
|
200
209
|
severity: e.severity
|
|
201
|
-
})), ...u(
|
|
210
|
+
})), ...u(b).map((e) => ({
|
|
202
211
|
rule: `page/${e.kind}`,
|
|
203
212
|
node: e.file,
|
|
204
213
|
file: e.file,
|
|
205
214
|
breaks: [e.kind === "suffixed-outside-pages" ? "a page.tsx outside src/pages/ never routes" : "no test beside it"],
|
|
206
215
|
fix: e.kind === "suffixed-outside-pages" ? "move it under src/pages/ into the directory whose route it serves, or rename it — page.tsx is reserved" : "write the test with the file — the name is derivable",
|
|
207
216
|
severity: e.kind === "suffixed-outside-pages" ? "error" : "warn"
|
|
208
|
-
}))], N = (await n(
|
|
217
|
+
}))], N = (await n(b, y(b))).map((e) => ({
|
|
209
218
|
rule: "client/not-browser-safe",
|
|
210
219
|
node: e.file,
|
|
211
220
|
file: e.file,
|
|
@@ -216,7 +225,7 @@ var m = (e) => e === void 0 ? [] : typeof e == "string" ? [e] : e, h = /* @__PUR
|
|
|
216
225
|
...j.diagnostics,
|
|
217
226
|
...M,
|
|
218
227
|
...N
|
|
219
|
-
], F = j.ok && !M.some((e) => e.severity === "error") && N.length === 0, I = await
|
|
228
|
+
], F = j.ok && !M.some((e) => e.severity === "error") && N.length === 0, I = await x(d(b)), L = I === null ? null : v(A.procedures, I);
|
|
220
229
|
return p.format === "json" ? i(L === null ? {
|
|
221
230
|
...j,
|
|
222
231
|
ok: F,
|
|
@@ -226,7 +235,7 @@ var m = (e) => e === void 0 ? [] : typeof e == "string" ? [e] : e, h = /* @__PUR
|
|
|
226
235
|
ok: F,
|
|
227
236
|
diagnostics: P,
|
|
228
237
|
observed: L
|
|
229
|
-
}) : (console.log(
|
|
238
|
+
}) : (console.log(w(P)), L !== null && console.log(S(L)), console.log(F ? "\nvoltro check: OK" : "\nvoltro check: FAILED (errors above)")), +!F;
|
|
230
239
|
};
|
|
231
240
|
//#endregion
|
|
232
|
-
export {
|
|
241
|
+
export { E as n, C as r, T as t };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { B as e, Sn as t, l as n, u as r, xn as i } from "./dev-
|
|
2
|
-
import { dt as a } from "./webDev-
|
|
1
|
+
import { B as e, Sn as t, l as n, u as r, xn as i } from "./dev-C0cbqysu.js";
|
|
2
|
+
import { dt as a } from "./webDev-BbdkzMbd.js";
|
|
3
3
|
import { n as o } from "./frameworkTableAssembly-BwJVEKLr.js";
|
|
4
4
|
import { i as s, r as c } from "./codegen-CrMXs4hb.js";
|
|
5
5
|
import { basename as l, join as u, relative as d, resolve as f } from "node:path";
|