@ultimat3/cli 23.0.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 +15 -7
- package/README.md +3 -3
- package/package.json +28 -28
- package/src/affected.ts +27 -35
- package/src/api-registration.ts +89 -0
- package/src/app-artifacts.ts +1 -1
- package/src/app-auth.ts +4 -27
- package/src/app-config-load.ts +51 -0
- package/src/app-env.ts +5 -11
- package/src/app-manifest.ts +2 -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 +7 -17
- package/src/budgets.ts +4 -35
- 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.ts +64 -15
- package/src/cmd-db-backfill.ts +25 -12
- package/src/cmd-db.ts +6 -5
- 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 +41 -14
- package/src/cmd-env.ts +26 -2
- package/src/cmd-generate.ts +79 -32
- package/src/cmd-help.ts +19 -6
- package/src/cmd-i18n.ts +10 -9
- package/src/cmd-jobs.ts +18 -6
- 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-planned.ts +15 -8
- package/src/cmd-routes.ts +12 -2
- package/src/cmd-secrets.ts +47 -6
- package/src/cmd-shot-matrix.ts +1 -3
- package/src/cmd-shot.ts +12 -3
- package/src/cmd-tasks.ts +2 -2
- package/src/cmd-test-spec.ts +1 -7
- package/src/cmd-test.ts +13 -29
- package/src/cmd-verify.ts +4 -4
- 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/dispatch.ts +3 -6
- package/src/doctor-app-url.ts +39 -0
- package/src/doctor-auth.ts +95 -0
- package/src/drift-replica-identity.ts +42 -0
- package/src/error-catalog.ts +6 -9
- package/src/error-codes.ts +23 -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 +13 -2
- package/src/generate-files.ts +4 -3
- package/src/generate-kinds.ts +33 -1
- package/src/generate-shadow.ts +46 -0
- package/src/generate-write.ts +47 -13
- package/src/gh-target.ts +12 -7
- package/src/i18n-audit.ts +4 -1
- package/src/index.ts +1 -3
- package/src/island-bundle.ts +3 -15
- package/src/jobs-drain.ts +82 -15
- package/src/jobs-report.ts +4 -4
- package/src/loopback-host.ts +13 -0
- package/src/manifest-staleness.ts +62 -0
- package/src/mcp-errors.ts +12 -3
- package/src/mcp-host.ts +16 -9
- package/src/mcp-test-run.ts +38 -0
- package/src/messages.ts +2 -0
- package/src/metrics-endpoint.ts +25 -1
- package/src/output.ts +6 -0
- package/src/page-navigation.ts +7 -23
- package/src/page-speculation.ts +5 -20
- package/src/prerender-out.ts +34 -7
- package/src/prerender.ts +8 -5
- package/src/pwa-artifacts.ts +26 -94
- package/src/registry.ts +2 -0
- package/src/revalidated-response.ts +4 -3
- package/src/role-replicator.ts +16 -0
- package/src/role-start.ts +9 -4
- package/src/role-sync.ts +55 -20
- package/src/runtime-assets.ts +11 -7
- package/src/runtime-bindings.ts +4 -4
- package/src/runtime-cache.ts +38 -47
- 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 +31 -10
- package/src/runtime-realtime.ts +9 -45
- package/src/runtime-render.ts +62 -35
- package/src/runtime-services.ts +20 -17
- package/src/runtime-storage.ts +10 -5
- package/src/schema-diff.ts +16 -0
- package/src/schema-drift.ts +45 -15
- package/src/seo-routes.ts +80 -9
- package/src/serve-boot.ts +34 -6
- package/src/serve-drain.ts +9 -33
- package/src/serve-env.ts +9 -6
- package/src/serve-web.ts +12 -7
- 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-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 -29
- package/src/templates/action.ts +3 -0
- package/src/templates/naming.ts +28 -0
- package/src/templates/route.ts +3 -6
- package/src/templates/scaffold-app.ts +6 -2
- package/src/templates/scaffold-container-compose.ts +10 -1
- package/src/templates/scaffold-container.ts +4 -2
- package/src/templates/scaffold-db-client.ts +4 -2
- 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-repo.ts +9 -1
- package/src/templates/scaffold-shell.ts +39 -36
- package/src/test-counts.ts +2 -6
- 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/trusted-hops-error.ts +17 -0
- package/src/unthrown-codes.ts +26 -0
- package/src/verify-checks.ts +17 -46
- package/src/verify-deadline.ts +130 -33
- package/src/verify-merge.ts +60 -27
- package/src/verify-part-step.ts +90 -0
- package/src/verify-run.ts +29 -8
- package/src/verify-shard.ts +13 -2
- package/src/verify-stalled.ts +152 -0
- package/src/verify-tests.ts +5 -2
- package/src/build-errors.ts +0 -54
- package/src/verify-errors.ts +0 -58
package/CLAUDE.md
CHANGED
|
@@ -44,8 +44,8 @@ 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`)
|
|
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
|
+
| `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 |
|
|
49
49
|
| `load-findings.ts` | module-load failures are reported once, by `manifest`; other steps point there |
|
|
50
50
|
| `boundary-findings.ts` / `app-boundaries.ts` / `boundary-cuts.ts` | surface + layer rules; a finding's `fix:` is the concrete cut |
|
|
51
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 |
|
|
@@ -53,16 +53,16 @@ Commands: `bun test packages/cli` (from the repo root — the test preload lives
|
|
|
53
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 |
|
|
54
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 |
|
|
55
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` | `policy`: every permission granted or required is declared,
|
|
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()`) |
|
|
57
57
|
| `job-registration.ts` | `manifest`: no job or task under a positional `anonymous-*` name (`X_JOB_UNREGISTERED`) |
|
|
58
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
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 |
|
|
60
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 |
|
|
61
61
|
| `app-agents-md.ts` / `app-env.ts` / `app-openapi.ts` / `app-manifest.ts` | `manifest` and `contract-diff` |
|
|
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 |
|
|
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
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
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` | host check: a registered code nothing throws
|
|
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` |
|
|
66
66
|
|
|
67
67
|
### Generators (`x g`, `x new`)
|
|
68
68
|
|
|
@@ -70,6 +70,7 @@ Commands: `bun test packages/cli` (from the repo root — the test preload lives
|
|
|
70
70
|
|---|---|
|
|
71
71
|
| `cmd-generate.ts` / `generate-files.ts` / `generate-kinds.ts` / `generate-write.ts` | argv → pure file list → writes (conflict-checked, `merge: 'json'` / `'if-absent'`) |
|
|
72
72
|
| `generate-feature.ts` | a `--feature` naming no slice is `X_FEATURE_UNKNOWN`; nothing invents an entity |
|
|
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 |
|
|
73
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 |
|
|
74
75
|
| `api-registration.ts` | a written job or task is listed in `apps/web/api/index.ts` |
|
|
75
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 |
|
|
@@ -88,11 +89,16 @@ Commands: `bun test packages/cli` (from the repo root — the test preload lives
|
|
|
88
89
|
| `cmd-dev.ts` / `dev-route-table.ts` | `x dev`: every role in one process, `/_x`, the watcher |
|
|
89
90
|
| `runtime-bindings.ts` | which service each binding points at (embedded or external); events follow `realtime.transport` / `urlEnv` |
|
|
90
91
|
| `runtime-queue.ts` / `runtime-services.ts` | the db + queue pair; everything else and every ambient accessor |
|
|
91
|
-
| `
|
|
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 |
|
|
92
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` |
|
|
93
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` |
|
|
94
99
|
| `role-sync.ts` / `role-replicator.ts` / `runtime-live-feed.ts` | the sync node, the change feed |
|
|
95
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 |
|
|
96
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) |
|
|
97
103
|
| `app-openapi.ts` | `openapi.json` (complete when `defineApi({ openapi })` is declared) and each bearer mount's own document; staleness for all of them |
|
|
98
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 |
|
|
@@ -102,6 +108,7 @@ Commands: `bun test packages/cli` (from the repo root — the test preload lives
|
|
|
102
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 |
|
|
103
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 |
|
|
104
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 |
|
|
105
112
|
|
|
106
113
|
### Build and data
|
|
107
114
|
|
|
@@ -118,7 +125,8 @@ Commands: `bun test packages/cli` (from the repo root — the test preload lives
|
|
|
118
125
|
| File | Job |
|
|
119
126
|
|---|---|
|
|
120
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 |
|
|
121
|
-
| `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` |
|
|
122
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 |
|
|
123
131
|
| `cmd-pr.ts` / `cmd-ci.ts` | GitHub through `gh`, parsed against a schema. Not gate steps |
|
|
124
132
|
| `error-catalog.ts` | imports every `@ultimat3/*` package so `x errors` answers for any code |
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Commands and the `x verify` step count, `As of 2026-08`:
|
|
|
16
16
|
| `x g <primitive> <name>` | scaffolds a primitive **with a passing test** | never a TODO stub |
|
|
17
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 |
|
|
18
18
|
| `x mcp serve` | `@ultimat3/mcp`'s 18 dev tools, over stdio or HTTP | one catalog, one scope set, both transports |
|
|
19
|
-
| `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)) |
|
|
20
20
|
| `x deploy` | container deploy plan | compose or helm; zero platform primitives |
|
|
21
21
|
| `x manifest` / `x routes` | generated facts | `x.manifest.json`, `openapi.json`, route table |
|
|
22
22
|
| `x actions` / `x queries` / `x entities` | the declaration registries | `list` and `describe <name>`, straight off the registries |
|
|
@@ -24,6 +24,7 @@ Commands and the `x verify` step count, `As of 2026-08`:
|
|
|
24
24
|
| `x jobs ls\|show\|retry\|cancel\|drain` | the queue | depth, dead letters, step traces, `retry --from-step`, `cancel --reason`, `drain --to` |
|
|
25
25
|
| `x test [type]` | one of the six test types, or all | same type rule as the gate; `--filter`, `--sample N` |
|
|
26
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 }` |
|
|
27
28
|
| `x secrets show\|init\|edit\|set\|rotate` | the committed encrypted secrets | decrypted into the `envSchema` variables of the same names |
|
|
28
29
|
| `x policy list\|explain <subject>` | which clause decided a permission, and why | five packages print `x policy explain` as a denial's `fix:` |
|
|
29
30
|
| `x i18n check\|add\|sync` | catalogs: gaps, a new locale, key sync | all three of i18n's own error fixes name it |
|
|
@@ -192,14 +193,13 @@ a job boundary the class is gone and the `code` is what survives — match on th
|
|
|
192
193
|
| Class | Code | Declared in |
|
|
193
194
|
|---|---|---|
|
|
194
195
|
| `BadFlagError` | `X_CLI_BAD_FLAG` | `src/errors.ts` |
|
|
195
|
-
| `BuildEntryMissingError` | `X_BUILD_ENTRY_MISSING` | `src/build
|
|
196
|
+
| `BuildEntryMissingError` | `X_BUILD_ENTRY_MISSING` | `src/cmd-build.ts` |
|
|
196
197
|
| `BunVersionError` | `X_BUN_VERSION` | `src/errors.ts` |
|
|
197
198
|
| `CatalogExistsError` | `X_GENERATE_CONFLICT` | `src/errors.ts` |
|
|
198
199
|
| `CdpBrowserMissingError` | `X_CDP_BROWSER_MISSING` | `src/cdp-errors.ts` |
|
|
199
200
|
| `CdpCallFailedError` | `X_CDP_CALL_FAILED` | `src/cdp-errors.ts` |
|
|
200
201
|
| `CdpLaunchFailedError` | `X_CDP_LAUNCH_FAILED` | `src/cdp-errors.ts` |
|
|
201
202
|
| `CdpTimeoutError` | `X_CDP_TIMEOUT` | `src/cdp-errors.ts` |
|
|
202
|
-
| `CliNotImplementedError` | `X_NOT_IMPLEMENTED` | `src/errors.ts` |
|
|
203
203
|
| `DeclarationUnknownError` | `X_DECLARATION_UNKNOWN` | `src/errors.ts` |
|
|
204
204
|
| `E2eAppFailedError` | `X_E2E_APP_FAILED` | `src/e2e-errors.ts` |
|
|
205
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",
|
|
@@ -39,33 +39,33 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@babel/core": "^8.0.1",
|
|
42
|
-
"@ultimat3/action": "
|
|
43
|
-
"@ultimat3/admin": "
|
|
44
|
-
"@ultimat3/ai": "
|
|
45
|
-
"@ultimat3/auth": "
|
|
46
|
-
"@ultimat3/cache": "
|
|
47
|
-
"@ultimat3/core": "
|
|
48
|
-
"@ultimat3/db": "
|
|
49
|
-
"@ultimat3/entity": "
|
|
50
|
-
"@ultimat3/flags": "
|
|
51
|
-
"@ultimat3/http": "
|
|
52
|
-
"@ultimat3/i18n": "
|
|
53
|
-
"@ultimat3/jobs": "
|
|
54
|
-
"@ultimat3/mail": "
|
|
55
|
-
"@ultimat3/manifest": "
|
|
56
|
-
"@ultimat3/mcp": "
|
|
57
|
-
"@ultimat3/money": "
|
|
58
|
-
"@ultimat3/notify": "
|
|
59
|
-
"@ultimat3/policy": "
|
|
60
|
-
"@ultimat3/pwa": "
|
|
61
|
-
"@ultimat3/query": "
|
|
62
|
-
"@ultimat3/realtime": "
|
|
63
|
-
"@ultimat3/render": "
|
|
64
|
-
"@ultimat3/schema": "
|
|
65
|
-
"@ultimat3/seo": "
|
|
66
|
-
"@ultimat3/storage": "
|
|
67
|
-
"@ultimat3/testing": "
|
|
68
|
-
"@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",
|
|
69
69
|
"babel-preset-solid": "^1.9.15"
|
|
70
70
|
}
|
|
71
71
|
}
|
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
|
|
package/src/api-registration.ts
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
// `x.manifest.json` and in every dead-letter trace — under a green gate (plan 101 slice 11 b).
|
|
5
5
|
// Actions and queries are listed for the TYPE: `Api` is what the browser's client is shaped from.
|
|
6
6
|
|
|
7
|
+
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
7
8
|
import { API_INDEX } from './app-root';
|
|
8
9
|
import { containedPath } from './generate-write';
|
|
10
|
+
import type { Finding } from './output';
|
|
9
11
|
import { camel } from './templates/naming';
|
|
10
12
|
import { wrapList } from './templates/wrap';
|
|
11
13
|
|
|
@@ -143,6 +145,93 @@ function withImport(source: string, importLine: string): string {
|
|
|
143
145
|
return lines.join('\n');
|
|
144
146
|
}
|
|
145
147
|
|
|
148
|
+
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
149
|
+
|
|
150
|
+
/** Every binding an import clause declares: `* as x`, `x`, `{ a, b as c, type d }`. */
|
|
151
|
+
const clauseBindings = (clause: string): readonly string[] =>
|
|
152
|
+
clause
|
|
153
|
+
.replace(/^type\s+/, '')
|
|
154
|
+
.replace(/[{}]/g, ',')
|
|
155
|
+
.split(',')
|
|
156
|
+
.map((part) =>
|
|
157
|
+
part
|
|
158
|
+
.trim()
|
|
159
|
+
.replace(/^type\s+/, '')
|
|
160
|
+
.replace(/^\*\s+as\s+/, ''),
|
|
161
|
+
)
|
|
162
|
+
.map(
|
|
163
|
+
(part) =>
|
|
164
|
+
part
|
|
165
|
+
.split(/\s+as\s+/)
|
|
166
|
+
.at(-1)
|
|
167
|
+
?.trim() ?? '',
|
|
168
|
+
)
|
|
169
|
+
.filter((name) => IDENTIFIER.test(name));
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Every top-level name the index declares, and what holds it: a namespace import by its
|
|
173
|
+
* specifier, anything else (`defineApi`, `api`, `Api`, a named import) by its own name. A second
|
|
174
|
+
* binding of any of them is a module that does not load — `import * as api` beside
|
|
175
|
+
* `export const api` is a ReferenceError in every app-loading command.
|
|
176
|
+
*/
|
|
177
|
+
function declaredBindings(source: string): ReadonlyMap<string, string> {
|
|
178
|
+
const held = new Map<string, string>();
|
|
179
|
+
for (const found of source.matchAll(/^import\s+([^;]*?)\s+from\s+'([^']+)';/gm)) {
|
|
180
|
+
const [, clause = '', specifier = ''] = found;
|
|
181
|
+
const namespace = /^\*\s+as\s+([A-Za-z_$][\w$]*)$/.exec(clause.trim())?.[1];
|
|
182
|
+
if (namespace !== undefined) held.set(namespace, specifier);
|
|
183
|
+
else for (const name of clauseBindings(clause)) held.set(name, `an import from ${specifier}`);
|
|
184
|
+
}
|
|
185
|
+
const declaration =
|
|
186
|
+
/^(?:export\s+)?(?:declare\s+)?(?:const|let|var|function\*?|class|type|interface|enum)\s+([A-Za-z_$][\w$]*)/gm;
|
|
187
|
+
for (const found of source.matchAll(declaration)) {
|
|
188
|
+
const [, name = ''] = found;
|
|
189
|
+
if (!held.has(name)) held.set(name, `the index's own ${name}`);
|
|
190
|
+
}
|
|
191
|
+
return held;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* A written primitive whose namespace binding the index already holds — for another module, or as
|
|
196
|
+
* one of its own names — is a finding, decided BEFORE anything is written. Skipped silently, the
|
|
197
|
+
* job stayed unregistered under a green run; inserted, the index stopped loading.
|
|
198
|
+
*/
|
|
199
|
+
export function apiBindingFindings(
|
|
200
|
+
source: string,
|
|
201
|
+
written: readonly string[],
|
|
202
|
+
fix: string,
|
|
203
|
+
): readonly Finding[] {
|
|
204
|
+
const held = new Map(declaredBindings(source));
|
|
205
|
+
const findings: Finding[] = [];
|
|
206
|
+
for (const entry of apiEntriesFor(written)) {
|
|
207
|
+
const holder = held.get(entry.binding);
|
|
208
|
+
if (holder === entry.specifier) continue;
|
|
209
|
+
if (holder === undefined) {
|
|
210
|
+
held.set(entry.binding, entry.specifier);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
findings.push({
|
|
214
|
+
code: 'X_GENERATE_CONFLICT',
|
|
215
|
+
cause: `${API_INDEX} binds every listed module under its file name, and "${entry.binding}" is already ${holder.startsWith('.') ? `the namespace of ${holder}` : holder} — ${entry.specifier} cannot be listed under it`,
|
|
216
|
+
fix,
|
|
217
|
+
docs: ERROR_DOCS_URL,
|
|
218
|
+
at: API_INDEX,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return findings;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** `apiBindingFindings` against the app's own index; none when the app has no index. */
|
|
225
|
+
export async function indexBindingFindings(
|
|
226
|
+
root: string,
|
|
227
|
+
written: readonly string[],
|
|
228
|
+
fix: string,
|
|
229
|
+
): Promise<readonly Finding[]> {
|
|
230
|
+
const file = containedPath(root, API_INDEX);
|
|
231
|
+
if (!(await Bun.file(file).exists())) return [];
|
|
232
|
+
return apiBindingFindings(await Bun.file(file).text(), written, fix);
|
|
233
|
+
}
|
|
234
|
+
|
|
146
235
|
/** Performs `insertApiEntries` on the app's index. Answers the paths it rewrote. */
|
|
147
236
|
export async function registerGeneratedPrimitives(
|
|
148
237
|
root: string,
|
package/src/app-artifacts.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The two generated contracts an app commits — `x.manifest.json` and `openapi.json` — written by one
|
|
2
2
|
// function, because two commands wrote them and only one wrote both: `x g` refreshed the manifest
|
|
3
|
-
// and left `openapi.json` behind, so its own output failed the `
|
|
3
|
+
// and left `openapi.json` behind, so its own output failed the gate's `manifest` step next.
|
|
4
4
|
|
|
5
5
|
import { join } from 'node:path'; // why: Bun ships no path-join primitive.
|
|
6
6
|
import type { Manifest } from '@ultimat3/manifest';
|
package/src/app-auth.ts
CHANGED
|
@@ -1,32 +1,9 @@
|
|
|
1
1
|
// The one fact `x dev` and `serve.ts` need out of `app.config.ts` before a listener binds: where
|
|
2
|
-
// this app's sign-in page is
|
|
3
|
-
// for the same reason — a regex over the app's source is the pattern `app-load.ts` refuses.
|
|
2
|
+
// this app's sign-in page is, off the one loader (`app-config-load.ts`).
|
|
4
3
|
|
|
5
|
-
import {
|
|
6
|
-
import { join } from 'node:path';
|
|
7
|
-
import { APP_CONFIG_FILE } from './app-root';
|
|
4
|
+
import { loadAppConfig } from './app-config-load';
|
|
8
5
|
|
|
9
|
-
/**
|
|
10
|
-
export const APP_CONFIG_EXPORT = 'config';
|
|
11
|
-
|
|
12
|
-
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
13
|
-
typeof value === 'object' && value !== null;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* `auth.signInPath`, or `null` when the app declares none.
|
|
17
|
-
*
|
|
18
|
-
* Structural, not `instanceof`: `defineConfig` returns a plain object, and a config that resolved
|
|
19
|
-
* through an older version of core simply has no `auth` section. `null` is the safe answer either
|
|
20
|
-
* way — it is what turns the browser redirect off and leaves the problem document in place.
|
|
21
|
-
*/
|
|
6
|
+
/** `auth.signInPath`, or `null` when the app declares none or the root has no config file. */
|
|
22
7
|
export async function loadSignInPath(root: string): Promise<string | null> {
|
|
23
|
-
|
|
24
|
-
if (!existsSync(configPath)) return null;
|
|
25
|
-
const module = (await import(configPath)) as Record<string, unknown>;
|
|
26
|
-
const config = module[APP_CONFIG_EXPORT];
|
|
27
|
-
if (!isRecord(config)) return null;
|
|
28
|
-
const auth = config['auth'];
|
|
29
|
-
if (!isRecord(auth)) return null;
|
|
30
|
-
const path = auth['signInPath'];
|
|
31
|
-
return typeof path === 'string' && path.startsWith('/') ? path : null;
|
|
8
|
+
return (await loadAppConfig(root))?.auth.signInPath ?? null;
|
|
32
9
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// The one reader of an app's `app.config.ts`: imported here and nowhere else in the tree
|
|
2
|
+
// (`bun run boundaries`, `X_CONFIG_IMPORT_OUTSIDE_LOADER`), its `config` export handed back through
|
|
3
|
+
// core's own `defineConfig` — so every boot fact the CLI reads is the validated, default-merged
|
|
4
|
+
// `AppConfig`, never a structural walk with a fallback of its own.
|
|
5
|
+
|
|
6
|
+
// why: Bun exposes no path-join primitive, and the config path is app-root-relative.
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import type { AppConfig, AppConfigInput } from '@ultimat3/core';
|
|
9
|
+
import { ConfigInvalidError, defineConfig, describeValue } from '@ultimat3/core';
|
|
10
|
+
import { APP_CONFIG_FILE } from './app-root';
|
|
11
|
+
|
|
12
|
+
/** The export every app declares. Named, never default — the CLI and the runtime both import it. */
|
|
13
|
+
export const APP_CONFIG_EXPORT = 'config';
|
|
14
|
+
|
|
15
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
16
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* One named export of `root`'s `app.config.ts`, or `undefined` when the root has no such file or
|
|
20
|
+
* the module does not export `name`. A module that will not import throws as it did — the callers
|
|
21
|
+
* that report rather than crash (`x doctor`'s env check) turn that into a finding.
|
|
22
|
+
*/
|
|
23
|
+
export async function appConfigExport(root: string, name: string): Promise<unknown> {
|
|
24
|
+
const path = join(root, APP_CONFIG_FILE);
|
|
25
|
+
if (!(await Bun.file(path).exists())) return undefined;
|
|
26
|
+
const module = (await import(path)) as Record<string, unknown>;
|
|
27
|
+
return Object.hasOwn(module, name) ? module[name] : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* `root`'s `AppConfig`, or `undefined` when the root has no `app.config.ts` — not an app, and each
|
|
32
|
+
* caller's "nothing declared" answer is its own. A file that IS there is held to `defineConfig`'s
|
|
33
|
+
* one validator: a hand-built `config` object, or one resolved through an older core, reaches the
|
|
34
|
+
* boot validated and defaulted exactly as one built by `defineConfig` does, which re-merges to
|
|
35
|
+
* itself. A file with no `config` object is refused rather than read as "every default".
|
|
36
|
+
*/
|
|
37
|
+
export async function loadAppConfig(root: string): Promise<AppConfig | undefined> {
|
|
38
|
+
const path = join(root, APP_CONFIG_FILE);
|
|
39
|
+
if (!(await Bun.file(path).exists())) return undefined;
|
|
40
|
+
const exported = await appConfigExport(root, APP_CONFIG_EXPORT);
|
|
41
|
+
if (!isRecord(exported)) {
|
|
42
|
+
throw new ConfigInvalidError({
|
|
43
|
+
cause: `${path} exports no ${APP_CONFIG_EXPORT} object (${describeValue(exported)})`,
|
|
44
|
+
fix: "export const config = defineConfig({ name: 'my-app' });",
|
|
45
|
+
meta: { key: APP_CONFIG_EXPORT },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
// A cast over a record whose structure `defineConfig` screens first (`shapeIssues`, per layer):
|
|
49
|
+
// the validator IS the parse, and a second hand-written schema here would be a second answer.
|
|
50
|
+
return defineConfig(exported as unknown as AppConfigInput);
|
|
51
|
+
}
|
package/src/app-env.ts
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
// `app.config.ts`, the `.env.example` projected from it, and the drift between the two. One
|
|
3
3
|
// declaration, both files (axiom 2) — nothing here holds a second list of variable names.
|
|
4
4
|
|
|
5
|
-
// why: Bun ships no synchronous exists; `existsSync` answers whether this root is an app.
|
|
6
|
-
import { existsSync } from 'node:fs';
|
|
7
5
|
// why: Bun exposes no path API — the two files this module reads are joined to the app root.
|
|
8
6
|
import { join } from 'node:path';
|
|
9
7
|
import type { EnvSchema, EnvVarDecl } from '@ultimat3/core';
|
|
@@ -13,6 +11,7 @@ import {
|
|
|
13
11
|
ERROR_DOCS_URL,
|
|
14
12
|
renderEnvExample,
|
|
15
13
|
} from '@ultimat3/core';
|
|
14
|
+
import { appConfigExport } from './app-config-load';
|
|
16
15
|
import { APP_CONFIG_FILE } from './app-root';
|
|
17
16
|
import type { Finding } from './output';
|
|
18
17
|
import { findingFrom } from './output';
|
|
@@ -45,17 +44,12 @@ export const isEnvSchema = (value: unknown): value is EnvSchema =>
|
|
|
45
44
|
isRecord(value) && Object.values(value).every(isDecl);
|
|
46
45
|
|
|
47
46
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* that will not import throws, and the caller turns that into the finding.
|
|
47
|
+
* The declaration `app.config.ts` exports, through the one loader (`app-config-load.ts`).
|
|
48
|
+
* `undefined` means "this root declares no environment"; a config that will not import throws, and
|
|
49
|
+
* the caller turns that into the finding.
|
|
52
50
|
*/
|
|
53
51
|
export async function loadEnvSchema(root: string): Promise<EnvSchema | undefined> {
|
|
54
|
-
const
|
|
55
|
-
if (!existsSync(configPath)) return undefined;
|
|
56
|
-
const module = (await import(configPath)) as Record<string, unknown>;
|
|
57
|
-
const declared = module[ENV_SCHEMA_EXPORT];
|
|
58
|
-
if (declared === undefined) return undefined;
|
|
52
|
+
const declared = await appConfigExport(root, ENV_SCHEMA_EXPORT);
|
|
59
53
|
return isEnvSchema(declared) ? declared : undefined;
|
|
60
54
|
}
|
|
61
55
|
|
package/src/app-manifest.ts
CHANGED
|
@@ -117,14 +117,8 @@ const routeFacts = (): readonly RouteFact[] =>
|
|
|
117
117
|
...budgetOf(route),
|
|
118
118
|
}));
|
|
119
119
|
|
|
120
|
-
function budgetOf(route: RouteDescriptor): { budget?: { js
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
budget: {
|
|
124
|
-
...(route.budgetJs === null ? {} : { js: route.budgetJs }),
|
|
125
|
-
...(route.budgetLcp === null ? {} : { lcp: route.budgetLcp }),
|
|
126
|
-
},
|
|
127
|
-
};
|
|
120
|
+
function budgetOf(route: RouteDescriptor): { budget?: { js: string } } {
|
|
121
|
+
return route.budgetJs === null ? {} : { budget: { js: route.budgetJs } };
|
|
128
122
|
}
|
|
129
123
|
|
|
130
124
|
/**
|
package/src/app-mcp.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
// why: a directory's existence — `Bun.file().exists()` answers for files, and `apps/` is a directory.
|
|
11
11
|
import { existsSync } from 'node:fs';
|
|
12
|
-
// why: Bun exposes no path-join primitive;
|
|
12
|
+
// why: Bun exposes no path-join primitive; each candidate is joined to root.
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { logger } from '@ultimat3/core';
|
|
15
15
|
import type { Route } from '@ultimat3/http';
|
|
@@ -20,8 +20,7 @@ import {
|
|
|
20
20
|
McpPathDuplicateError,
|
|
21
21
|
PROTECTED_RESOURCE_WELL_KNOWN,
|
|
22
22
|
} from '@ultimat3/mcp';
|
|
23
|
-
import {
|
|
24
|
-
import { APP_CONFIG_FILE } from './app-root';
|
|
23
|
+
import { loadAppConfig } from './app-config-load';
|
|
25
24
|
|
|
26
25
|
/** The one file an app writes, per app directory. */
|
|
27
26
|
export const APP_MCP_GLOB = 'apps/*/mcp.ts';
|
|
@@ -57,26 +56,9 @@ interface ExposeDeclaration {
|
|
|
57
56
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
58
57
|
typeof value === 'object' && value !== null;
|
|
59
58
|
|
|
60
|
-
/**
|
|
61
|
-
* `config.ai.mcp`, off the app's own resolved config — the exported `config` is what
|
|
62
|
-
* `defineConfig` returned, so both keys are present and defaulted. Read the same way
|
|
63
|
-
* `loadSignInPath` reads `auth.signInPath`: the config file is imported, never re-parsed.
|
|
64
|
-
* An app with no config file has nothing exposed and nothing to warn about.
|
|
65
|
-
*/
|
|
59
|
+
/** `config.ai.mcp` off the one loader (`app-config-load.ts`); nothing exposed with no config file. */
|
|
66
60
|
async function exposeDeclaration(root: string): Promise<ExposeDeclaration | undefined> {
|
|
67
|
-
|
|
68
|
-
if (!(await Bun.file(configPath).exists())) return undefined;
|
|
69
|
-
const module = (await import(configPath)) as Record<string, unknown>;
|
|
70
|
-
const config = module[APP_CONFIG_EXPORT];
|
|
71
|
-
if (!isRecord(config) || !isRecord(config['ai']) || !isRecord(config['ai']['mcp'])) {
|
|
72
|
-
return undefined;
|
|
73
|
-
}
|
|
74
|
-
const mcp = config['ai']['mcp'];
|
|
75
|
-
const path = mcp['path'];
|
|
76
|
-
return {
|
|
77
|
-
expose: mcp['expose'] === true,
|
|
78
|
-
path: typeof path === 'string' && path.startsWith('/') ? path : '/mcp',
|
|
79
|
-
};
|
|
61
|
+
return (await loadAppConfig(root))?.ai.mcp;
|
|
80
62
|
}
|
|
81
63
|
|
|
82
64
|
const isAppMcp = (value: unknown): value is AppMcp =>
|
|
@@ -213,8 +195,13 @@ function endpointRoutes(route: Descriptor, path: string, index: number): readonl
|
|
|
213
195
|
// The PUBLIC origin, as the pipeline resolved it (`ctx.https` honours a trusted proxy's
|
|
214
196
|
// `x-forwarded-proto`): behind a TLS-terminating ingress the raw request URL is `http://`
|
|
215
197
|
// on an internal host, and a `resource_metadata` naming that is a URL no client can reach.
|
|
198
|
+
// The client ADDRESS likewise: failed tokens are metered per address, and the descriptor
|
|
199
|
+
// has no other way to learn it.
|
|
216
200
|
handler: (request, ctx) =>
|
|
217
|
-
route.handle(request.raw, {
|
|
201
|
+
route.handle(request.raw, {
|
|
202
|
+
origin: selfOrigin(ctx.url, ctx.https),
|
|
203
|
+
address: ctx.ip ?? undefined,
|
|
204
|
+
}),
|
|
218
205
|
meta: { name, auth: 'public', enforcedBy: 'handler' },
|
|
219
206
|
},
|
|
220
207
|
...metadata,
|