@ultimat3/cli 22.15.0 → 23.0.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/CLAUDE.md +23 -8
- package/README.md +28 -5
- package/package.json +29 -28
- package/src/admin-catalog-keys.ts +16 -0
- package/src/admin-registration.ts +192 -0
- package/src/api-registration.ts +36 -11
- package/src/app-boundaries.ts +6 -20
- package/src/app-load.ts +55 -11
- package/src/app-manifest.ts +14 -0
- package/src/app-permissions.ts +0 -0
- package/src/app-reload-graph.ts +7 -30
- package/src/app-transport.ts +219 -0
- package/src/boundary-findings.ts +16 -4
- package/src/browser-transport.ts +160 -0
- package/src/budgets.ts +25 -1
- package/src/cmd-build-spec.ts +10 -3
- package/src/cmd-build.ts +56 -9
- package/src/cmd-db.ts +60 -11
- package/src/cmd-doctor.ts +35 -11
- package/src/cmd-generate.ts +118 -17
- package/src/cmd-jobs-spec.ts +19 -3
- package/src/cmd-jobs.ts +120 -3
- package/src/cmd-new.ts +15 -1
- package/src/cmd-routes.ts +15 -1
- package/src/cmd-secrets-ring.ts +76 -0
- package/src/cmd-secrets-spec.ts +10 -2
- package/src/cmd-secrets.ts +48 -12
- package/src/cmd-tasks.ts +84 -15
- package/src/cmd-verify.ts +10 -2
- package/src/comment-safe.ts +16 -0
- package/src/coverage-floor.ts +334 -0
- package/src/coverage-lcov.ts +193 -0
- package/src/coverage-source.ts +136 -0
- package/src/db-object-drift.ts +30 -0
- package/src/db-replay-engine.ts +115 -0
- package/src/db-schema-dump.ts +150 -0
- package/src/db-schema-refresh.ts +122 -0
- package/src/dev-dashboard.ts +2 -2
- package/src/dev-route-table.ts +9 -0
- package/src/doctor-coverage.ts +39 -0
- package/src/doctor-guards.ts +54 -0
- package/src/doctor-sealed.ts +132 -0
- package/src/document-graph.ts +82 -0
- package/src/error-codes.ts +32 -0
- package/src/framework-schema.ts +15 -0
- package/src/generate-files.ts +76 -10
- package/src/generate-grant-findings.ts +76 -0
- package/src/generate-grants.ts +77 -3
- package/src/generate-kinds.ts +19 -0
- package/src/generate-write.ts +15 -27
- package/src/generated-imports.ts +59 -0
- package/src/guard-sources.ts +104 -0
- package/src/guards.ts +23 -7
- package/src/handle-registration.ts +217 -0
- package/src/i18n-registration.ts +36 -0
- package/src/image-prepare.ts +51 -8
- package/src/import-closure.ts +165 -0
- package/src/index.ts +11 -2
- package/src/island-store.ts +24 -16
- package/src/jobs-drain.ts +14 -7
- package/src/jobs-json.ts +50 -0
- package/src/jobs-report.ts +3 -0
- package/src/json-merge.ts +46 -0
- package/src/mcp-errors.ts +29 -1
- package/src/messages.ts +33 -0
- package/src/metrics-endpoint.ts +71 -24
- package/src/migration-extensions.ts +29 -0
- package/src/module-imports.ts +47 -0
- package/src/output.ts +18 -9
- package/src/permission-grants.ts +41 -9
- package/src/publish-closure.ts +101 -0
- package/src/role-load.ts +143 -0
- package/src/role-start.ts +18 -1
- package/src/role-wake.ts +45 -0
- package/src/runtime-admin.ts +141 -0
- package/src/runtime-hooks.ts +5 -2
- package/src/runtime-queue.ts +8 -1
- package/src/runtime-render.ts +6 -0
- package/src/runtime-services.ts +2 -1
- package/src/scaffold-typecheck.ts +1 -0
- package/src/schema-dump-drift.ts +64 -0
- package/src/serve-boot.ts +85 -157
- package/src/serve-prebuilt-paths.ts +19 -0
- package/src/serve-prebuilt.ts +84 -0
- package/src/serve-web.ts +164 -0
- package/src/serve.ts +6 -9
- package/src/server-barrels.ts +54 -0
- package/src/source-edit.ts +49 -0
- package/src/tasks-facts.ts +4 -1
- package/src/templates/action.ts +144 -11
- package/src/templates/admin-catalog.ts +58 -0
- package/src/templates/admin-page.ts +36 -8
- package/src/templates/admin.ts +57 -17
- package/src/templates/backfill.ts +121 -87
- package/src/templates/entity.ts +140 -44
- package/src/templates/guard-animated-layout-property.ts +4 -11
- package/src/templates/guard-bare-error.ts +9 -13
- package/src/templates/guard-focus-visible.ts +4 -11
- package/src/templates/guard-image-dimensions.ts +6 -6
- package/src/templates/guard-island-without-states.ts +2 -4
- package/src/templates/guard-raw-breakpoint.ts +210 -0
- package/src/templates/guard-raw-colour.ts +4 -11
- package/src/templates/guard-raw-length.ts +375 -0
- package/src/templates/guard-raw-motion.ts +213 -0
- package/src/templates/guard-raw-shadow.ts +187 -0
- package/src/templates/guard-raw-z-index.ts +166 -0
- package/src/templates/guard-repo-raw-sql.ts +345 -0
- package/src/templates/guard-semantic-interactive.ts +6 -6
- package/src/templates/guard-undeclared-custom-property.ts +217 -0
- package/src/templates/guard-undefined-style-class.ts +215 -0
- package/src/templates/guard-untranslated-string.ts +6 -6
- package/src/templates/guard-unzoned-date.ts +9 -13
- package/src/templates/index.ts +3 -2
- package/src/templates/island.ts +34 -51
- package/src/templates/job-body-test.ts +121 -0
- package/src/templates/job.ts +23 -8
- package/src/templates/policy.ts +16 -9
- package/src/templates/query.ts +242 -26
- package/src/templates/resource-create.ts +62 -18
- package/src/templates/resource-form-island.ts +58 -80
- package/src/templates/resource-page.ts +269 -0
- package/src/templates/resource-service.ts +96 -0
- package/src/templates/resource.ts +86 -61
- package/src/templates/route.ts +38 -29
- package/src/templates/scaffold-api.ts +55 -1
- package/src/templates/scaffold-app.ts +171 -51
- package/src/templates/scaffold-auth.ts +83 -19
- package/src/templates/scaffold-browser-client.ts +90 -0
- package/src/templates/scaffold-container.ts +16 -0
- package/src/templates/scaffold-dashboard-bare.ts +64 -2
- package/src/templates/scaffold-dashboard-example.ts +124 -31
- package/src/templates/scaffold-dashboard-shared.ts +1 -20
- package/src/templates/scaffold-dashboard.ts +1 -2
- package/src/templates/scaffold-db-client.ts +142 -0
- package/src/templates/scaffold-db-package.ts +90 -8
- package/src/templates/scaffold-docs.ts +50 -5
- package/src/templates/scaffold-guards.ts +54 -11
- package/src/templates/scaffold-http.ts +9 -7
- package/src/templates/scaffold-repo.ts +52 -2
- package/src/templates/scaffold-roles.ts +56 -9
- package/src/templates/scaffold-shell.ts +36 -70
- package/src/templates/scaffold-site.ts +45 -19
- package/src/templates/scaffold-ui-package.ts +18 -0
- package/src/templates/slice-foundation.ts +46 -8
- package/src/test-counts.ts +18 -2
- package/src/transport-calls.ts +105 -0
- package/src/unscanned-admin.ts +54 -0
- package/src/verify-checks.ts +26 -7
- package/src/verify-coverage-run.ts +190 -0
- package/src/verify-deadline.ts +256 -0
- package/src/verify-errors.ts +15 -6
- package/src/verify-floor.ts +36 -9
- package/src/verify-merge.ts +117 -24
- package/src/verify-progress.ts +25 -0
- package/src/verify-role-load-probe.ts +54 -0
- package/src/verify-role-load.ts +148 -0
- package/src/verify-run.ts +93 -35
- package/src/verify-shard.ts +20 -9
- package/src/verify-step.ts +42 -1
- package/src/verify-tests.ts +78 -6
- package/src/workspace-checks.ts +50 -8
- package/src/workspace-dep-edit.ts +127 -0
- package/src/cmd-dev-fixture.ts +0 -204
- package/src/dev-live-fixture.ts +0 -122
- package/src/policy-fixture.ts +0 -78
- package/src/realtime-browser-probe-fixture.ts +0 -9
- package/src/role-start-fixture.ts +0 -78
- package/src/scaffold-fixture.ts +0 -160
- package/src/templates/island-fixture.ts +0 -76
package/CLAUDE.md
CHANGED
|
@@ -44,17 +44,24 @@ Commands: `bun test packages/cli` (from the repo root — the test preload lives
|
|
|
44
44
|
|---|---|
|
|
45
45
|
| `verify-checks.ts` / `verify-step.ts` / `verify-run.ts` | the step list, the outcome shape, the run order (`BESIDE_SERIAL_SUITES`) |
|
|
46
46
|
| `verify-floor.ts` | `x.verify.json`: a floor step that ran zero tests is `X_VERIFY_SUITE_VANISHED` |
|
|
47
|
+
| `coverage-floor.ts` / `coverage-lcov.ts` / `coverage-source.ts` / `verify-coverage-run.ts` | an APP's `unit` step holds `coverage` in `x.verify.json` over its whole source tree. The suite runs as fixed slices, one plain `bun test` each — never `--parallel --coverage`, whose merge moves with the core count. `coverage-lcov.ts` is the ONE lcov reader; `scripts/coverage-gate.ts` reads through it. A shard defers to `verify-merge.ts` (`coverageRiders`); `doctor-coverage.ts` lists the excludes |
|
|
48
|
+
| `verify-deadline.ts` / `verify-progress.ts` | a step past its deadline is `X_VERIFY_STEP_TIMEOUT`: its children carry `ULTIMATE_VERIFY_STEP` and are killed by that tag; `--json` streams a line per finished step to stderr. A `fix:` names `ctx.command` — `bun run verify` at the framework root |
|
|
47
49
|
| `load-findings.ts` | module-load failures are reported once, by `manifest`; other steps point there |
|
|
48
50
|
| `boundary-findings.ts` / `app-boundaries.ts` / `boundary-cuts.ts` | surface + layer rules; a finding's `fix:` is the concrete cut |
|
|
49
|
-
| `
|
|
51
|
+
| `app-transport.ts` / `browser-transport.ts` / `transport-calls.ts` / `import-closure.ts` / `server-barrels.ts` | `boundaries`, built in: one browser transport. `app-transport.ts` reuses the files `readAppSources` read (no second walk), resolves through the app's tsconfig `paths` and workspace manifests, reads the app's own `packages/*` lazily, and STOPS at `node_modules`. `browser-transport.ts` is the pure rule — `scripts/browser-transport.ts` calls the same function with this repo's seam FILES. Never a `guards/` file: an app must not be able to delete it |
|
|
52
|
+
| `guards.ts` / `guard-sources.ts` | an app's `guards/*.ts`, discovered, run on `boundaries`, held to the error contract; every `check(root, sources)` shares ONE walk, read and Sass compile per file (`GuardSources`) |
|
|
53
|
+
| `templates/scaffold-guards.ts` | `SHIPPED` — the one list of shipped guards: `x new` writes all, `x g guard <name>` writes one, `doctor-guards.ts` lists the ones an app lacks (a listing, never a finding). A new one is a `templates/guard-<name>.ts` + a row + `guard-<name>.test.ts`; `scaffold-guards-style.test.ts` holds embedded scales to `@ultimat3/ui`, the framework's own sheets to the rules, and both tracked apps byte-equal to the templates |
|
|
50
54
|
| `error-contract.ts` / `ts-scan.ts` / `fix-scan.ts` / `fix-imports.ts` / `fix-command.ts` / `fix-path.ts` | the `errors` step: every `fix:` names a runnable command, call or existing file |
|
|
51
|
-
| `workspace-checks.ts` / `workspace-graph.ts` / `tsconfig-references.ts` | `package-shape`, `filesize` |
|
|
55
|
+
| `workspace-checks.ts` / `workspace-graph.ts` / `tsconfig-references.ts` / `publish-closure.ts` | `package-shape`, `filesize`. A published package's `files` negates `*.test.ts` and `*-fixture.ts`, and no entry point (`exports`, `bin`) may reach a `-fixture.ts` — `X_PACKAGE_SHAPE` names the file to rename |
|
|
52
56
|
| `app-permissions.ts` / `permission-grants.ts` | `policy`: every permission granted or required is declared, and every one required is granted by some role (`X_PERMISSION_UNGRANTED`) |
|
|
53
57
|
| `job-registration.ts` | `manifest`: no job or task under a positional `anonymous-*` name (`X_JOB_UNREGISTERED`) |
|
|
58
|
+
| `unscanned-admin.ts` | `manifest`: a `defineAdmin()` outside the app scan (`apps/*/src/`) is `X_ADMIN_UNSCANNED`, with the `git mv` that mounts it |
|
|
59
|
+
| `verify-role-load.ts` / `verify-role-load-probe.ts` | `manifest`: a child imports the app as a worker does, then the rest module by module; a registration only a document-reaching module makes is `X_ROLE_LOAD_INCOMPLETE` with the module and the import the API index lacks |
|
|
54
60
|
| `async-pages.ts` / `live-routes.ts` / `budgets.ts` | `budgets` riders: an async `Page` with no `load`, a live read no island imports, bytes per route |
|
|
55
61
|
| `app-agents-md.ts` / `app-env.ts` / `app-openapi.ts` / `app-manifest.ts` | `manifest` and `contract-diff` |
|
|
56
62
|
| `schema-drift.ts` / `drift.ts` / `db-destructive.ts` / `db-ungeneratable.ts` | `drift`: snapshot vs declarations, the source hash (core's `canonicalJson`), the header markers |
|
|
57
|
-
| `
|
|
63
|
+
| `schema-dump-drift.ts` | `drift`'s fourth rail, the only one that boots a database: committed `packages/db/schema/` vs a scratch replay, and that dump loaded back (`X_SCHEMA_DUMP_DRIFT`) |
|
|
64
|
+
| `i18n-registration.ts` / `i18n-audit.ts` / `admin-catalog-keys.ts` | `i18n`; a mounted admin's keys are `AdminApp.catalogKeys()`, asked of every app catalog — never re-derived here (`X_CATALOG_MISSING_KEYS`) |
|
|
58
65
|
| `error-unthrown.ts` | host check: a registered code nothing throws must say so |
|
|
59
66
|
|
|
60
67
|
### Generators (`x g`, `x new`)
|
|
@@ -63,21 +70,27 @@ Commands: `bun test packages/cli` (from the repo root — the test preload lives
|
|
|
63
70
|
|---|---|
|
|
64
71
|
| `cmd-generate.ts` / `generate-files.ts` / `generate-kinds.ts` / `generate-write.ts` | argv → pure file list → writes (conflict-checked, `merge: 'json'` / `'if-absent'`) |
|
|
65
72
|
| `generate-feature.ts` | a `--feature` naming no slice is `X_FEATURE_UNKNOWN`; nothing invents an entity |
|
|
66
|
-
| `generate-grants.ts` | a written `policy.ts` grants `:read` to `member`, `:write` to `admin` in `apps/web/shared/roles.ts` |
|
|
73
|
+
| `generate-grants.ts` | a written `policy.ts` grants `:read` to `member`, `:write` to `admin` in `apps/web/shared/roles.ts`; a written `entity.ts` declares its table's `<table>:read\|write\|delete` there and grants all three to `admin` — what the admin asks for the screen that entity just became |
|
|
67
74
|
| `api-registration.ts` | a written job or task is listed in `apps/web/api/index.ts` |
|
|
75
|
+
| `handle-registration.ts` / `workspace-dep-edit.ts` / `templates/scaffold-db-client.ts` | a written entity joins `const entities = { … }` in `packages/db/src/client.ts` (the typed handle every generated `repo.ts` reads), with the two manifest lines the new imports need; no anchor is `X_DB_HANDLE_UNREGISTERED` carrying the lines. `cmd-generate.ts`'s `nextSteps` prints what the new table owes |
|
|
76
|
+
| `generated-imports.ts` | every sibling workspace a written file imports is declared in the manifest it landed under (`x g admin:page`'s `@<app>/i18n` in `apps/admin`) — `package-shape` would otherwise refuse the generator's own output (`X_WORKSPACE_DEP_UNDECLARED`) |
|
|
77
|
+
| `admin-registration.ts` / `templates/admin-catalog.ts` / `source-edit.ts` | `--admin` lists the override it wrote under `resources:` in `apps/admin/app/admin/admin.ts` (`X_ADMIN_RESOURCE_UNWIRED` when it cannot); every generated entity ships the `admin.<table>.*` labels its screen reads; `source-edit.ts` is the keyed-line and sorted-import edit both registrars share |
|
|
68
78
|
| `app-artifacts.ts` | `x.manifest.json` + `openapi.json`, written together by `x g` and `x manifest` |
|
|
69
|
-
| `templates/` | every emitted file; `scaffold-*.ts` for `x new`, one file per generator otherwise |
|
|
79
|
+
| `templates/` | every emitted file; `scaffold-*.ts` for `x new`, one file per generator otherwise. `templates/emitted-contract.test.ts` holds every emitted string to the scaffold's own Biome format and lint, its imports to what the scaffold installs, and its `fix:` lines to the error contract — run it after touching a template: `bun test packages/cli/src/templates/emitted-contract.test.ts` |
|
|
70
80
|
|
|
71
81
|
### Boot (`x dev`, the container)
|
|
72
82
|
|
|
73
83
|
| File | Job |
|
|
74
84
|
|---|---|
|
|
75
|
-
| `serve.ts` / `serve-boot.ts` / `serve-entry.ts` / `serve-env.ts` | the production entry: roles from `ROLE`, bindings from env, `ROLE=migrate` = `x db migrate` |
|
|
85
|
+
| `serve.ts` / `serve-boot.ts` / `serve-entry.ts` / `serve-env.ts` | the production entry: roles from `ROLE`, bindings from env, `ROLE=migrate` = `x db migrate`. `serve-boot.ts` (services + roles) is ONE `await import()` in `serveApp`, so `migrate` loads neither |
|
|
86
|
+
| `role-load.ts` / `document-graph.ts` / `serve-web.ts` | a role imports what it runs: `ROLE_LOADS` (declared over `Role`) — `worker`/`scheduler` import the API index and every module reaching no `.tsx`/stylesheet, verified against `x.manifest.json`'s jobs and tasks (`X_ROLE_LOAD_INCOMPLETE` imports everything); `serve-web.ts` is the web surface, behind `await import()`. `scanAppModules(root, { track: false })` is the container's scan — no reload graph, no error-code walk. Per-role module pins and `NEVER_AT_BOOT`: `serve-graph.test.ts` |
|
|
87
|
+
| `module-imports.ts` | the files a module's source imports, resolved once for both graphs: `referenced` (reload) and `evaluated` (document) |
|
|
76
88
|
| `cmd-dev.ts` / `dev-route-table.ts` | `x dev`: every role in one process, `/_x`, the watcher |
|
|
77
89
|
| `runtime-bindings.ts` | which service each binding points at (embedded or external); events follow `realtime.transport` / `urlEnv` |
|
|
78
90
|
| `runtime-queue.ts` / `runtime-services.ts` | the db + queue pair; everything else and every ambient accessor |
|
|
79
91
|
| `runtime-jobs.ts` / `runtime-realtime.ts` / `runtime-notify-retention.ts` / `runtime-cache.ts` / `runtime-purge.ts` / `runtime-replica.ts` | `app.config.ts` sections the boot obeys, and what each wires |
|
|
80
92
|
| `role-start.ts` / `role-start-types.ts` / `role-realtime.ts` | `--role` selection and start/stop for `web`, `sync`, `worker`, `scheduler`; `realtime.enabled: false` drops `sync` and `replicator` |
|
|
93
|
+
| `role-wake.ts` | the worker's LISTEN session (`@ultimat3/jobs`' `startQueueWake`) — worker role only, only over a client that `canListen` — and `queue_wake_live` |
|
|
81
94
|
| `role-sync.ts` / `role-replicator.ts` / `runtime-live-feed.ts` | the sync node, the change feed |
|
|
82
95
|
| `runtime-render.ts` / `runtime-assets.ts` / `runtime-storage.ts` / `runtime-hooks.ts` / `api-routes.ts` | the HTTP surface: pages, `/icons` + `/media`, `/_storage`, authz, the app's API — plus its bearer mounts (`apiMountRoutes`) and pages' bound `POST`s (`pagePostRoutes`), mounted by BOTH boots |
|
|
83
96
|
| `runtime-overrides.ts` | the one field a host hands the framework a driver through — and `routes`, the plain routes for a wire format no primitive speaks (OAuth token endpoint) |
|
|
@@ -86,17 +99,19 @@ Commands: `bun test packages/cli` (from the repo root — the test preload lives
|
|
|
86
99
|
| `page-navigation.ts` | the client router for `navigation.client` surfaces — built, routed and named ONCE for `x dev`, the container and the static export; none opted in builds nothing |
|
|
87
100
|
| `page-speculation.ts` | Speculation Rules for documents WITHOUT the router (`navigation.speculation`): the allow-list of pure-read pages, the one tag, and its `script-src` hash — composed once for all three writers. An allow-list, never "every link but": a prefetch is a real GET with cookies |
|
|
88
101
|
| `static-document.ts` | a served `static` page: `304` on a matching `If-None-Match`, and (container only, `memoStatic`) the document kept after its first render. Never `x dev` — a save moves the stylesheet URL the kept document names |
|
|
89
|
-
| `island-bundle.ts` / `island-store.ts` / `island-realtime.ts` / `solid-loader.ts` | islands: one `Bun.build` each, source-addressed;
|
|
102
|
+
| `island-bundle.ts` / `island-store.ts` / `island-realtime.ts` / `solid-loader.ts` | islands: one `Bun.build` each, source-addressed; the image build writes a verified store the container loads |
|
|
103
|
+
| `serve-prebuilt.ts` / `serve-prebuilt-paths.ts` | the image's prebuilt store (`node_modules/.cache/ultimate`, never `.x/` — state, tmpfs): every role adopts its compiled stylesheets before importing the app; a boot that built an island or ran Sass logs `X_IMAGE_NOT_PREBUILT` once |
|
|
90
104
|
| `dev-*.ts` | `x dev` only: dashboard sources, traces, the N+1 ledger, the watcher, the reload, the lock, the port |
|
|
91
105
|
|
|
92
106
|
### Build and data
|
|
93
107
|
|
|
94
108
|
| File | Job |
|
|
95
109
|
|---|---|
|
|
96
|
-
| `cmd-build.ts` / `image-prepare.ts` | `x build`: static gate first; docker stamps `BUILD_ID` and
|
|
110
|
+
| `cmd-build.ts` / `image-prepare.ts` | `x build`: static gate first; `docker` stamps `BUILD_ID`; `prebuilt` is the Dockerfile's own `RUN` — islands and stylesheets, written by the image's Bun at the image's paths, no gate and no subprocess |
|
|
97
111
|
| `prerender.ts` / `measure-scope.ts` / `static-report.ts` | the static export; routes rendered only to weigh run inside a request as core's `measurementActor()`, with the app's API answered in process (`withInProcessFetch`) |
|
|
98
112
|
| `sw-artifacts.ts` / `pwa-artifacts.ts` / `favicon.ts` | the service worker, the web manifest, `/favicon.ico` |
|
|
99
113
|
| `cmd-db.ts` / `migrations.ts` / `db-generate.ts` / `db-branch.ts` / `db-seed.ts` / `db-backfill.ts` / `db-subscribes.ts` / `db-accept-created.ts` | one migration engine for `x db` and `ROLE=migrate` |
|
|
114
|
+
| `db-schema-dump.ts` / `db-schema-refresh.ts` / `db-replay-engine.ts` / `migration-extensions.ts` / `db-object-drift.ts` | the schema dump: replay on a scratch database (embedded, snapshot-cached under `.x/cache`; a real Postgres only when a migration creates an extension PGlite cannot link), written by `x db gen` and `x db migrate`. `migration-extensions.ts` is also what `runtime-queue.ts` links `x dev`'s database with |
|
|
100
115
|
|
|
101
116
|
### Introspection and tools
|
|
102
117
|
|
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ Commands and the `x verify` step count, `As of 2026-08`:
|
|
|
11
11
|
| `x new <name>` | scaffolds the monorepo | interactive-free; auth, seeded DB, example route |
|
|
12
12
|
| `x dev` | every role in one process | embedded Postgres/events/storage, `/_x` mounted |
|
|
13
13
|
| `x build --target docker\|binary\|static` | one artifact | `ROLE` selects behaviour at start |
|
|
14
|
+
| `x build --target prebuilt` | the store a container boots from: island chunks, compiled stylesheets | a `RUN` line in the image build, never run by hand |
|
|
14
15
|
| `x verify` | **the gate** | 20 named steps, each with pass/fail + duration |
|
|
15
16
|
| `x g <primitive> <name>` | scaffolds a primitive **with a passing test** | never a TODO stub |
|
|
16
17
|
| `x db gen\|migrate\|reset\|branch\|backfill` | everything DB | `branch` = copy-on-write clone + preview URL; `backfill` dry-runs unless `--write`. `x db studio` is **planned** — it parses, and exits `X_NOT_IMPLEMENTED` naming `/_x`'s db panel |
|
|
@@ -91,11 +92,30 @@ than a skip.
|
|
|
91
92
|
"Nothing" is both ways a suite disappears — no files at all, and every test in the files it found
|
|
92
93
|
skipping itself, which is read back out of `bun test`'s own summary. `x new` writes one.
|
|
93
94
|
|
|
95
|
+
The same file states the app's **coverage floor**: `"coverage": { "lines": 95, "funcs": 95 }`,
|
|
96
|
+
held by the `unit` step over every `.ts`/`.tsx` under `apps/` and `packages/`, a file no unit test
|
|
97
|
+
loads counted at 0%. No `coverage` is `X_COVERAGE_FLOOR_UNSTATED` (the finding carries the line to
|
|
98
|
+
add, with the measured numbers), under it is `X_COVERAGE_BELOW_FLOOR` naming the ten files losing
|
|
99
|
+
the most lines, and a floor under 95 the tree has left 1.5 points behind is
|
|
100
|
+
`X_COVERAGE_FLOOR_STALE` — the floor only rises. An `exclude` entry names code a unit test cannot
|
|
101
|
+
execute and carries its `why`; `x doctor` prints them. A `--shard` slice hands its facts to
|
|
102
|
+
`x verify merge`, which judges the fold. `x new` writes 95 / 95.
|
|
103
|
+
|
|
104
|
+
A step has a **deadline** — 8 minutes for the six suites, 5 for every other step,
|
|
105
|
+
`"stepTimeoutMs": { "<step>": ms }` in `x.verify.json` to change one. Past it the step fails by
|
|
106
|
+
name with `X_VERIFY_STEP_TIMEOUT` and every process it started is killed. Under `--json` each
|
|
107
|
+
finished step is one line on stderr (`{"step":"lint","ok":true,"ms":21987}`); stdout stays the
|
|
108
|
+
single document.
|
|
109
|
+
|
|
94
110
|
An app extends the gate with its own conventions, never with its own step: a file in `guards/`
|
|
95
|
-
exports a `guard` whose `check(root)` returns `Finding[]`, and the `boundaries` step runs
|
|
96
|
-
of them.
|
|
111
|
+
exports a `guard` whose `check(root, sources)` returns `Finding[]`, and the `boundaries` step runs
|
|
112
|
+
every one of them. `sources` is the run's ONE read of the app (`GuardSources`): `files(glob)` is
|
|
113
|
+
walked and read once however many guards ask, and `compiled(path)` is a stylesheet as the build
|
|
114
|
+
compiles it. Nothing registers a guard — the directory is the registration — and what a guard returns
|
|
97
115
|
is held to the same error contract shipped source is (`X_GUARD_INVALID`, `X_GUARD_FAILED`,
|
|
98
|
-
`X_GUARD_FINDING_INVALID`). `x g guard <name>` scaffolds one with its test
|
|
116
|
+
`X_GUARD_FINDING_INVALID`). `x g guard <name>` scaffolds one with its test — the shipped guard of
|
|
117
|
+
that name where there is one (`x new` writes all seventeen), the blank template otherwise — and
|
|
118
|
+
`x doctor` lists the shipped ones an app does not hold.
|
|
99
119
|
|
|
100
120
|
## Layout
|
|
101
121
|
|
|
@@ -111,6 +131,7 @@ is held to the same error contract shipped source is (`X_GUARD_INVALID`, `X_GUAR
|
|
|
111
131
|
| `registry.ts` | the one command list |
|
|
112
132
|
| `generate-kinds.ts` | which generators exist, and how a command line names one |
|
|
113
133
|
| `guards.ts` | the app's own conventions: `guards/` discovered, run, and held to the error contract |
|
|
134
|
+
| `guard-sources.ts` | one walk, one read and one Sass compile per file per gate run, shared by every guard |
|
|
114
135
|
| `cmd-*.ts` | one command group each |
|
|
115
136
|
| `templates/` | scaffolding as typed string modules, not copied fixtures |
|
|
116
137
|
| `app-load.ts` | import an app's modules so the framework registries hold it |
|
|
@@ -122,10 +143,12 @@ is held to the same error contract shipped source is (`X_GUARD_INVALID`, `X_GUAR
|
|
|
122
143
|
| `app-manifest.ts` | `x.manifest.json`, projected by `@ultimat3/manifest` |
|
|
123
144
|
| `app-openapi.ts` | `openapi.json`, projected by `@ultimat3/action` |
|
|
124
145
|
| `app-boundaries.ts` | app import boundaries, over `@ultimat3/render`'s surface check |
|
|
146
|
+
| `app-transport.ts` / `browser-transport.ts` | one browser transport, on `boundaries` in every app: a raw `fetch(`, `new WebSocket(` or `new XMLHttpRequest(` in an island's closure is `X_BROWSER_TRANSPORT_BYPASS`; a server barrel there is `X_BROWSER_SERVER_BARREL` |
|
|
125
147
|
| `app-agents-md.ts` | `AGENTS.md` exists and stays short, over `@ultimat3/manifest`'s check |
|
|
126
148
|
| `serve.ts` | **what a container starts** — `runRole(options)`, the same boot `x dev` runs minus the watcher, `/_x` and `dev: true`. `x new`'s `apps/web/server.ts` is three lines that call it. `ROLE`, `PORT` and `HOST` are read from `env`; `role`, `port` and `hostname` on `ServeOptions` override each |
|
|
127
149
|
| `prerender.ts` | `x build --target static`: which `site/` routes qualify, and where the bytes land |
|
|
128
|
-
| `metrics-endpoint.ts` | the `METRICS_PATH` scrape listener every role opens, on `METRICS_PORT` |
|
|
150
|
+
| `metrics-endpoint.ts` | the `METRICS_PATH` scrape listener every role opens, on `METRICS_PORT`; opening it starts the `process_*` series, labelled by `startMetricsEndpoint({ role })` |
|
|
151
|
+
| `role-load.ts` / `document-graph.ts` / `serve-web.ts` | what each container role imports: `web`, `sync` and `replicator` every app module; `worker` and `scheduler` the API index plus every module that reaches no component or stylesheet |
|
|
129
152
|
| `otlp-export.ts` | the exporters `OTEL_EXPORTER_OTLP_ENDPOINT` switches on, and their drain hooks |
|
|
130
153
|
| `dev-*.ts` | what `x dev` boots: services, runtime, routes, hooks, roles, the `/_x` mount |
|
|
131
154
|
| `island-bundle.ts` | every `*.island.tsx` built as its own entry point, content-hashed |
|
|
@@ -150,7 +173,7 @@ apps/web/app/<feature>/{entity,repo,service,policy,errors,ui}.ts
|
|
|
150
173
|
apps/web/app/<feature>/{actions,queries,live,jobs,tasks}/<name>.ts
|
|
151
174
|
apps/web/{site,app}/<path>/page.tsx
|
|
152
175
|
apps/web/{site,app}/<path>/<name>.island.tsx # x g island <name> --at <dir>
|
|
153
|
-
apps/admin/
|
|
176
|
+
apps/admin/app/admin/pages/<name>.tsx # x g admin:page <name> --permission p
|
|
154
177
|
guards/<name>.ts # x g guard <name>
|
|
155
178
|
```
|
|
156
179
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "23.0.0",
|
|
4
4
|
"description": "The `x` binary: new, dev, build, verify, generate, db, mcp, doctor, deploy",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"files": [
|
|
24
24
|
"src",
|
|
25
25
|
"!src/**/*.test.ts",
|
|
26
|
+
"!src/**/*-fixture.ts",
|
|
26
27
|
"types",
|
|
27
28
|
"CLAUDE.md",
|
|
28
29
|
"README.md",
|
|
@@ -38,33 +39,33 @@
|
|
|
38
39
|
},
|
|
39
40
|
"dependencies": {
|
|
40
41
|
"@babel/core": "^8.0.1",
|
|
41
|
-
"@ultimat3/action": "
|
|
42
|
-
"@ultimat3/admin": "
|
|
43
|
-
"@ultimat3/ai": "
|
|
44
|
-
"@ultimat3/auth": "
|
|
45
|
-
"@ultimat3/cache": "
|
|
46
|
-
"@ultimat3/core": "
|
|
47
|
-
"@ultimat3/db": "
|
|
48
|
-
"@ultimat3/entity": "
|
|
49
|
-
"@ultimat3/flags": "
|
|
50
|
-
"@ultimat3/http": "
|
|
51
|
-
"@ultimat3/i18n": "
|
|
52
|
-
"@ultimat3/jobs": "
|
|
53
|
-
"@ultimat3/mail": "
|
|
54
|
-
"@ultimat3/manifest": "
|
|
55
|
-
"@ultimat3/mcp": "
|
|
56
|
-
"@ultimat3/money": "
|
|
57
|
-
"@ultimat3/notify": "
|
|
58
|
-
"@ultimat3/policy": "
|
|
59
|
-
"@ultimat3/pwa": "
|
|
60
|
-
"@ultimat3/query": "
|
|
61
|
-
"@ultimat3/realtime": "
|
|
62
|
-
"@ultimat3/render": "
|
|
63
|
-
"@ultimat3/schema": "
|
|
64
|
-
"@ultimat3/seo": "
|
|
65
|
-
"@ultimat3/storage": "
|
|
66
|
-
"@ultimat3/testing": "
|
|
67
|
-
"@ultimat3/time": "
|
|
42
|
+
"@ultimat3/action": "23.0.0",
|
|
43
|
+
"@ultimat3/admin": "23.0.0",
|
|
44
|
+
"@ultimat3/ai": "23.0.0",
|
|
45
|
+
"@ultimat3/auth": "23.0.0",
|
|
46
|
+
"@ultimat3/cache": "23.0.0",
|
|
47
|
+
"@ultimat3/core": "23.0.0",
|
|
48
|
+
"@ultimat3/db": "23.0.0",
|
|
49
|
+
"@ultimat3/entity": "23.0.0",
|
|
50
|
+
"@ultimat3/flags": "23.0.0",
|
|
51
|
+
"@ultimat3/http": "23.0.0",
|
|
52
|
+
"@ultimat3/i18n": "23.0.0",
|
|
53
|
+
"@ultimat3/jobs": "23.0.0",
|
|
54
|
+
"@ultimat3/mail": "23.0.0",
|
|
55
|
+
"@ultimat3/manifest": "23.0.0",
|
|
56
|
+
"@ultimat3/mcp": "23.0.0",
|
|
57
|
+
"@ultimat3/money": "23.0.0",
|
|
58
|
+
"@ultimat3/notify": "23.0.0",
|
|
59
|
+
"@ultimat3/policy": "23.0.0",
|
|
60
|
+
"@ultimat3/pwa": "23.0.0",
|
|
61
|
+
"@ultimat3/query": "23.0.0",
|
|
62
|
+
"@ultimat3/realtime": "23.0.0",
|
|
63
|
+
"@ultimat3/render": "23.0.0",
|
|
64
|
+
"@ultimat3/schema": "23.0.0",
|
|
65
|
+
"@ultimat3/seo": "23.0.0",
|
|
66
|
+
"@ultimat3/storage": "23.0.0",
|
|
67
|
+
"@ultimat3/testing": "23.0.0",
|
|
68
|
+
"@ultimat3/time": "23.0.0",
|
|
68
69
|
"babel-preset-solid": "^1.9.15"
|
|
69
70
|
}
|
|
70
71
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// The catalog keys a MOUNTED admin renders: asked of each `AdminApp` (`catalogKeys()`), never
|
|
2
|
+
// walked here — the admin owns the one derivation, so a key `defineAdmin` would draw and the
|
|
3
|
+
// `i18n` step audits can never be two spellings of one rule.
|
|
4
|
+
|
|
5
|
+
import { describeRoutes } from '@ultimat3/render';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The keys of every admin this process declared. Asked of the route table first: an app with no
|
|
9
|
+
* admin never loads `@ultimat3/admin` (axiom 6), and one that has one has loaded it already, so
|
|
10
|
+
* the dynamic import below is a cache hit.
|
|
11
|
+
*/
|
|
12
|
+
export async function mountedAdminKeys(): Promise<readonly string[]> {
|
|
13
|
+
if (!describeRoutes().some((route) => route.mount?.by === 'defineAdmin')) return [];
|
|
14
|
+
const { adminMounts } = await import('@ultimat3/admin');
|
|
15
|
+
return [...new Set(adminMounts().flatMap((app) => app.catalogKeys()))].sort();
|
|
16
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// `x g resource --admin` wires the override it wrote. The entity is already an admin screen — it
|
|
2
|
+
// is in the handle `defineAdmin({ entities: adminEntitiesOf(db) })` reads — so the override file is
|
|
3
|
+
// the ONLY thing `--admin` adds, and an override nothing lists under `resources:` is a dead file.
|
|
4
|
+
// Performed here; where the admin is not a `defineAdmin({ … })` this can add to, the finding names
|
|
5
|
+
// the two lines.
|
|
6
|
+
|
|
7
|
+
import { ERROR_DOCS_URL, stripComments } from '@ultimat3/core';
|
|
8
|
+
import { containedPath } from './generate-write';
|
|
9
|
+
import type { Finding } from './output';
|
|
10
|
+
import { holdsKey, withKeyedLine, withSortedImport } from './source-edit';
|
|
11
|
+
import type { GeneratedFile } from './templates/naming';
|
|
12
|
+
import { declareWorkspaceDependency, readWorkspace } from './workspace-dep-edit';
|
|
13
|
+
|
|
14
|
+
/** The scaffold's one admin declaration. */
|
|
15
|
+
export const ADMIN_FILE = 'apps/admin/app/admin/admin.ts';
|
|
16
|
+
const ADMIN_WORKSPACE = 'apps/admin';
|
|
17
|
+
const WEB_WORKSPACE = 'apps/web';
|
|
18
|
+
|
|
19
|
+
const OVERRIDE_PATH = /^apps\/web\/(?<rest>[^/]+\/[^/]+)\/admin\/resource\.ts$/;
|
|
20
|
+
const OVERRIDE_EXPORT = /\bexport const ([A-Za-z_$][\w$]*AdminResource)\b/;
|
|
21
|
+
const ENTITY_NAME = /\bentity\(\s*'([^']+)'/;
|
|
22
|
+
|
|
23
|
+
/** One override to list: `resources: { <table>: <binding> }`. */
|
|
24
|
+
export interface AdminResourceEntry {
|
|
25
|
+
/** The entity's own name — the key `defineAdmin` resolves an override by. */
|
|
26
|
+
readonly table: string;
|
|
27
|
+
/** The override's exported binding: `widgetAdminResource`. */
|
|
28
|
+
readonly binding: string;
|
|
29
|
+
readonly specifier: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const importLine = (entry: AdminResourceEntry): string =>
|
|
33
|
+
`import { ${entry.binding} } from '${entry.specifier}';`;
|
|
34
|
+
|
|
35
|
+
const resourceLine = (entry: AdminResourceEntry): string => ` ${entry.table}: ${entry.binding},`;
|
|
36
|
+
|
|
37
|
+
export const adminResourceUnwiredFinding = (
|
|
38
|
+
entry: AdminResourceEntry,
|
|
39
|
+
reason: string,
|
|
40
|
+
): Finding => ({
|
|
41
|
+
code: 'X_ADMIN_RESOURCE_UNWIRED',
|
|
42
|
+
cause: `${ADMIN_FILE} ${reason}, so the override ${entry.binding} is read by nothing and /admin/${entry.table} keeps its derived defaults`,
|
|
43
|
+
fix: `add ${importLine(entry)} and "resources: { ${entry.table}: ${entry.binding} }" to the defineAdmin() call in ${ADMIN_FILE}`,
|
|
44
|
+
docs: ERROR_DOCS_URL,
|
|
45
|
+
at: ADMIN_FILE,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/** Past the brace that closes the one opening at `open`, read off comment-masked text. */
|
|
49
|
+
function closeOf(masked: string, open: number): number {
|
|
50
|
+
let depth = 0;
|
|
51
|
+
for (let index = open; index < masked.length; index += 1) {
|
|
52
|
+
if (masked[index] === '{') depth += 1;
|
|
53
|
+
if (masked[index] === '}') {
|
|
54
|
+
depth -= 1;
|
|
55
|
+
if (depth === 0) return index;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return -1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* `source` with each override imported and listed under `resources:`, which is created — last in
|
|
63
|
+
* the call — when the declaration has none. Positions are found on the comment-masked text: the
|
|
64
|
+
* scaffold's own comment says `resources: { <entity>: … }`, and that is prose, not the key.
|
|
65
|
+
*/
|
|
66
|
+
export function insertAdminResources(
|
|
67
|
+
source: string,
|
|
68
|
+
entries: readonly AdminResourceEntry[],
|
|
69
|
+
): { readonly source: string; readonly missing: readonly AdminResourceEntry[] } {
|
|
70
|
+
let next = source;
|
|
71
|
+
const missing: AdminResourceEntry[] = [];
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
const masked = stripComments(next);
|
|
74
|
+
const call = /\bdefineAdmin\(\s*\{/.exec(masked);
|
|
75
|
+
const open = call === null ? -1 : call.index + call[0].length - 1;
|
|
76
|
+
const close = open === -1 ? -1 : closeOf(masked, open);
|
|
77
|
+
if (open === -1 || close === -1) {
|
|
78
|
+
missing.push(entry);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const key = /\n[ \t]*resources:\s*\{/.exec(masked.slice(open, close));
|
|
82
|
+
if (key === null) {
|
|
83
|
+
// On its own rows before the call's closing brace, after whatever the author wrote last.
|
|
84
|
+
const at = next.lastIndexOf('\n', close) + 1;
|
|
85
|
+
const block = ` resources: {\n${resourceLine(entry)}\n },\n`;
|
|
86
|
+
next = `${next.slice(0, at)}${block}${next.slice(at)}`;
|
|
87
|
+
} else {
|
|
88
|
+
const bodyOpen = open + key.index + key[0].length;
|
|
89
|
+
const bodyClose = closeOf(masked, bodyOpen - 1);
|
|
90
|
+
const body = next.slice(bodyOpen, bodyClose);
|
|
91
|
+
if (holdsKey(stripComments(body), entry.table)) continue;
|
|
92
|
+
const listed = withKeyedLine(body, entry.table, resourceLine(entry), ' ');
|
|
93
|
+
next = `${next.slice(0, bodyOpen)}${listed}${next.slice(bodyClose)}`;
|
|
94
|
+
}
|
|
95
|
+
next = withSortedImport(next, importLine(entry), entry.specifier);
|
|
96
|
+
}
|
|
97
|
+
return { source: next, missing };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The entry an override file implies, given the source of the entity beside it. */
|
|
101
|
+
function entryOf(
|
|
102
|
+
path: string,
|
|
103
|
+
override: string,
|
|
104
|
+
entity: string | undefined,
|
|
105
|
+
webModule: string,
|
|
106
|
+
): AdminResourceEntry | undefined {
|
|
107
|
+
const rest = OVERRIDE_PATH.exec(path)?.groups?.['rest'];
|
|
108
|
+
const binding = OVERRIDE_EXPORT.exec(override)?.[1];
|
|
109
|
+
const table = entity === undefined ? undefined : ENTITY_NAME.exec(entity)?.[1];
|
|
110
|
+
if (rest === undefined || binding === undefined || table === undefined) return undefined;
|
|
111
|
+
return { table, binding, specifier: `${webModule}/${rest}/admin/resource` };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const entityPathOf = (overridePath: string): string =>
|
|
115
|
+
overridePath.replace(/\/admin\/resource\.ts$/, '/entity.ts');
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The same wiring over a file LIST, for a caller that composes generators without a disk — the
|
|
119
|
+
* scaffold fixture compiles the result, which is what proves the override's type fits `resources:`.
|
|
120
|
+
*/
|
|
121
|
+
export function withAdminResources(
|
|
122
|
+
files: readonly GeneratedFile[],
|
|
123
|
+
webModule: string,
|
|
124
|
+
): readonly GeneratedFile[] {
|
|
125
|
+
const text = new Map(
|
|
126
|
+
files.flatMap((file) =>
|
|
127
|
+
typeof file.contents === 'string' ? [[file.path, file.contents] as const] : [],
|
|
128
|
+
),
|
|
129
|
+
);
|
|
130
|
+
const entries = [...text].flatMap(([path, contents]) => {
|
|
131
|
+
const entry = entryOf(path, contents, text.get(entityPathOf(path)), webModule);
|
|
132
|
+
return entry === undefined ? [] : [entry];
|
|
133
|
+
});
|
|
134
|
+
return files.map((file) =>
|
|
135
|
+
file.path === ADMIN_FILE && typeof file.contents === 'string' && file.merge === undefined
|
|
136
|
+
? { ...file, contents: insertAdminResources(file.contents, entries).source }
|
|
137
|
+
: file,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The overrides among the written paths, each named by what its file and its entity export. */
|
|
142
|
+
async function entriesFor(
|
|
143
|
+
root: string,
|
|
144
|
+
written: readonly string[],
|
|
145
|
+
webModule: string,
|
|
146
|
+
): Promise<readonly AdminResourceEntry[]> {
|
|
147
|
+
const entries: AdminResourceEntry[] = [];
|
|
148
|
+
for (const path of written) {
|
|
149
|
+
if (!OVERRIDE_PATH.test(path)) continue;
|
|
150
|
+
const entity = Bun.file(containedPath(root, entityPathOf(path)));
|
|
151
|
+
const entry = entryOf(
|
|
152
|
+
path,
|
|
153
|
+
await Bun.file(containedPath(root, path)).text(),
|
|
154
|
+
(await entity.exists()) ? await entity.text() : undefined,
|
|
155
|
+
webModule,
|
|
156
|
+
);
|
|
157
|
+
if (entry !== undefined) entries.push(entry);
|
|
158
|
+
}
|
|
159
|
+
return entries;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface AdminRegistration {
|
|
163
|
+
/** App-root-relative paths this rewrote. */
|
|
164
|
+
readonly edited: readonly string[];
|
|
165
|
+
readonly findings: readonly Finding[];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Performs `insertAdminResources` on the app's admin, and declares the edge the import is. */
|
|
169
|
+
export async function registerAdminResources(
|
|
170
|
+
root: string,
|
|
171
|
+
written: readonly string[],
|
|
172
|
+
): Promise<AdminRegistration> {
|
|
173
|
+
const web = await readWorkspace(root, WEB_WORKSPACE);
|
|
174
|
+
const entries = web === undefined ? [] : await entriesFor(root, written, web.name);
|
|
175
|
+
if (entries.length === 0) return { edited: [], findings: [] };
|
|
176
|
+
const file = Bun.file(containedPath(root, ADMIN_FILE));
|
|
177
|
+
if (!(await file.exists())) {
|
|
178
|
+
return {
|
|
179
|
+
edited: [],
|
|
180
|
+
findings: entries.map((entry) => adminResourceUnwiredFinding(entry, 'does not exist')),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const before = await file.text();
|
|
184
|
+
const { source, missing } = insertAdminResources(before, entries);
|
|
185
|
+
const findings = missing.map((entry) =>
|
|
186
|
+
adminResourceUnwiredFinding(entry, 'holds no defineAdmin({ … }) call to add to'),
|
|
187
|
+
);
|
|
188
|
+
if (source === before) return { edited: [], findings };
|
|
189
|
+
await Bun.write(containedPath(root, ADMIN_FILE), source);
|
|
190
|
+
const manifest = await declareWorkspaceDependency(root, ADMIN_WORKSPACE, WEB_WORKSPACE);
|
|
191
|
+
return { edited: manifest === undefined ? [ADMIN_FILE] : [ADMIN_FILE, manifest], findings };
|
|
192
|
+
}
|
package/src/api-registration.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Every generator hands the primitives it wrote to `apps/web/api/index.ts`. The scan registers
|
|
2
|
+
// actions and queries by export name on its own and registers NO job: a job module nothing lists
|
|
3
|
+
// keeps the positional `anonymous-job-2` that `job()` minted, on the queue row, in
|
|
4
4
|
// `x.manifest.json` and in every dead-letter trace — under a green gate (plan 101 slice 11 b).
|
|
5
|
+
// Actions and queries are listed for the TYPE: `Api` is what the browser's client is shaped from.
|
|
5
6
|
|
|
6
7
|
import { API_INDEX } from './app-root';
|
|
7
8
|
import { containedPath } from './generate-write';
|
|
@@ -10,7 +11,7 @@ import { wrapList } from './templates/wrap';
|
|
|
10
11
|
|
|
11
12
|
/** One module to import as a namespace and list under a `defineApi` key. */
|
|
12
13
|
export interface ApiEntry {
|
|
13
|
-
readonly key: 'jobs' | 'tasks';
|
|
14
|
+
readonly key: 'actions' | 'queries' | 'jobs' | 'tasks';
|
|
14
15
|
/** The namespace binding: the file's own name, camelCased — `reindexPost`. */
|
|
15
16
|
readonly binding: string;
|
|
16
17
|
/** From `apps/web/api/`: `../app/post/jobs/reindex-post`. */
|
|
@@ -18,17 +19,37 @@ export interface ApiEntry {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
const PRIMITIVE_PATH =
|
|
21
|
-
/^apps\/web\/(?<rest>[^/]+\/[^/]+\/(?<dir>jobs|tasks)\/(?<file>[a-z0-9-]+))\.ts$/;
|
|
22
|
+
/^apps\/web\/(?<rest>[^/]+\/[^/]+\/(?<dir>actions|queries|live|jobs|tasks)\/(?<file>[a-z0-9-]+))\.ts$/;
|
|
22
23
|
|
|
23
|
-
/** The
|
|
24
|
+
/** The order `x new` writes the lists in, which a list this adds keeps. */
|
|
25
|
+
const LIST_ORDER: readonly ApiEntry['key'][] = ['actions', 'queries', 'jobs', 'tasks'];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The `defineApi` list each generated directory belongs in. `actions/` holds mutators too, and a
|
|
29
|
+
* mutator IS an action — `defineApi` merges both lists into one registration — so one key serves.
|
|
30
|
+
*/
|
|
31
|
+
const KEY_OF: Readonly<Record<string, ApiEntry['key']>> = {
|
|
32
|
+
actions: 'actions',
|
|
33
|
+
queries: 'queries',
|
|
34
|
+
live: 'queries',
|
|
35
|
+
jobs: 'jobs',
|
|
36
|
+
tasks: 'tasks',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The primitive modules among the paths a generator wrote. Tests are not modules to list.
|
|
41
|
+
*
|
|
42
|
+
* Actions and queries too, though the scan registers those at boot without being told: `Api` —
|
|
43
|
+
* the type the browser's typed client is shaped from — is `typeof defineApi({ … })`, so one the
|
|
44
|
+
* index does not list is an endpoint the page has no typed call for.
|
|
45
|
+
*/
|
|
24
46
|
export function apiEntriesFor(written: readonly string[]): readonly ApiEntry[] {
|
|
25
47
|
return written.flatMap((path) => {
|
|
26
48
|
const groups = PRIMITIVE_PATH.exec(path)?.groups;
|
|
27
49
|
if (groups === undefined) return [];
|
|
28
50
|
const { rest = '', dir, file = '' } = groups;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
];
|
|
51
|
+
const key = dir !== undefined && Object.hasOwn(KEY_OF, dir) ? KEY_OF[dir] : undefined;
|
|
52
|
+
return key === undefined ? [] : [{ key, binding: camel(file), specifier: `../${rest}` }];
|
|
32
53
|
});
|
|
33
54
|
}
|
|
34
55
|
|
|
@@ -87,8 +108,12 @@ export function insertApiEntries(
|
|
|
87
108
|
const list = listOf(next, entry.key, call);
|
|
88
109
|
if (list?.items.includes(entry.binding) === true) continue;
|
|
89
110
|
if (list === undefined) {
|
|
90
|
-
//
|
|
91
|
-
|
|
111
|
+
// In the order `x new` writes — actions, queries, jobs, tasks: after the nearest list
|
|
112
|
+
// before it that the call already holds, which is `actions` at the least.
|
|
113
|
+
const earlier = LIST_ORDER.slice(0, LIST_ORDER.indexOf(entry.key))
|
|
114
|
+
.map((key) => listOf(next, key, call))
|
|
115
|
+
.findLast((found) => found !== undefined);
|
|
116
|
+
const after = next.indexOf('\n', (earlier ?? actions).end);
|
|
92
117
|
const line = ` ${entry.key}: [${entry.binding}],`;
|
|
93
118
|
next = `${next.slice(0, after + 1)}${line}\n${next.slice(after + 1)}`;
|
|
94
119
|
} else {
|
package/src/app-boundaries.ts
CHANGED
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
// Runtime imports only: `Bun.Transpiler.scanImports` sees what survives type erasure, which is
|
|
10
10
|
// exactly the distinction the `app/ -> api/` rule needs (`import type` is allowed, a value
|
|
11
11
|
// import is not).
|
|
12
|
+
//
|
|
13
|
+
// The browser-transport rule rides the same read: `boundary-findings.ts`'s `appBoundaryFindings`
|
|
14
|
+
// hands the files read here to `app-transport.ts`, so a raw `fetch(` in an island is a finding of
|
|
15
|
+
// this step with no second walk of the app.
|
|
12
16
|
|
|
13
17
|
// Bun ships no `Bun.*` path API: `joinPath` reaches a file on disk with the host's separator.
|
|
14
18
|
import { join as joinPath } from 'node:path';
|
|
@@ -17,6 +21,7 @@ import { dirname, join, normalize, relative } from 'node:path/posix';
|
|
|
17
21
|
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
18
22
|
import type { BoundaryRule, ImportGraph } from '@ultimat3/render';
|
|
19
23
|
import { checkSurfaceBoundary, importGraph, SURFACES } from '@ultimat3/render';
|
|
24
|
+
import { commentSafe } from './comment-safe';
|
|
20
25
|
import { scanRuntimeImports, stripShebang } from './import-scan';
|
|
21
26
|
import type { Finding } from './output';
|
|
22
27
|
import { hasPathSegment } from './path-segments';
|
|
@@ -156,20 +161,6 @@ function subjectOf(path: string): string | undefined {
|
|
|
156
161
|
return parent === undefined || SURFACE_NAMES.has(parent) ? undefined : parent;
|
|
157
162
|
}
|
|
158
163
|
|
|
159
|
-
/**
|
|
160
|
-
* Control characters, `\u00xx`-escaped. The path is a value read off the repository being scanned,
|
|
161
|
-
* and it rides in a `#` comment: a directory holding a NEWLINE ends that comment, so everything
|
|
162
|
-
* after it is a second command in a line whose whole purpose is to be pasted into a shell.
|
|
163
|
-
* Escaped rather than deleted, because the comment still has to name the file the reader owns.
|
|
164
|
-
*/
|
|
165
|
-
const commentSafe = (path: string): string =>
|
|
166
|
-
[...path]
|
|
167
|
-
.map((char) => {
|
|
168
|
-
const code = char.codePointAt(0) ?? 0;
|
|
169
|
-
return code < 0x20 || code === 0x7f ? `\\u${code.toString(16).padStart(4, '0')}` : char;
|
|
170
|
-
})
|
|
171
|
-
.join('');
|
|
172
|
-
|
|
173
164
|
/**
|
|
174
165
|
* `x g query posts` where the path names a resource, `x g query <name>` where it names a surface.
|
|
175
166
|
* The placeholder form is the shape `MissingPositionalError` already hands out (`x g route
|
|
@@ -233,7 +224,7 @@ const APP_GLOBS = ['apps/*/{site,app,api,shared}/**/*.{ts,tsx}'];
|
|
|
233
224
|
/**
|
|
234
225
|
* Read every source file under an app's site, app, api and shared surfaces (`APP_GLOBS`). The
|
|
235
226
|
* only I/O in this module, and the one definition of "which files are the app's sources" —
|
|
236
|
-
* `
|
|
227
|
+
* `appBoundaryFindings` reads through this, and so does `x fix boundary`, never a second glob.
|
|
237
228
|
*/
|
|
238
229
|
export async function readAppSources(root: string): Promise<readonly SourceFile[]> {
|
|
239
230
|
const files: SourceFile[] = [];
|
|
@@ -252,8 +243,3 @@ export async function readAppSources(root: string): Promise<readonly SourceFile[
|
|
|
252
243
|
export function appImportGraph(files: readonly SourceFile[]): ImportGraph {
|
|
253
244
|
return graphOf(scan(files));
|
|
254
245
|
}
|
|
255
|
-
|
|
256
|
-
/** Read an app's sources and check them. */
|
|
257
|
-
export async function checkAppBoundaries(root: string): Promise<readonly Finding[]> {
|
|
258
|
-
return checkImportRules(await readAppSources(root));
|
|
259
|
-
}
|