@ultimat3/cli 22.15.0 → 24.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 +36 -13
- package/README.md +31 -8
- package/package.json +29 -28
- package/src/admin-catalog-keys.ts +16 -0
- package/src/admin-registration.ts +192 -0
- package/src/affected.ts +27 -35
- package/src/api-registration.ts +125 -11
- package/src/app-artifacts.ts +1 -1
- package/src/app-auth.ts +4 -27
- package/src/app-boundaries.ts +6 -20
- package/src/app-config-load.ts +51 -0
- package/src/app-env.ts +5 -11
- package/src/app-load.ts +55 -11
- package/src/app-manifest.ts +16 -8
- package/src/app-mcp.ts +10 -23
- package/src/app-permissions-borrowed.ts +129 -0
- package/src/app-permissions-site.ts +24 -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 +27 -34
- package/src/cdp-shot-driver.ts +3 -1
- package/src/cmd-auth-spec.ts +16 -0
- package/src/cmd-auth.ts +65 -0
- package/src/cmd-build-spec.ts +10 -3
- package/src/cmd-build.ts +107 -11
- package/src/cmd-db-backfill.ts +25 -12
- package/src/cmd-db.ts +66 -16
- package/src/cmd-deploy-helm.ts +7 -6
- package/src/cmd-deploy.ts +4 -4
- package/src/cmd-dev.ts +9 -8
- package/src/cmd-doctor-spec.ts +2 -9
- package/src/cmd-doctor.ts +75 -24
- package/src/cmd-env.ts +26 -2
- package/src/cmd-generate.ts +195 -47
- package/src/cmd-help.ts +19 -6
- package/src/cmd-i18n.ts +10 -9
- package/src/cmd-jobs-spec.ts +19 -3
- package/src/cmd-jobs.ts +136 -7
- package/src/cmd-manifest-spec.ts +1 -1
- package/src/cmd-manifest.ts +6 -25
- package/src/cmd-mcp.ts +46 -15
- package/src/cmd-new.ts +15 -1
- package/src/cmd-planned.ts +15 -8
- package/src/cmd-routes.ts +27 -3
- package/src/cmd-secrets-ring.ts +76 -0
- package/src/cmd-secrets-spec.ts +10 -2
- package/src/cmd-secrets.ts +95 -18
- package/src/cmd-shot-matrix.ts +1 -3
- package/src/cmd-shot.ts +12 -3
- package/src/cmd-tasks.ts +84 -15
- package/src/cmd-test-spec.ts +1 -7
- package/src/cmd-test.ts +13 -29
- package/src/cmd-verify.ts +13 -5
- 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/db-subscribes.ts +12 -4
- package/src/dev-dashboard-guard.ts +76 -0
- package/src/dev-dashboard.ts +51 -29
- package/src/dev-lock.ts +4 -7
- package/src/dev-route-table.ts +9 -0
- package/src/dispatch.ts +3 -6
- package/src/doctor-app-url.ts +39 -0
- package/src/doctor-auth.ts +95 -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/drift-replica-identity.ts +42 -0
- package/src/error-catalog.ts +6 -9
- package/src/error-codes.ts +55 -3
- package/src/error-unthrown.ts +29 -11
- package/src/errors.ts +54 -83
- package/src/framework-schema-apply.ts +78 -0
- package/src/framework-schema-stamp.ts +96 -0
- package/src/framework-schema.ts +28 -2
- package/src/generate-files.ts +79 -12
- package/src/generate-grant-findings.ts +76 -0
- package/src/generate-grants.ts +77 -3
- package/src/generate-kinds.ts +52 -1
- package/src/generate-shadow.ts +46 -0
- package/src/generate-write.ts +62 -40
- package/src/generated-imports.ts +59 -0
- package/src/gh-target.ts +12 -7
- package/src/guard-sources.ts +104 -0
- package/src/guards.ts +23 -7
- package/src/handle-registration.ts +217 -0
- package/src/i18n-audit.ts +4 -1
- 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 +12 -5
- package/src/island-bundle.ts +3 -15
- package/src/island-store.ts +24 -16
- package/src/jobs-drain.ts +95 -21
- package/src/jobs-json.ts +50 -0
- package/src/jobs-report.ts +7 -4
- package/src/json-merge.ts +46 -0
- package/src/loopback-host.ts +13 -0
- package/src/manifest-staleness.ts +62 -0
- package/src/mcp-errors.ts +41 -4
- package/src/mcp-host.ts +16 -9
- package/src/mcp-test-run.ts +38 -0
- package/src/messages.ts +35 -0
- package/src/metrics-endpoint.ts +95 -24
- package/src/migration-extensions.ts +29 -0
- package/src/module-imports.ts +47 -0
- package/src/output.ts +24 -9
- package/src/page-navigation.ts +7 -23
- package/src/page-speculation.ts +5 -20
- package/src/permission-grants.ts +41 -9
- package/src/prerender-out.ts +34 -7
- package/src/prerender.ts +8 -5
- package/src/publish-closure.ts +101 -0
- package/src/pwa-artifacts.ts +26 -94
- package/src/registry.ts +2 -0
- package/src/revalidated-response.ts +4 -3
- package/src/role-load.ts +143 -0
- package/src/role-replicator.ts +16 -0
- package/src/role-start.ts +27 -5
- package/src/role-sync.ts +55 -20
- package/src/role-wake.ts +45 -0
- package/src/runtime-admin.ts +141 -0
- package/src/runtime-assets.ts +11 -7
- package/src/runtime-bindings.ts +4 -4
- package/src/runtime-cache.ts +38 -47
- package/src/runtime-hooks.ts +5 -2
- package/src/runtime-idempotency-scope.ts +23 -0
- package/src/runtime-isr.ts +36 -0
- package/src/runtime-jobs.ts +8 -33
- package/src/runtime-mfa-warning.ts +29 -0
- package/src/runtime-notify-retention.ts +10 -46
- package/src/runtime-overrides.ts +1 -1
- package/src/runtime-purge.ts +38 -16
- package/src/runtime-queue.ts +39 -11
- package/src/runtime-realtime.ts +9 -45
- package/src/runtime-render.ts +68 -35
- package/src/runtime-services.ts +22 -18
- package/src/runtime-storage.ts +10 -5
- package/src/scaffold-typecheck.ts +1 -0
- package/src/schema-diff.ts +16 -0
- package/src/schema-drift.ts +45 -15
- package/src/schema-dump-drift.ts +64 -0
- package/src/seo-routes.ts +80 -9
- package/src/serve-boot.ts +114 -158
- package/src/serve-drain.ts +9 -33
- package/src/serve-env.ts +9 -6
- package/src/serve-prebuilt-paths.ts +19 -0
- package/src/serve-prebuilt.ts +84 -0
- package/src/serve-web.ts +169 -0
- package/src/serve.ts +6 -9
- package/src/server-barrels.ts +54 -0
- package/src/shot-locale.ts +5 -23
- package/src/site-assets.ts +3 -5
- package/src/site-config.ts +9 -48
- package/src/site-seo.ts +1 -1
- package/src/sitemap-lastmod.ts +5 -0
- package/src/solid-loader.ts +12 -3
- package/src/source-edit.ts +49 -0
- package/src/source-files.ts +6 -0
- package/src/stored-object-headers.ts +43 -0
- package/src/sync-url.ts +2 -2
- package/src/tasks-facts.ts +3 -26
- package/src/templates/action.ts +147 -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/naming.ts +28 -0
- 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 +41 -35
- package/src/templates/scaffold-api.ts +55 -1
- package/src/templates/scaffold-app.ts +177 -53
- package/src/templates/scaffold-auth.ts +83 -19
- package/src/templates/scaffold-browser-client.ts +90 -0
- package/src/templates/scaffold-container-compose.ts +10 -1
- package/src/templates/scaffold-container.ts +20 -2
- 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 +144 -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-helm-fences.ts +76 -0
- package/src/templates/scaffold-helm-templates.ts +32 -7
- package/src/templates/scaffold-helm.ts +31 -1
- package/src/templates/scaffold-http.ts +9 -7
- package/src/templates/scaffold-repo.ts +61 -3
- package/src/templates/scaffold-roles.ts +56 -9
- package/src/templates/scaffold-shell.ts +74 -105
- 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 +14 -2
- package/src/test-ignores.ts +25 -0
- package/src/test-passes.ts +0 -15
- package/src/test-select.ts +5 -14
- package/src/test-shards.ts +41 -93
- package/src/test-workers.ts +26 -4
- package/src/theme-boot.ts +6 -31
- package/src/transport-calls.ts +105 -0
- package/src/trusted-hops-error.ts +17 -0
- package/src/unscanned-admin.ts +54 -0
- package/src/unthrown-codes.ts +26 -0
- package/src/verify-checks.ts +43 -53
- package/src/verify-coverage-run.ts +190 -0
- package/src/verify-deadline.ts +353 -0
- package/src/verify-floor.ts +36 -9
- package/src/verify-merge.ts +171 -45
- package/src/verify-part-step.ts +90 -0
- 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 +116 -37
- package/src/verify-shard.ts +33 -11
- package/src/verify-stalled.ts +152 -0
- package/src/verify-step.ts +42 -1
- package/src/verify-tests.ts +83 -8
- package/src/workspace-checks.ts +50 -8
- package/src/workspace-dep-edit.ts +127 -0
- package/src/build-errors.ts +0 -54
- 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/src/verify-errors.ts +0 -49
package/CLAUDE.md
CHANGED
|
@@ -44,18 +44,25 @@ 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`), which also holds the shards' `files` to a partition of the corpus they hashed and reads each part step through `verify-part-step.ts`; `doctor-coverage.ts` lists the excludes |
|
|
48
|
+
| `verify-deadline.ts` / `verify-stalled.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; the finding names the test file `bun test` had not finished (`at`, `meta.inFlight`) and its `fix:` runs that file; `--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` |
|
|
52
|
-
| `app-permissions.ts` / `permission-grants.ts` | `policy`: every permission granted or required is declared,
|
|
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 |
|
|
56
|
+
| `app-permissions.ts` / `permission-grants.ts` / `app-permissions-borrowed.ts` / `app-permissions-site.ts` | `policy`: every permission granted or required is declared, every one required is granted by some role (`X_PERMISSION_UNGRANTED`), and every one an app rule requires is declared by an app module, not only by a package (`X_PERMISSION_BORROWED`, read off `permissionDeclarationSites()`) |
|
|
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
|
-
| `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
|
-
| `
|
|
58
|
-
| `
|
|
62
|
+
| `schema-drift.ts` / `drift.ts` / `drift-replica-identity.ts` / `db-destructive.ts` / `db-ungeneratable.ts` | `drift`: snapshot vs declarations (`REPLICA IDENTITY FULL` a params channel or `subscribes:` needs included), the source hash (core's `canonicalJson`), the header markers |
|
|
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`) |
|
|
65
|
+
| `error-unthrown.ts` / `unthrown-codes.ts` | host check: a registered code nothing throws is listed in `UNTHROWN_CODES` (a row's wording waives nothing); a listed code thrown again is `X_ERROR_CODE_UNTHROWN_STALE` |
|
|
59
66
|
|
|
60
67
|
### Generators (`x g`, `x new`)
|
|
61
68
|
|
|
@@ -63,47 +70,63 @@ 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-
|
|
73
|
+
| `generate-shadow.ts` | a name whose type spelling a planned file also uses as a global or local type (`promise`, `row`) is `X_CLI_BAD_FLAG`, decided on the planned files |
|
|
74
|
+
| `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
75
|
| `api-registration.ts` | a written job or task is listed in `apps/web/api/index.ts` |
|
|
76
|
+
| `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 |
|
|
77
|
+
| `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`) |
|
|
78
|
+
| `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
79
|
| `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 |
|
|
80
|
+
| `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
81
|
|
|
71
82
|
### Boot (`x dev`, the container)
|
|
72
83
|
|
|
73
84
|
| File | Job |
|
|
74
85
|
|---|---|
|
|
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` |
|
|
86
|
+
| `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 |
|
|
87
|
+
| `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` |
|
|
88
|
+
| `module-imports.ts` | the files a module's source imports, resolved once for both graphs: `referenced` (reload) and `evaluated` (document) |
|
|
76
89
|
| `cmd-dev.ts` / `dev-route-table.ts` | `x dev`: every role in one process, `/_x`, the watcher |
|
|
77
90
|
| `runtime-bindings.ts` | which service each binding points at (embedded or external); events follow `realtime.transport` / `urlEnv` |
|
|
78
91
|
| `runtime-queue.ts` / `runtime-services.ts` | the db + queue pair; everything else and every ambient accessor |
|
|
79
|
-
| `
|
|
92
|
+
| `framework-schema-apply.ts` / `framework-schema-stamp.ts` | the framework tables: `ROLE=migrate`, `x dev` and every CLI command APPLY them in one transaction behind `MIGRATION_LOCK_KEY` (`pg_advisory_xact_lock`) with a bounded `lock_timeout`, then stamp the build on `x_jobs`' table comment; a serving role on an external database runs no DDL and VERIFIES the stamp (`X_FRAMEWORK_SCHEMA_UNAPPLIED`, a major skew is a warning). A comment, never a table: a new framework table moves every app's schema dump |
|
|
93
|
+
| `runtime-isr.ts` | the ISR controller both boots serve through, ATTACHED to `invalidateTags` and released on stop (`attachedIsr`); `appRoutes` without one builds and attaches its own |
|
|
94
|
+
| `runtime-mfa-warning.ts` / `runtime-idempotency-scope.ts` | boot warnings an operator can act on: unsealed MFA secrets (web role, `x auth seal-mfa`), a per-process idempotency store after the app loaded |
|
|
95
|
+
| `app-config-load.ts` | the ONE reader of an app's `app.config.ts`: `loadAppConfig(root)` — the `config` export through core's `defineConfig`, so validated and default-merged; `undefined` with no file; no `config` object is `X_CONFIG_INVALID`. Every boot fact (`app-auth.ts`, `theme-boot.ts`, `site-config.ts`, `pwa-artifacts.ts`, …) reads a section off it, and `startServices` loads it once. An `import(` of the config module anywhere else is `X_CONFIG_IMPORT_OUTSIDE_LOADER` (`scripts/lib/config-import.ts`) |
|
|
96
|
+
| `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 (`*Of(config)` over the one loaded config), and what each wires |
|
|
80
97
|
| `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` |
|
|
98
|
+
| `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
99
|
| `role-sync.ts` / `role-replicator.ts` / `runtime-live-feed.ts` | the sync node, the change feed |
|
|
82
100
|
| `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 |
|
|
101
|
+
| `stored-object-headers.ts` | how `/_storage` and `/media` present a stored object: inline only for raster images, audio and video; every other type (`text/html`, SVG, XML, PDF) is `content-disposition: attachment` with a `sandbox` CSP, which http's security stage keeps beside the app's |
|
|
83
102
|
| `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) |
|
|
84
103
|
| `app-openapi.ts` | `openapi.json` (complete when `defineApi({ openapi })` is declared) and each bearer mount's own document; staleness for all of them |
|
|
85
104
|
| `script-csp.ts` / `style-csp.ts` / `style-bundle.ts` / `page-sync.ts` / `worker-bundle.ts` | CSP hashes, the CSS file, the page's sync target and worker |
|
|
86
105
|
| `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
106
|
| `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
107
|
| `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;
|
|
108
|
+
| `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 |
|
|
109
|
+
| `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
110
|
| `dev-*.ts` | `x dev` only: dashboard sources, traces, the N+1 ledger, the watcher, the reload, the lock, the port |
|
|
111
|
+
| `dev-dashboard-guard.ts` / `loopback-host.ts` | `/_x` answers a loopback `Host` only (`X_DEV_HOST_REFUSED`, 421 — DNS rebinding); the SQL panel runs on a same-origin `POST /_x/db` only, through admin's `readOnlySql`. Checked in the route, never left to the app's `csrf` config |
|
|
91
112
|
|
|
92
113
|
### Build and data
|
|
93
114
|
|
|
94
115
|
| File | Job |
|
|
95
116
|
|---|---|
|
|
96
|
-
| `cmd-build.ts` / `image-prepare.ts` | `x build`: static gate first; docker stamps `BUILD_ID` and
|
|
117
|
+
| `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
118
|
| `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
119
|
| `sw-artifacts.ts` / `pwa-artifacts.ts` / `favicon.ts` | the service worker, the web manifest, `/favicon.ico` |
|
|
99
120
|
| `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` |
|
|
121
|
+
| `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
122
|
|
|
101
123
|
### Introspection and tools
|
|
102
124
|
|
|
103
125
|
| File | Job |
|
|
104
126
|
|---|---|
|
|
105
127
|
| `cmd-registries.ts` / `cmd-jobs.ts` / `cmd-tasks.ts` / `cmd-policy.ts` / `cmd-i18n.ts` | project a framework registry; each pairs CLI wiring with a facts module |
|
|
106
|
-
| `cmd-
|
|
128
|
+
| `cmd-auth.ts` / `cmd-auth-spec.ts` / `doctor-auth.ts` | `x auth seal-mfa`: the one-shot that seals plaintext `x_users.mfa_secret` through auth's `sealMfaSecrets` (compare-and-set per row; `{ sealed, alreadySealed, skipped }`), over `startQueue`'s client, `open` injectable. `doctor-auth.ts` is the deploy-time half: unsealed secrets and retired framework tables (`RETIRED_TABLE_FIXES`, a `Map` holding a whole literal per table), asked of an external `DATABASE_URL` only |
|
|
129
|
+
| `cmd-mcp.ts` / `mcp-host.ts` / `mcp-errors.ts` / `mcp-db-target.ts` | `x mcp serve`: 18 tools, two transports; a taken port is `X_PORT_IN_USE`. `mcp-test-run.ts` folds `bun test`'s `N errors` line and exit code into `tests.run` |
|
|
107
130
|
| `cmd-shot*.ts` / `cdp-shot-*.ts` / `browser-launcher*.ts` / `island-*` | `x shot` over raw CDP; `verdict.json` names its own blind spots. Not a gate step |
|
|
108
131
|
| `cmd-pr.ts` / `cmd-ci.ts` | GitHub through `gh`, parsed against a schema. Not gate steps |
|
|
109
132
|
| `error-catalog.ts` | imports every `@ultimat3/*` package so `x errors` answers for any code |
|
package/README.md
CHANGED
|
@@ -11,11 +11,12 @@ 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 |
|
|
17
18
|
| `x mcp serve` | `@ultimat3/mcp`'s 18 dev tools, over stdio or HTTP | one catalog, one scope set, both transports |
|
|
18
|
-
| `x doctor` | environment, ports, drift, PWA prerequisites | every finding carries a fix command |
|
|
19
|
+
| `x doctor` | environment, ports, drift, PWA prerequisites, `APP_URL` | every finding carries a fix command; probes the port `x dev` binds ([which one](../../wiki/CLI-Reference.md#x-doctor)) |
|
|
19
20
|
| `x deploy` | container deploy plan | compose or helm; zero platform primitives |
|
|
20
21
|
| `x manifest` / `x routes` | generated facts | `x.manifest.json`, `openapi.json`, route table |
|
|
21
22
|
| `x actions` / `x queries` / `x entities` | the declaration registries | `list` and `describe <name>`, straight off the registries |
|
|
@@ -23,6 +24,7 @@ Commands and the `x verify` step count, `As of 2026-08`:
|
|
|
23
24
|
| `x jobs ls\|show\|retry\|cancel\|drain` | the queue | depth, dead letters, step traces, `retry --from-step`, `cancel --reason`, `drain --to` |
|
|
24
25
|
| `x test [type]` | one of the six test types, or all | same type rule as the gate; `--filter`, `--sample N` |
|
|
25
26
|
| `x env check\|example` | the typed environment `envSchema` declares | and the `.env.example` rendered from it |
|
|
27
|
+
| `x auth seal-mfa` | one-shot auth maintenance | seals every `x_users.mfa_secret` still in the clear; idempotent, `{ sealed, alreadySealed, skipped }` |
|
|
26
28
|
| `x secrets show\|init\|edit\|set\|rotate` | the committed encrypted secrets | decrypted into the `envSchema` variables of the same names |
|
|
27
29
|
| `x policy list\|explain <subject>` | which clause decided a permission, and why | five packages print `x policy explain` as a denial's `fix:` |
|
|
28
30
|
| `x i18n check\|add\|sync` | catalogs: gaps, a new locale, key sync | all three of i18n's own error fixes name it |
|
|
@@ -91,11 +93,30 @@ than a skip.
|
|
|
91
93
|
"Nothing" is both ways a suite disappears — no files at all, and every test in the files it found
|
|
92
94
|
skipping itself, which is read back out of `bun test`'s own summary. `x new` writes one.
|
|
93
95
|
|
|
96
|
+
The same file states the app's **coverage floor**: `"coverage": { "lines": 95, "funcs": 95 }`,
|
|
97
|
+
held by the `unit` step over every `.ts`/`.tsx` under `apps/` and `packages/`, a file no unit test
|
|
98
|
+
loads counted at 0%. No `coverage` is `X_COVERAGE_FLOOR_UNSTATED` (the finding carries the line to
|
|
99
|
+
add, with the measured numbers), under it is `X_COVERAGE_BELOW_FLOOR` naming the ten files losing
|
|
100
|
+
the most lines, and a floor under 95 the tree has left 1.5 points behind is
|
|
101
|
+
`X_COVERAGE_FLOOR_STALE` — the floor only rises. An `exclude` entry names code a unit test cannot
|
|
102
|
+
execute and carries its `why`; `x doctor` prints them. A `--shard` slice hands its facts to
|
|
103
|
+
`x verify merge`, which judges the fold. `x new` writes 95 / 95.
|
|
104
|
+
|
|
105
|
+
A step has a **deadline** — 8 minutes for the six suites, 5 for every other step,
|
|
106
|
+
`"stepTimeoutMs": { "<step>": ms }` in `x.verify.json` to change one. Past it the step fails by
|
|
107
|
+
name with `X_VERIFY_STEP_TIMEOUT` and every process it started is killed. Under `--json` each
|
|
108
|
+
finished step is one line on stderr (`{"step":"lint","ok":true,"ms":21987}`); stdout stays the
|
|
109
|
+
single document.
|
|
110
|
+
|
|
94
111
|
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.
|
|
112
|
+
exports a `guard` whose `check(root, sources)` returns `Finding[]`, and the `boundaries` step runs
|
|
113
|
+
every one of them. `sources` is the run's ONE read of the app (`GuardSources`): `files(glob)` is
|
|
114
|
+
walked and read once however many guards ask, and `compiled(path)` is a stylesheet as the build
|
|
115
|
+
compiles it. Nothing registers a guard — the directory is the registration — and what a guard returns
|
|
97
116
|
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
|
|
117
|
+
`X_GUARD_FINDING_INVALID`). `x g guard <name>` scaffolds one with its test — the shipped guard of
|
|
118
|
+
that name where there is one (`x new` writes all seventeen), the blank template otherwise — and
|
|
119
|
+
`x doctor` lists the shipped ones an app does not hold.
|
|
99
120
|
|
|
100
121
|
## Layout
|
|
101
122
|
|
|
@@ -111,6 +132,7 @@ is held to the same error contract shipped source is (`X_GUARD_INVALID`, `X_GUAR
|
|
|
111
132
|
| `registry.ts` | the one command list |
|
|
112
133
|
| `generate-kinds.ts` | which generators exist, and how a command line names one |
|
|
113
134
|
| `guards.ts` | the app's own conventions: `guards/` discovered, run, and held to the error contract |
|
|
135
|
+
| `guard-sources.ts` | one walk, one read and one Sass compile per file per gate run, shared by every guard |
|
|
114
136
|
| `cmd-*.ts` | one command group each |
|
|
115
137
|
| `templates/` | scaffolding as typed string modules, not copied fixtures |
|
|
116
138
|
| `app-load.ts` | import an app's modules so the framework registries hold it |
|
|
@@ -122,10 +144,12 @@ is held to the same error contract shipped source is (`X_GUARD_INVALID`, `X_GUAR
|
|
|
122
144
|
| `app-manifest.ts` | `x.manifest.json`, projected by `@ultimat3/manifest` |
|
|
123
145
|
| `app-openapi.ts` | `openapi.json`, projected by `@ultimat3/action` |
|
|
124
146
|
| `app-boundaries.ts` | app import boundaries, over `@ultimat3/render`'s surface check |
|
|
147
|
+
| `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
148
|
| `app-agents-md.ts` | `AGENTS.md` exists and stays short, over `@ultimat3/manifest`'s check |
|
|
126
149
|
| `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
150
|
| `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` |
|
|
151
|
+
| `metrics-endpoint.ts` | the `METRICS_PATH` scrape listener every role opens, on `METRICS_PORT`; opening it starts the `process_*` series, labelled by `startMetricsEndpoint({ role })` |
|
|
152
|
+
| `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
153
|
| `otlp-export.ts` | the exporters `OTEL_EXPORTER_OTLP_ENDPOINT` switches on, and their drain hooks |
|
|
130
154
|
| `dev-*.ts` | what `x dev` boots: services, runtime, routes, hooks, roles, the `/_x` mount |
|
|
131
155
|
| `island-bundle.ts` | every `*.island.tsx` built as its own entry point, content-hashed |
|
|
@@ -150,7 +174,7 @@ apps/web/app/<feature>/{entity,repo,service,policy,errors,ui}.ts
|
|
|
150
174
|
apps/web/app/<feature>/{actions,queries,live,jobs,tasks}/<name>.ts
|
|
151
175
|
apps/web/{site,app}/<path>/page.tsx
|
|
152
176
|
apps/web/{site,app}/<path>/<name>.island.tsx # x g island <name> --at <dir>
|
|
153
|
-
apps/admin/
|
|
177
|
+
apps/admin/app/admin/pages/<name>.tsx # x g admin:page <name> --permission p
|
|
154
178
|
guards/<name>.ts # x g guard <name>
|
|
155
179
|
```
|
|
156
180
|
|
|
@@ -169,14 +193,13 @@ a job boundary the class is gone and the `code` is what survives — match on th
|
|
|
169
193
|
| Class | Code | Declared in |
|
|
170
194
|
|---|---|---|
|
|
171
195
|
| `BadFlagError` | `X_CLI_BAD_FLAG` | `src/errors.ts` |
|
|
172
|
-
| `BuildEntryMissingError` | `X_BUILD_ENTRY_MISSING` | `src/build
|
|
196
|
+
| `BuildEntryMissingError` | `X_BUILD_ENTRY_MISSING` | `src/cmd-build.ts` |
|
|
173
197
|
| `BunVersionError` | `X_BUN_VERSION` | `src/errors.ts` |
|
|
174
198
|
| `CatalogExistsError` | `X_GENERATE_CONFLICT` | `src/errors.ts` |
|
|
175
199
|
| `CdpBrowserMissingError` | `X_CDP_BROWSER_MISSING` | `src/cdp-errors.ts` |
|
|
176
200
|
| `CdpCallFailedError` | `X_CDP_CALL_FAILED` | `src/cdp-errors.ts` |
|
|
177
201
|
| `CdpLaunchFailedError` | `X_CDP_LAUNCH_FAILED` | `src/cdp-errors.ts` |
|
|
178
202
|
| `CdpTimeoutError` | `X_CDP_TIMEOUT` | `src/cdp-errors.ts` |
|
|
179
|
-
| `CliNotImplementedError` | `X_NOT_IMPLEMENTED` | `src/errors.ts` |
|
|
180
203
|
| `DeclarationUnknownError` | `X_DECLARATION_UNKNOWN` | `src/errors.ts` |
|
|
181
204
|
| `E2eAppFailedError` | `X_E2E_APP_FAILED` | `src/e2e-errors.ts` |
|
|
182
205
|
| `E2eEvaluateCapturedError` | `X_E2E_EVALUATE_CAPTURED` | `src/e2e-errors.ts` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "24.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": "24.0.0",
|
|
43
|
+
"@ultimat3/admin": "24.0.0",
|
|
44
|
+
"@ultimat3/ai": "24.0.0",
|
|
45
|
+
"@ultimat3/auth": "24.0.0",
|
|
46
|
+
"@ultimat3/cache": "24.0.0",
|
|
47
|
+
"@ultimat3/core": "24.0.0",
|
|
48
|
+
"@ultimat3/db": "24.0.0",
|
|
49
|
+
"@ultimat3/entity": "24.0.0",
|
|
50
|
+
"@ultimat3/flags": "24.0.0",
|
|
51
|
+
"@ultimat3/http": "24.0.0",
|
|
52
|
+
"@ultimat3/i18n": "24.0.0",
|
|
53
|
+
"@ultimat3/jobs": "24.0.0",
|
|
54
|
+
"@ultimat3/mail": "24.0.0",
|
|
55
|
+
"@ultimat3/manifest": "24.0.0",
|
|
56
|
+
"@ultimat3/mcp": "24.0.0",
|
|
57
|
+
"@ultimat3/money": "24.0.0",
|
|
58
|
+
"@ultimat3/notify": "24.0.0",
|
|
59
|
+
"@ultimat3/policy": "24.0.0",
|
|
60
|
+
"@ultimat3/pwa": "24.0.0",
|
|
61
|
+
"@ultimat3/query": "24.0.0",
|
|
62
|
+
"@ultimat3/realtime": "24.0.0",
|
|
63
|
+
"@ultimat3/render": "24.0.0",
|
|
64
|
+
"@ultimat3/schema": "24.0.0",
|
|
65
|
+
"@ultimat3/seo": "24.0.0",
|
|
66
|
+
"@ultimat3/storage": "24.0.0",
|
|
67
|
+
"@ultimat3/testing": "24.0.0",
|
|
68
|
+
"@ultimat3/time": "24.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/affected.ts
CHANGED
|
@@ -25,23 +25,6 @@ import { readWorkspaceGraph } from './workspace-graph';
|
|
|
25
25
|
/** The branch a change is measured against when `--base` says nothing. */
|
|
26
26
|
export const DEFAULT_BASE = 'main';
|
|
27
27
|
|
|
28
|
-
/**
|
|
29
|
-
* Root files that belong to no workspace and change what every workspace compiles to: a compiler
|
|
30
|
-
* option, a lint rule, the root manifest, the resolved dependency tree, the test preload, the app
|
|
31
|
-
* config. A "scoped" run that skipped them reports green over packages the edit just broke.
|
|
32
|
-
*
|
|
33
|
-
* Matched on the WHOLE path — `packages/cli/package.json` is the cli workspace's own file and
|
|
34
|
-
* reaches only cli's dependents, `package.json` at the root reaches everything.
|
|
35
|
-
*/
|
|
36
|
-
export const ROOT_WIDE_FILES: readonly string[] = [
|
|
37
|
-
'app.config.ts',
|
|
38
|
-
'biome.json',
|
|
39
|
-
'bun.lock',
|
|
40
|
-
'bunfig.toml',
|
|
41
|
-
'package.json',
|
|
42
|
-
'tsconfig.json',
|
|
43
|
-
];
|
|
44
|
-
|
|
45
28
|
/**
|
|
46
29
|
* A file with no compilation unit behind it. A doc or a plan re-checks nothing, so it maps to no
|
|
47
30
|
* workspace at all rather than to the one it happens to sit inside — `packages/cli/README.md` is
|
|
@@ -107,7 +90,13 @@ export interface AffectedPlan {
|
|
|
107
90
|
readonly changed: readonly string[];
|
|
108
91
|
/** The subset with no compilation unit behind it, named so an empty answer explains itself. */
|
|
109
92
|
readonly ignored: readonly string[];
|
|
110
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* The changed files no workspace owns, which forced every workspace in — and the root itself — or
|
|
95
|
+
* empty when none did. A root file (`tsconfig.base.json`, `biome.json`, `bun.lock`, the test
|
|
96
|
+
* preload, `scripts/`, `guards/`) changes what every workspace compiles to or is checked by, and
|
|
97
|
+
* a scoped run that skipped it reports green over packages the edit just broke. Matched on
|
|
98
|
+
* ownership, never on a list of names: a list misses the file nobody thought to add to it.
|
|
99
|
+
*/
|
|
111
100
|
readonly rootWide: readonly string[];
|
|
112
101
|
readonly workspaces: readonly WorkspaceNode[];
|
|
113
102
|
}
|
|
@@ -124,14 +113,15 @@ export function planAffected(
|
|
|
124
113
|
): AffectedPlan {
|
|
125
114
|
const ignored = changed.filter(isDoc);
|
|
126
115
|
const considered = changed.filter((path) => !isDoc(path));
|
|
127
|
-
const rootWide =
|
|
128
|
-
if (rootWide.length > 0) {
|
|
129
|
-
return { changed, ignored, rootWide, workspaces: [...graph].sort(byName) };
|
|
130
|
-
}
|
|
116
|
+
const rootWide: string[] = [];
|
|
131
117
|
const seeds = new Set<string>();
|
|
132
118
|
for (const path of considered) {
|
|
133
119
|
const node = owningWorkspace(graph, path);
|
|
134
|
-
if (node
|
|
120
|
+
if (node === undefined) rootWide.push(path);
|
|
121
|
+
else seeds.add(node.name);
|
|
122
|
+
}
|
|
123
|
+
if (rootWide.length > 0) {
|
|
124
|
+
return { changed, ignored, rootWide, workspaces: [...graph].sort(byName) };
|
|
135
125
|
}
|
|
136
126
|
const reached = withDependents(graph, seeds);
|
|
137
127
|
return {
|
|
@@ -263,14 +253,20 @@ export interface AffectedScope {
|
|
|
263
253
|
}
|
|
264
254
|
|
|
265
255
|
/**
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
256
|
+
* The plan's scope, relative to the directory the caller scans. A ROOT-WIDE plan is the whole scan
|
|
257
|
+
* root (`''`): every workspace is in it, and so is every file no workspace owns — a `scripts/` test
|
|
258
|
+
* stayed out of the run while `package.json` changed when only workspace directories were listed.
|
|
259
|
+
*
|
|
260
|
+
* Otherwise an empty prefix means the scan root IS an affected workspace, so every path it yields
|
|
261
|
+
* is in scope; a prefix starting with `..` is a workspace outside that root, which has no file there
|
|
262
|
+
* to select and must not be allowed to collapse into a match-everything empty string.
|
|
269
263
|
*/
|
|
270
|
-
const scopePrefixes = (
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
264
|
+
export const scopePrefixes = (plan: AffectedPlan, root: string, cwd: string): readonly string[] =>
|
|
265
|
+
plan.rootWide.length > 0
|
|
266
|
+
? ['']
|
|
267
|
+
: plan.workspaces
|
|
268
|
+
.map((workspace) => relative(cwd, join(root, workspace.dir)).split('\\').join('/'))
|
|
269
|
+
.filter((path) => !path.startsWith('..'));
|
|
274
270
|
|
|
275
271
|
export const inScope = (path: string, prefixes: readonly string[]): boolean =>
|
|
276
272
|
prefixes.some((prefix) => prefix === '' || path === prefix || path.startsWith(`${prefix}/`));
|
|
@@ -300,11 +296,7 @@ export async function affectedScope(options: AffectedScopeOptions): Promise<Affe
|
|
|
300
296
|
selection,
|
|
301
297
|
root,
|
|
302
298
|
plan,
|
|
303
|
-
prefixes: scopePrefixes(
|
|
304
|
-
root,
|
|
305
|
-
options.cwd,
|
|
306
|
-
plan.workspaces.map((workspace) => workspace.dir),
|
|
307
|
-
),
|
|
299
|
+
prefixes: scopePrefixes(plan, root, options.cwd),
|
|
308
300
|
};
|
|
309
301
|
}
|
|
310
302
|
|