@ultimat3/cli 24.0.0 → 25.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 -8
- package/README.md +30 -5
- package/package.json +29 -29
- package/src/admin-catalog-keys.ts +2 -2
- package/src/affected.ts +3 -1
- package/src/app-boundaries.ts +17 -4
- package/src/app-config-load.ts +3 -3
- package/src/app-env.ts +21 -14
- package/src/app-load.ts +38 -14
- package/src/app-manifest.ts +2 -2
- package/src/app-mcp.ts +25 -27
- package/src/app-name.ts +52 -0
- package/src/app-permissions-borrowed.ts +2 -8
- package/src/app-permissions-site.ts +31 -7
- package/src/app-root.ts +9 -5
- package/src/bin.ts +3 -7
- package/src/browser-launcher.ts +4 -2
- package/src/budgets-charged.ts +60 -0
- package/src/budgets-scan.ts +76 -0
- package/src/budgets-unmeasured.ts +91 -0
- package/src/budgets.ts +115 -121
- package/src/cdp-shot-driver.ts +17 -0
- package/src/cdp-shot-watch.ts +35 -31
- package/src/ci-log.ts +0 -0
- package/src/cmd-auth.ts +2 -2
- package/src/cmd-build-spec.ts +7 -1
- package/src/cmd-build.ts +89 -9
- package/src/cmd-db-backfill.ts +1 -1
- package/src/cmd-db-branch.ts +2 -2
- package/src/cmd-db.ts +5 -2
- package/src/cmd-deploy-compose.ts +94 -0
- package/src/cmd-deploy-drain.ts +48 -0
- package/src/cmd-deploy-start-first.ts +178 -0
- package/src/cmd-deploy.ts +158 -36
- package/src/cmd-dev.ts +18 -357
- package/src/cmd-doctor.ts +16 -66
- package/src/cmd-env.ts +29 -14
- package/src/cmd-generate.ts +12 -2
- package/src/cmd-i18n.ts +11 -8
- package/src/cmd-jobs-spec.ts +7 -22
- package/src/cmd-jobs.ts +18 -137
- package/src/cmd-mcp.ts +25 -7
- package/src/cmd-new.ts +36 -8
- package/src/cmd-planned.ts +9 -0
- package/src/cmd-routes.ts +3 -3
- package/src/cmd-shot-matrix.ts +21 -4
- package/src/cmd-shot-spec.ts +10 -2
- package/src/cmd-shot.ts +35 -39
- package/src/cmd-test.ts +1 -1
- package/src/coverage-lcov.ts +4 -2
- package/src/db-backfill.ts +2 -2
- package/src/db-replay-engine.ts +4 -10
- package/src/db-subscribes.ts +19 -16
- package/src/dev-boot.ts +290 -0
- package/src/dev-child-watch.ts +63 -12
- package/src/dev-dashboard-guard.ts +1 -1
- package/src/dev-dashboard.ts +5 -1
- package/src/dev-lock.ts +96 -24
- package/src/dev-rebuild.ts +80 -0
- package/src/dev-restart-retire.ts +46 -0
- package/src/dev-route-table.ts +30 -8
- package/src/dev-supervisor.ts +114 -22
- package/src/dev-traces.ts +53 -20
- package/src/dispatch.ts +14 -7
- package/src/doctor-auth.ts +4 -4
- package/src/doctor-offline.ts +6 -5
- package/src/document-options.ts +7 -0
- package/src/drift-replica-identity.ts +2 -2
- package/src/drift.ts +29 -6
- package/src/error-codes.ts +6 -0
- package/src/error-page-csp.ts +14 -15
- package/src/exec.ts +8 -0
- package/src/favicon.ts +2 -2
- package/src/fix-command.ts +9 -1
- package/src/framework-env.ts +148 -0
- package/src/framework-schema.ts +13 -2
- package/src/generate-catalog-locales.ts +79 -0
- package/src/generate-kinds.ts +3 -17
- package/src/generate-plural.ts +50 -0
- package/src/generate-shadow.ts +50 -1
- package/src/hold.ts +56 -1
- package/src/i18n-audit.ts +21 -8
- package/src/i18n-index.ts +5 -6
- package/src/i18n-registration.ts +7 -9
- package/src/import-scan.ts +28 -3
- package/src/index.ts +1 -3
- package/src/island-bundle.ts +54 -64
- package/src/island-capture.ts +1 -1
- package/src/island-duplicate-refusal.ts +57 -0
- package/src/island-duplicates.ts +221 -0
- package/src/island-entry-missing.ts +64 -0
- package/src/island-harness-route.ts +20 -4
- package/src/island-harness-script.ts +23 -2
- package/src/island-identity.ts +17 -14
- package/src/island-package-dedupe.ts +150 -0
- package/src/island-realtime.ts +36 -16
- package/src/island-routes.ts +2 -2
- package/src/island-runtime.ts +241 -0
- package/src/island-sources.ts +105 -0
- package/src/island-states-load.ts +6 -0
- package/src/island-store.ts +90 -6
- package/src/jobs-json.ts +0 -26
- package/src/jobs-report.ts +32 -16
- package/src/local-cli-handoff.ts +90 -0
- package/src/mcp-errors.ts +8 -1
- package/src/mcp-host.ts +6 -5
- package/src/mcp-ui-inspect.ts +3 -2
- package/src/mcp-ui-interact.ts +3 -2
- package/src/mcp-ui.ts +8 -7
- package/src/measure-scope.ts +15 -10
- package/src/messages.ts +27 -16
- package/src/metrics-endpoint.ts +3 -2
- package/src/migration-extensions.ts +1 -1
- package/src/otlp-export.ts +2 -2
- package/src/output.ts +18 -0
- package/src/page-speculation.ts +14 -11
- package/src/parse.ts +0 -8
- package/src/permission-grants.ts +3 -3
- package/src/posix-path.ts +30 -0
- package/src/prerender-locales.ts +6 -4
- package/src/prerender-report.ts +58 -0
- package/src/prerender-served.ts +52 -0
- package/src/prerender.ts +63 -98
- package/src/pwa-artifacts.ts +8 -1
- package/src/pwa-manifest-locales.ts +9 -9
- package/src/retry-memo.ts +1 -1
- package/src/role-realtime.ts +7 -1
- package/src/role-replicator.ts +110 -31
- package/src/role-start.ts +64 -85
- package/src/role-sync.ts +65 -6
- package/src/root-env.ts +157 -28
- package/src/route-islands.ts +69 -0
- package/src/runtime-admin.ts +4 -1
- package/src/runtime-assets.ts +15 -2
- package/src/runtime-bindings.ts +23 -3
- package/src/runtime-cache.ts +8 -8
- package/src/runtime-hooks.ts +2 -1
- package/src/runtime-isr-outcome.ts +100 -0
- package/src/runtime-isr.ts +2 -2
- package/src/runtime-jobs.ts +78 -18
- package/src/runtime-live-feed.ts +1 -1
- package/src/runtime-mail.ts +26 -0
- package/src/runtime-mfa-warning.ts +2 -2
- package/src/runtime-overrides.ts +3 -3
- package/src/runtime-purge.ts +1 -1
- package/src/runtime-queue.ts +12 -12
- package/src/runtime-realtime.ts +5 -3
- package/src/runtime-render.ts +77 -82
- package/src/runtime-replica.ts +3 -8
- package/src/runtime-services.ts +33 -13
- package/src/runtime-storage-upload.ts +2 -2
- package/src/runtime-storage.ts +3 -3
- package/src/schema-diff.ts +1 -1
- package/src/secrets-rotation.ts +8 -9
- package/src/seo-routes.ts +2 -2
- package/src/serve-boot.ts +22 -11
- package/src/serve-drain.ts +3 -1
- package/src/serve-retire.ts +95 -0
- package/src/serve-web.ts +19 -8
- package/src/serve.ts +27 -10
- package/src/server-barrels.ts +6 -3
- package/src/shot-browser.ts +1 -1
- package/src/shot-locale.ts +6 -15
- package/src/shot-settle.ts +3 -3
- package/src/shot-theme.ts +1 -1
- package/src/shot-viewport.ts +152 -0
- package/src/signal-shred.ts +26 -12
- package/src/site-asset-routes.ts +2 -2
- package/src/site-seo.ts +6 -4
- package/src/source-files.ts +13 -3
- package/src/static-report.ts +5 -0
- package/src/style-routes.ts +2 -2
- package/src/sw-artifacts.ts +8 -8
- package/src/sw-precache-plan.ts +34 -4
- package/src/sync-authenticator.ts +3 -3
- package/src/sync-url.ts +1 -1
- package/src/templates/action.ts +4 -4
- package/src/templates/backfill.ts +4 -4
- package/src/templates/entity.ts +2 -2
- package/src/templates/github/ci.yml.ts +28 -18
- package/src/templates/github/image.yml.ts +92 -0
- package/src/templates/github/runner.ts +18 -0
- package/src/templates/guard-bare-error.ts +16 -2
- package/src/templates/guard-unzoned-date.ts +56 -6
- package/src/templates/index.ts +1 -1
- package/src/templates/job-body-test.ts +2 -2
- package/src/templates/job.ts +6 -6
- package/src/templates/query.ts +9 -8
- package/src/templates/resource-page.ts +9 -7
- package/src/templates/resource-service.ts +2 -2
- package/src/templates/route.ts +23 -8
- package/src/templates/scaffold-api.ts +2 -3
- package/src/templates/scaffold-app.ts +2 -1
- package/src/templates/scaffold-auth.ts +205 -70
- package/src/templates/scaffold-bin.ts +122 -0
- package/src/templates/scaffold-claude-commands.ts +11 -11
- package/src/templates/scaffold-container.ts +12 -6
- package/src/templates/scaffold-dashboard-bare.ts +8 -4
- package/src/templates/scaffold-dashboard-example-tests.ts +208 -0
- package/src/templates/scaffold-dashboard-example.ts +112 -172
- package/src/templates/scaffold-dashboard-shared.ts +62 -1
- package/src/templates/scaffold-dashboard.ts +9 -5
- package/src/templates/scaffold-db-package.ts +2 -2
- package/src/templates/scaffold-docs.ts +20 -77
- package/src/templates/scaffold-domain-package.ts +0 -4
- package/src/templates/scaffold-entries.ts +4 -3
- package/src/templates/scaffold-env.ts +6 -3
- package/src/templates/scaffold-gitattributes.ts +59 -0
- package/src/templates/scaffold-helm-templates.ts +74 -18
- package/src/templates/scaffold-helm.ts +31 -4
- package/src/templates/scaffold-http.ts +1 -1
- package/src/templates/scaffold-i18n.ts +15 -2
- package/src/templates/scaffold-icon.ts +2 -2
- package/src/templates/scaffold-repo.ts +18 -15
- package/src/templates/scaffold-shell.ts +17 -2
- package/src/test-only-shipped.ts +193 -0
- package/src/test-select.ts +2 -1
- package/src/test-shards.ts +1 -4
- package/src/theme-boot.ts +2 -2
- package/src/theme-brand.ts +66 -0
- package/src/trusted-hops.ts +49 -0
- package/src/unscanned-admin.ts +2 -2
- package/src/verify-checks.ts +12 -2
- package/src/verify-coverage-run.ts +3 -2
- package/src/verify-deadline.ts +87 -10
- package/src/verify-merge.ts +48 -16
- package/src/verify-part-step.ts +10 -2
- package/src/verify-role-load-probe.ts +12 -2
- package/src/verify-role-load.ts +9 -4
- package/src/verify-run.ts +3 -0
- package/src/verify-step.ts +2 -0
- package/src/verify-test-run.ts +3 -2
- package/src/verify-tests.ts +0 -1
- package/src/worker-bundle.ts +2 -2
- package/src/workspace-checks.ts +53 -7
- package/src/workspace-graph.ts +3 -2
- package/src/write-line.ts +82 -7
- package/src/browser-launcher-fake-html.ts +0 -80
- package/src/browser-launcher-fake.ts +0 -168
- package/src/jobs-drain.ts +0 -230
- package/src/scaffold-typecheck.ts +0 -227
- package/src/thrown-by.ts +0 -24
package/CLAUDE.md
CHANGED
|
@@ -52,14 +52,14 @@ Commands: `bun test packages/cli` (from the repo root — the test preload lives
|
|
|
52
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
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
|
-
| `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 |
|
|
55
|
+
| `workspace-checks.ts` / `test-only-shipped.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; a `src/` module only tests import must be `*-fixture.ts` (`X_PACKAGE_TEST_ONLY_SHIPPED`) |
|
|
56
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
|
-
| `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
|
-
| `app-agents-md.ts` / `app-env.ts` / `app-openapi.ts` / `app-manifest.ts` | `manifest` and `contract-diff` |
|
|
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
|
|
60
|
+
| `async-pages.ts` / `live-routes.ts` / `budgets.ts` / `budgets-unmeasured.ts` | `budgets` riders: an async `Page` with no `load`, a live read no island imports, bytes per route; an unweighed route's finding names a refused measurement actor and `defineMeasurementActor()` |
|
|
61
|
+
| `app-agents-md.ts` / `app-env.ts` / `app-openapi.ts` / `app-manifest.ts` | `manifest` and `contract-diff`. `framework-env.ts` is the ONE list of framework-required deploy secrets `.env.example`, `x env check` and `x doctor` read |
|
|
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 needs included — never a `subscribes:` table, #518), 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
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` |
|
|
@@ -70,7 +70,10 @@ 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
|
+
| `generate-shadow.ts` | a name whose type spelling a planned file also uses as a global or local type (`promise`, `row`), or whose camel VALUE a planned file both imports and declares (`x g job job`, TS2440), is `X_CLI_BAD_FLAG`, decided on the planned files |
|
|
74
|
+
| `generate-plural.ts` | a planned `entity.ts` in an already-plural slice (`posts` → `postses`) is `X_CLI_BAD_FLAG`; the fix names the singular |
|
|
75
|
+
| `generate-catalog-locales.ts` | `--locales` never starts a catalog in an app with any: a missing non-default locale is `X_CLI_BAD_FLAG`, `fix: x i18n add <locale>`; non-default values land `⟦…⟧`-marked (`seedCatalog`) |
|
|
76
|
+
| `app-name.ts` | `x new`'s slug is asked of core's `defineConfig`, never a copied pattern; refused with a suggested name (`9lives` → `app-9lives`, `a` → `a-app`, cut at 64) |
|
|
74
77
|
| `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 |
|
|
75
78
|
| `api-registration.ts` | a written job or task is listed in `apps/web/api/index.ts` |
|
|
76
79
|
| `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 |
|
|
@@ -86,18 +89,21 @@ Commands: `bun test packages/cli` (from the repo root — the test preload lives
|
|
|
86
89
|
| `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
90
|
| `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
91
|
| `module-imports.ts` | the files a module's source imports, resolved once for both graphs: `referenced` (reload) and `evaluated` (document) |
|
|
89
|
-
| `cmd-dev.ts` / `dev-route-table.ts` | `x dev`: every role in one process, `/_x`, the watcher |
|
|
92
|
+
| `cmd-dev.ts` / `dev-boot.ts` / `dev-rebuild.ts` / `dev-route-table.ts` | `x dev`: the command (`cmd-dev.ts`, re-exports `startDev`); the boot — every role in one process, `/_x`, the watcher (`dev-boot.ts`); one save rebuilt as one generation (`dev-rebuild.ts`) |
|
|
90
93
|
| `runtime-bindings.ts` | which service each binding points at (embedded or external); events follow `realtime.transport` / `urlEnv` |
|
|
91
94
|
| `runtime-queue.ts` / `runtime-services.ts` | the db + queue pair; everything else and every ambient accessor |
|
|
92
95
|
| `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
96
|
| `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
97
|
| `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
98
|
| `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`) |
|
|
99
|
+
| `theme-brand.ts` | the ONE reader of an app's theme: `export const brand` in `apps/web/shared/theme.ts` (ui's `defineTheme`), inlined after the surface stylesheet with its hash in `style-src`; none → nothing emitted |
|
|
96
100
|
| `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 |
|
|
101
|
+
| `runtime-jobs.ts` (queues) | union of `jobs.queues` + registered queues; `WORKER_QUEUES` set = the EXACT set, `jobs.worker.queue-unserved` names the rest. `jobs.concurrency` passes as written |
|
|
102
|
+
| `serve-retire.ts` / `dev-restart-retire.ts` | the worker retire — `stop()`: claim nothing, abort nothing — on SIGUSR2 under `runRole` (armed before boot, then drain + exit 0; none on win32; the chart's `roles.worker.retireSeconds` preStop sends it), and before a supervised `x dev` restart's drain (#677) |
|
|
97
103
|
| `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
104
|
| `role-wake.ts` | the worker's LISTEN session (`@ultimat3/jobs`' `startQueueWake`) — worker role only, only over a client that `canListen` — and `queue_wake_live` |
|
|
99
105
|
| `role-sync.ts` / `role-replicator.ts` / `runtime-live-feed.ts` | the sync node, the change feed |
|
|
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 |
|
|
106
|
+
| `runtime-render.ts` / `runtime-isr-outcome.ts` / `route-islands.ts` / `runtime-assets.ts` / `runtime-storage.ts` / `runtime-hooks.ts` / `api-routes.ts` | the HTTP surface: pages (an `isr` hit never runs `load`, and a `load` redirect is answered per request, never stored or shared — `runtime-isr-outcome.ts`; `route-islands.ts` is a render's island collector and realtime boot), `/icons` + `/media`, `/_storage`, authz, the app's API — plus its bearer mounts (`apiMountRoutes`) and pages' bound `POST`s (`pagePostRoutes`), mounted by BOTH boots |
|
|
101
107
|
| `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 |
|
|
102
108
|
| `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) |
|
|
103
109
|
| `app-openapi.ts` | `openapi.json` (complete when `defineApi({ openapi })` is declared) and each bearer mount's own document; staleness for all of them |
|
|
@@ -105,7 +111,8 @@ Commands: `bun test packages/cli` (from the repo root — the test preload lives
|
|
|
105
111
|
| `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 |
|
|
106
112
|
| `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 |
|
|
107
113
|
| `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 |
|
|
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 |
|
|
114
|
+
| `island-bundle.ts` / `island-store.ts` / `island-sources.ts` / `island-realtime.ts` / `island-runtime.ts` / `solid-loader.ts` | islands: one `Bun.build` each, source-addressed; the image build writes a verified store the container loads — each chunk's recorded source files (`island-sources.ts`) are rehashed at read, so an edit since the build is a rebuild, never the old chunk. `island-runtime.ts` builds realtime's page runtime ONCE per build as `/islands/page-runtime.<id>.js` (resolved through `@ultimat3/realtime/page-runtime`), which a realtime island awaits where no page boot is rendered |
|
|
115
|
+
| `local-cli.ts` / `local-cli-handoff.ts` | a global `x` hands over to the app's own CLI (`local-cli.ts` decides whether); the hand-off forwards SIGTERM/SIGHUP at once and a SIGINT only if the child is still running after a 1 s grace (a terminal Ctrl-C already reached it), and exits with the child's code |
|
|
109
116
|
| `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 |
|
|
110
117
|
| `dev-*.ts` | `x dev` only: dashboard sources, traces, the N+1 ledger, the watcher, the reload, the lock, the port |
|
|
111
118
|
| `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 |
|
package/README.md
CHANGED
|
@@ -16,14 +16,14 @@ 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, `APP_URL` | every finding carries a fix command; probes the port `x dev` binds ([which one](../../wiki/CLI-Reference.md#x-doctor)) |
|
|
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)); the framework's deploy secrets only where the machine names `staging`/`production` |
|
|
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 |
|
|
23
23
|
| `x tasks list\|show` | cron tasks | timezone and next run, off `registeredTasks()` |
|
|
24
|
-
| `x jobs ls\|show\|retry\|cancel\|
|
|
24
|
+
| `x jobs ls\|show\|retry\|cancel\|rm\|promote\|pause\|resume` | the queue | depth, dead letters, step traces, `retry --from-step`, `cancel --reason`; `ls --json` pages as `rows`/`nextCursor`/`hasMore`; `drain` is planned (`X_NOT_IMPLEMENTED`) |
|
|
25
25
|
| `x test [type]` | one of the six test types, or all | same type rule as the gate; `--filter`, `--sample N` |
|
|
26
|
-
| `x env check\|example` | the typed environment `envSchema` declares | and the `.env.example` rendered from it |
|
|
26
|
+
| `x env check\|example` | the typed environment `envSchema` declares | and the `.env.example` rendered from it, plus the framework's deploy-required secrets (`ULTIMATE_CURSOR_SECRET`, `STORAGE_SIGNING_SECRET` unless `S3_ENDPOINT` is set), which `check` asks for wherever the boot refuses them — outside `development`/`test`, or with no environment named |
|
|
27
27
|
| `x auth seal-mfa` | one-shot auth maintenance | seals every `x_users.mfa_secret` still in the clear; idempotent, `{ sealed, alreadySealed, skipped }` |
|
|
28
28
|
| `x secrets show\|init\|edit\|set\|rotate` | the committed encrypted secrets | decrypted into the `envSchema` variables of the same names |
|
|
29
29
|
| `x policy list\|explain <subject>` | which clause decided a permission, and why | five packages print `x policy explain` as a denial's `fix:` |
|
|
@@ -36,6 +36,31 @@ Everything in [CLI reference](../../wiki/CLI-Reference.md)'s planned table is al
|
|
|
36
36
|
and exits `X_NOT_IMPLEMENTED` with a `fix:` naming the closest shipped command — "not built yet"
|
|
37
37
|
and "not a command" are different facts.
|
|
38
38
|
|
|
39
|
+
## `x build`: who a weighed page renders as
|
|
40
|
+
|
|
41
|
+
`x build --target static` renders every budgeted `app/` route it cannot publish only to weigh it,
|
|
42
|
+
then discards the document. That render runs as core's `measurementActor()`: the actor
|
|
43
|
+
`defineMeasurementActor()` declares in `app.config.ts`, else a **service actor holding `*` with no
|
|
44
|
+
`orgId` and no roles**. A page whose policy names a role, or whose `load` reads a tenant-scoped
|
|
45
|
+
entity, is refused under the default, and the `budgets` step reports it as `X_BUDGET_UNMEASURED`
|
|
46
|
+
with the actor, the refusal's code and cause (`X_FORBIDDEN`, `X_UNAUTHENTICATED`,
|
|
47
|
+
`X_TENANCY_ACTOR_ORG_REQUIRED`), and the edit that weighs it:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// app.config.ts — the one file every build imports; never used for a published site/ artifact
|
|
51
|
+
import { defineMeasurementActor, userActor } from '@ultimat3/core';
|
|
52
|
+
|
|
53
|
+
/** The org your dev seed creates. */
|
|
54
|
+
const DEMO_ORG_ID = '00000000-0000-4000-8000-000000000001';
|
|
55
|
+
|
|
56
|
+
defineMeasurementActor(() => userActor({ id: 'measure', orgId: DEMO_ORG_ID, roles: ['dev'] }));
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Pick the org your dev seed creates and the most-privileged role your gated pages require: the
|
|
60
|
+
weigh measures the page a holder of that role is served. Any other refusal (a missing database, a
|
|
61
|
+
`TypeError` in `load`) keeps the generic `fix:` — the `unmeasured` list of
|
|
62
|
+
`x build --target static --json` — with the code already in the cause.
|
|
63
|
+
|
|
39
64
|
## Which `x` runs
|
|
40
65
|
|
|
41
66
|
**The app's own.** `x` is a workspace dependency, and `bunx x` / the `package.json` scripts resolve
|
|
@@ -136,7 +161,7 @@ that name where there is one (`x new` writes all seventeen), the blank template
|
|
|
136
161
|
| `cmd-*.ts` | one command group each |
|
|
137
162
|
| `templates/` | scaffolding as typed string modules, not copied fixtures |
|
|
138
163
|
| `app-load.ts` | import an app's modules so the framework registries hold it |
|
|
139
|
-
| `app-mcp.ts` | the app's own MCP endpoint: `apps/<app>/mcp.ts` exports `mcp`, and both boots mount `POST
|
|
164
|
+
| `app-mcp.ts` | the app's own MCP endpoint: `apps/<app>/mcp.ts` exports `mcp`, and both boots mount `POST` at each endpoint's own `defineAppMcp({ path })` through this one call |
|
|
140
165
|
| `app-runtime.ts` | the app's `RuntimeOverrides`: `apps/<app>/runtime.ts` exports `runtime`, read by `x dev` and by `runRole` when its caller passed none |
|
|
141
166
|
| `local-cli.ts` | which `x` runs: a global CLI inside an app hands over to the app's own, because a second module instance is an empty registry |
|
|
142
167
|
| `measurement-actor.ts` | the actor a weigh-and-discard render runs as — every permission, never served |
|
|
@@ -146,7 +171,7 @@ that name where there is one (`x new` writes all seventeen), the blank template
|
|
|
146
171
|
| `app-boundaries.ts` | app import boundaries, over `@ultimat3/render`'s surface check |
|
|
147
172
|
| `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` |
|
|
148
173
|
| `app-agents-md.ts` | `AGENTS.md` exists and stays short, over `@ultimat3/manifest`'s check |
|
|
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 |
|
|
174
|
+
| `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. `ROLE=worker` also retires on SIGUSR2 (`serve-retire.ts`, 25.0.0): stop claiming, finish every held job, abort nothing, drain, exit 0 — what the chart's opt-in `roles.worker.retireSeconds` `preStop` sends; a no-op on Windows ([Deployment](../../wiki/Deployment.md#retiring-a-worker-before-sigterm)) |
|
|
150
175
|
| `prerender.ts` | `x build --target static`: which `site/` routes qualify, and where the bytes land |
|
|
151
176
|
| `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
177
|
| `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 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "25.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",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"LICENSE"
|
|
31
31
|
],
|
|
32
32
|
"engines": {
|
|
33
|
-
"bun": ">=1.4.
|
|
33
|
+
"bun": ">=1.4.2"
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
36
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
@@ -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": "25.0.0",
|
|
43
|
+
"@ultimat3/admin": "25.0.0",
|
|
44
|
+
"@ultimat3/ai": "25.0.0",
|
|
45
|
+
"@ultimat3/auth": "25.0.0",
|
|
46
|
+
"@ultimat3/cache": "25.0.0",
|
|
47
|
+
"@ultimat3/core": "25.0.0",
|
|
48
|
+
"@ultimat3/db": "25.0.0",
|
|
49
|
+
"@ultimat3/entity": "25.0.0",
|
|
50
|
+
"@ultimat3/flags": "25.0.0",
|
|
51
|
+
"@ultimat3/http": "25.0.0",
|
|
52
|
+
"@ultimat3/i18n": "25.0.0",
|
|
53
|
+
"@ultimat3/jobs": "25.0.0",
|
|
54
|
+
"@ultimat3/mail": "25.0.0",
|
|
55
|
+
"@ultimat3/manifest": "25.0.0",
|
|
56
|
+
"@ultimat3/mcp": "25.0.0",
|
|
57
|
+
"@ultimat3/money": "25.0.0",
|
|
58
|
+
"@ultimat3/notify": "25.0.0",
|
|
59
|
+
"@ultimat3/policy": "25.0.0",
|
|
60
|
+
"@ultimat3/pwa": "25.0.0",
|
|
61
|
+
"@ultimat3/query": "25.0.0",
|
|
62
|
+
"@ultimat3/realtime": "25.0.0",
|
|
63
|
+
"@ultimat3/render": "25.0.0",
|
|
64
|
+
"@ultimat3/schema": "25.0.0",
|
|
65
|
+
"@ultimat3/seo": "25.0.0",
|
|
66
|
+
"@ultimat3/storage": "25.0.0",
|
|
67
|
+
"@ultimat3/testing": "25.0.0",
|
|
68
|
+
"@ultimat3/time": "25.0.0",
|
|
69
69
|
"babel-preset-solid": "^1.9.15"
|
|
70
70
|
}
|
|
71
71
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// walked here — the admin owns the one derivation, so a key `defineAdmin` would draw and the
|
|
3
3
|
// `i18n` step audits can never be two spellings of one rule.
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { describePages } from '@ultimat3/render';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* The keys of every admin this process declared. Asked of the route table first: an app with no
|
|
@@ -10,7 +10,7 @@ import { describeRoutes } from '@ultimat3/render';
|
|
|
10
10
|
* the dynamic import below is a cache hit.
|
|
11
11
|
*/
|
|
12
12
|
export async function mountedAdminKeys(): Promise<readonly string[]> {
|
|
13
|
-
if (!
|
|
13
|
+
if (!describePages().some((route) => route.mount?.by === 'defineAdmin')) return [];
|
|
14
14
|
const { adminMounts } = await import('@ultimat3/admin');
|
|
15
15
|
return [...new Set(adminMounts().flatMap((app) => app.catalogKeys()))].sort();
|
|
16
16
|
}
|
package/src/affected.ts
CHANGED
|
@@ -181,7 +181,9 @@ export async function gitRoot(runner: Runner, cwd: string, command: string): Pro
|
|
|
181
181
|
throw new UltimateError({
|
|
182
182
|
code: 'X_CLI_UNEXPECTED',
|
|
183
183
|
cause: `x ${command} reads its diff from git and "git rev-parse --show-toplevel" exited ${result.code} in ${cwd}: ${singleLine(execOutput(result))}`,
|
|
184
|
-
|
|
184
|
+
// Repeating `git rev-parse` HERE cannot help — it is what failed; the fix names how to get INTO
|
|
185
|
+
// a checkout from anywhere, then re-run.
|
|
186
|
+
fix: `cd "$(git -C '<a directory inside your checkout>' rev-parse --show-toplevel)" # then run x ${command} there — ${cwd} is not inside a git checkout`,
|
|
185
187
|
});
|
|
186
188
|
}
|
|
187
189
|
|
package/src/app-boundaries.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { commentSafe } from './comment-safe';
|
|
|
25
25
|
import { scanRuntimeImports, stripShebang } from './import-scan';
|
|
26
26
|
import type { Finding } from './output';
|
|
27
27
|
import { hasPathSegment } from './path-segments';
|
|
28
|
+
import { toPosix } from './posix-path';
|
|
28
29
|
import { quoteArg } from './shell-quote';
|
|
29
30
|
|
|
30
31
|
export const BOUNDARY_CODES = [
|
|
@@ -61,11 +62,21 @@ export const boundaryCodeOf = (rule: BoundaryRule): BoundaryCode => CODE_OF[rule
|
|
|
61
62
|
|
|
62
63
|
const isRoute = (path: string): boolean => /\/(page|layout|route)\.[cm]?tsx?$/.test(path);
|
|
63
64
|
const isService = (path: string): boolean => /\/service\.[cm]?ts$/.test(path);
|
|
65
|
+
/**
|
|
66
|
+
* A slice's `repo.ts` counts: it is the typed handle one hop away, and a page reading it skips the
|
|
67
|
+
* query that carries the read's policy, bound and cache tag — the scaffold's own dashboard showed
|
|
68
|
+
* every post to a viewer holding `dashboard:read` alone. Matched as a whole module name, resolved
|
|
69
|
+
* (`…/repo.ts`), NodeNext-spelled (`./repo.js`, `.mjs`, `.cjs`) or bare (`../post/repo`), so
|
|
70
|
+
* `report` and `repository` are names, not repos.
|
|
71
|
+
*/
|
|
72
|
+
const isRepoSpecifier = (specifier: string): boolean =>
|
|
73
|
+
/(^|\/)repo(\/index)?(\.[cm]?[jt]sx?)?$/.test(specifier);
|
|
64
74
|
const isDbSpecifier = (specifier: string): boolean =>
|
|
65
75
|
/(^|\/)packages\/db($|\/)/.test(specifier) ||
|
|
66
76
|
specifier.endsWith('/db') ||
|
|
67
77
|
/^@[^/]+\/db$/.test(specifier) ||
|
|
68
|
-
specifier === 'drizzle-orm'
|
|
78
|
+
specifier === 'drizzle-orm' ||
|
|
79
|
+
isRepoSpecifier(specifier);
|
|
69
80
|
/**
|
|
70
81
|
* `node:http` and `node:https` are HTTP too, and the anchor could not reach them: `http` had to
|
|
71
82
|
* follow a `/` or start the string, and a `node:` specifier has a colon there. So the one spelling
|
|
@@ -104,7 +115,9 @@ export function resolveSpecifier(
|
|
|
104
115
|
* the form `CANDIDATE_SUFFIXES` resolves and the form every app source already writes.
|
|
105
116
|
*/
|
|
106
117
|
export function relativeSpecifier(fromFile: string, target: string): string {
|
|
107
|
-
|
|
118
|
+
// Through `toPosix` even over the POSIX `relative`: a key spelt `apps\web\…` is ONE segment to
|
|
119
|
+
// it, and the specifier came out `./apps\web\…` — an import no host resolves.
|
|
120
|
+
const path = relative(dirname(toPosix(fromFile)), toPosix(target)).replace(/\.[cm]?tsx?$/, '');
|
|
108
121
|
return path.startsWith('.') ? path : `./${path}`;
|
|
109
122
|
}
|
|
110
123
|
|
|
@@ -190,7 +203,7 @@ function layerFindings(scanned: readonly ScannedFile[]): readonly Finding[] {
|
|
|
190
203
|
if (isRoute(file.path) && isDbSpecifier(specifier)) {
|
|
191
204
|
findings.push({
|
|
192
205
|
code: 'X_BOUNDARY_ROUTE_TO_DB',
|
|
193
|
-
cause: `route imports the database ("${specifier}") — routes call actions and queries`,
|
|
206
|
+
cause: `route imports the database ("${specifier}")${isRepoSpecifier(specifier) ? ', through a repo' : ''} — routes call actions and queries`,
|
|
194
207
|
fix: generate('query', file.path, 'then call it from'),
|
|
195
208
|
docs: ERROR_DOCS_URL,
|
|
196
209
|
at: file.path,
|
|
@@ -232,7 +245,7 @@ export async function readAppSources(root: string): Promise<readonly SourceFile[
|
|
|
232
245
|
const glob = new Bun.Glob(pattern);
|
|
233
246
|
for await (const path of glob.scan({ cwd: root, absolute: false })) {
|
|
234
247
|
if (hasPathSegment(path, 'node_modules') || path.includes('.test.')) continue;
|
|
235
|
-
const posix = path
|
|
248
|
+
const posix = toPosix(path);
|
|
236
249
|
files.push({ path: posix, source: await Bun.file(joinPath(root, posix)).text() });
|
|
237
250
|
}
|
|
238
251
|
}
|
package/src/app-config-load.ts
CHANGED
|
@@ -30,9 +30,9 @@ export async function appConfigExport(root: string, name: string): Promise<unkno
|
|
|
30
30
|
/**
|
|
31
31
|
* `root`'s `AppConfig`, or `undefined` when the root has no `app.config.ts` — not an app, and each
|
|
32
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
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
* one validator: a hand-built `config` object reaches the boot validated and defaulted exactly as
|
|
34
|
+
* one built by `defineConfig` does, which re-merges to itself. One resolved through a 24.x core is
|
|
35
|
+
* refused (its defaults wrote keys 25.0.0 removed): install every `@ultimat3/*` at one version. A file with no `config` object is refused rather than read as "every default".
|
|
36
36
|
*/
|
|
37
37
|
export async function loadAppConfig(root: string): Promise<AppConfig | undefined> {
|
|
38
38
|
const path = join(root, APP_CONFIG_FILE);
|
package/src/app-env.ts
CHANGED
|
@@ -5,14 +5,10 @@
|
|
|
5
5
|
// why: Bun exposes no path API — the two files this module reads are joined to the app root.
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import type { EnvSchema, EnvVarDecl } from '@ultimat3/core';
|
|
8
|
-
import {
|
|
9
|
-
checkEnvExample,
|
|
10
|
-
ENV_EXAMPLE_PATH,
|
|
11
|
-
ERROR_DOCS_URL,
|
|
12
|
-
renderEnvExample,
|
|
13
|
-
} from '@ultimat3/core';
|
|
8
|
+
import { ENV_EXAMPLE_PATH, ERROR_DOCS_URL, parseEnvKeys } from '@ultimat3/core';
|
|
14
9
|
import { appConfigExport } from './app-config-load';
|
|
15
10
|
import { APP_CONFIG_FILE } from './app-root';
|
|
11
|
+
import { appEnvExample, FRAMEWORK_SECRETS } from './framework-env';
|
|
16
12
|
import type { Finding } from './output';
|
|
17
13
|
import { findingFrom } from './output';
|
|
18
14
|
|
|
@@ -53,14 +49,17 @@ export async function loadEnvSchema(root: string): Promise<EnvSchema | undefined
|
|
|
53
49
|
return isEnvSchema(declared) ? declared : undefined;
|
|
54
50
|
}
|
|
55
51
|
|
|
56
|
-
/**
|
|
57
|
-
|
|
52
|
+
/**
|
|
53
|
+
* The bytes `.env.example` must hold: the app's declaration, then the framework's deploy-required
|
|
54
|
+
* keys (`framework-env.ts`). Deterministic, so a rewrite that changes nothing diffs to nothing.
|
|
55
|
+
*/
|
|
56
|
+
export const envExampleFor = (schema: EnvSchema): string => appEnvExample(schema);
|
|
58
57
|
|
|
59
58
|
const driftFinding = (cause: string): Finding => ({
|
|
60
59
|
code: 'X_ENV_EXAMPLE_DRIFT',
|
|
61
60
|
cause,
|
|
62
|
-
// The generator,
|
|
63
|
-
//
|
|
61
|
+
// The generator, never a `Bun.write(…)` call: that needs a schema object in scope, and a gate
|
|
62
|
+
// reader has a shell.
|
|
64
63
|
fix: 'x env example',
|
|
65
64
|
docs: ERROR_DOCS_URL,
|
|
66
65
|
at: ENV_EXAMPLE_PATH,
|
|
@@ -91,12 +90,20 @@ export async function envExampleFindings(root: string): Promise<readonly Finding
|
|
|
91
90
|
}
|
|
92
91
|
const text = await file.text();
|
|
93
92
|
if (text === expected) return [];
|
|
94
|
-
const
|
|
93
|
+
const present = new Set(parseEnvKeys(text));
|
|
94
|
+
const missing = Object.keys(schema).filter((key) => !present.has(key));
|
|
95
|
+
// The framework's keys are owed by every deployed app, whatever its schema says (#679), and an
|
|
96
|
+
// example written before they were part of the contract is missing exactly these.
|
|
97
|
+
const owed = FRAMEWORK_SECRETS.map((secret) => secret.key).filter(
|
|
98
|
+
(key) => !Object.hasOwn(schema, key) && !present.has(key),
|
|
99
|
+
);
|
|
95
100
|
return [
|
|
96
101
|
driftFinding(
|
|
97
|
-
|
|
98
|
-
? `${ENV_EXAMPLE_PATH} does not declare ${
|
|
99
|
-
:
|
|
102
|
+
missing.length > 0
|
|
103
|
+
? `${ENV_EXAMPLE_PATH} does not declare ${missing.join(', ')}, declared by ${ENV_SCHEMA_EXPORT} in ${APP_CONFIG_FILE}`
|
|
104
|
+
: owed.length > 0
|
|
105
|
+
? `${ENV_EXAMPLE_PATH} does not declare ${owed.join(', ')}, which the framework refuses to boot without outside development/test`
|
|
106
|
+
: `${ENV_EXAMPLE_PATH} is no longer the projection of ${ENV_SCHEMA_EXPORT} — a description, a default or the required flag has moved`,
|
|
100
107
|
),
|
|
101
108
|
];
|
|
102
109
|
}
|
package/src/app-load.ts
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
// `registerQueries` / `registerRoute` name the rest — so `x manifest`, `x routes` and `x verify`
|
|
4
4
|
// read exactly the tables the running server reads.
|
|
5
5
|
|
|
6
|
-
// Bun ships no `Bun.*` path API: `
|
|
7
|
-
|
|
8
|
-
import { join, relative, resolve, sep } from 'node:path';
|
|
6
|
+
// Bun ships no `Bun.*` path API: `join`/`resolve` reach the files a scan imports.
|
|
7
|
+
import { join, resolve } from 'node:path';
|
|
9
8
|
import { isAction, isMutator, listActions, registerActions } from '@ultimat3/action';
|
|
10
9
|
import { describeEntities, registeredEntities } from '@ultimat3/entity';
|
|
11
|
-
import {
|
|
10
|
+
import type { AppLocaleSet } from '@ultimat3/i18n/app-catalogs';
|
|
11
|
+
import { appLocaleSet, UNDECLARED_LOCALES } from '@ultimat3/i18n/app-catalogs';
|
|
12
12
|
import { isJobHandle, isTaskHandle, registeredJobs, registeredTasks } from '@ultimat3/jobs';
|
|
13
13
|
import type { ErrorCodeFact } from '@ultimat3/manifest';
|
|
14
14
|
import { isQuery, listQueries, registerQueries } from '@ultimat3/query';
|
|
@@ -40,6 +40,7 @@ import { collectDeclaredCodes } from './error-contract';
|
|
|
40
40
|
import type { Finding } from './output';
|
|
41
41
|
import { findingFrom } from './output';
|
|
42
42
|
import { hasPathSegment } from './path-segments';
|
|
43
|
+
import { posixRelative, toPosix } from './posix-path';
|
|
43
44
|
import { siteAssetTable } from './site-assets';
|
|
44
45
|
import { isTest } from './source-files';
|
|
45
46
|
|
|
@@ -83,13 +84,13 @@ export interface LoadedApp {
|
|
|
83
84
|
/** Every `X_*` code the app's source declares, by code — the one fact no registry holds. */
|
|
84
85
|
readonly errorCodes: readonly ErrorCodeFact[];
|
|
85
86
|
/**
|
|
86
|
-
* The
|
|
87
|
-
* `defineCatalogs()
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
87
|
+
* The app's locales, default first — `@ultimat3/i18n`'s `appLocaleSet`, the ONE reader of
|
|
88
|
+
* `defineCatalogs()`, asked once here so every step after the load reads one answer. Never
|
|
89
|
+
* `localeConfig()` read blind (it answers an earlier import's declaration for an app that made
|
|
90
|
+
* none) and never a regex over the source. An app with no catalogs, or whose catalog module would
|
|
91
|
+
* not import (the scan's finding already says so), is `UNDECLARED_LOCALES`.
|
|
91
92
|
*/
|
|
92
|
-
readonly
|
|
93
|
+
readonly locales: AppLocaleSet;
|
|
93
94
|
/** Modules that would not import, and primitives that would not register. */
|
|
94
95
|
readonly findings: readonly Finding[];
|
|
95
96
|
}
|
|
@@ -121,6 +122,17 @@ export function resetAppLoad(): void {
|
|
|
121
122
|
resetReloadGraph();
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
/**
|
|
126
|
+
* One glob's hits in code-unit order of their POSIX spelling — never the host's. Sorted raw, `\`
|
|
127
|
+
* (0x5C) sorts after a digit or a capital where `/` (0x2F) sorts before one, so Windows imported
|
|
128
|
+
* `app/a1/page.tsx` before `app/a/page.tsx` and Linux after: one app, two cascades.
|
|
129
|
+
*/
|
|
130
|
+
export function sortModulePaths(paths: readonly string[]): string[] {
|
|
131
|
+
const keyed = paths.map((path) => ({ path, key: toPosix(path) }));
|
|
132
|
+
keyed.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
|
133
|
+
return keyed.map((one) => one.path);
|
|
134
|
+
}
|
|
135
|
+
|
|
124
136
|
/**
|
|
125
137
|
* Every module path the app globs match, SORTED — with the API index first. `Bun.Glob` answers in
|
|
126
138
|
* directory order, which is the filesystem's — ext4 hashes names with a per-filesystem seed — so
|
|
@@ -144,7 +156,7 @@ export async function appModulePaths(root: string): Promise<readonly string[]> {
|
|
|
144
156
|
if (!seen.has(absolute)) matched.push(absolute);
|
|
145
157
|
seen.add(absolute);
|
|
146
158
|
}
|
|
147
|
-
found.push(...matched
|
|
159
|
+
found.push(...sortModulePaths(matched));
|
|
148
160
|
}
|
|
149
161
|
// Resolved, like the glob's own answers: `loadApp('.')` is a legal call.
|
|
150
162
|
const index = resolve(root, API_INDEX);
|
|
@@ -196,7 +208,7 @@ export async function scanAppModules(root: string, options: ScanOptions): Promis
|
|
|
196
208
|
// file it holds, so this loop imported none of them and the app registered nothing.
|
|
197
209
|
// The ROOT-RELATIVE path is tested, never the absolute one: an app checked out under
|
|
198
210
|
// `~/work/my.test.app` matched `.test.` on every file and loaded none of them.
|
|
199
|
-
const file =
|
|
211
|
+
const file = posixRelative(root, absolute);
|
|
200
212
|
if (hasPathSegment(absolute, 'node_modules') || isTest(file)) continue;
|
|
201
213
|
if (ENTRY_POINT.test(file) || CLIENT_ENTRY_POINT.test(file) || STATES_FILE.test(file)) {
|
|
202
214
|
continue;
|
|
@@ -249,16 +261,28 @@ export async function loadApp(root: string): Promise<LoadedApp> {
|
|
|
249
261
|
) {
|
|
250
262
|
findings.push(emptyAppFinding(root));
|
|
251
263
|
}
|
|
252
|
-
// Read after the loop, never before it: `configureLocales` runs on the app's own import.
|
|
253
264
|
return {
|
|
254
265
|
root,
|
|
255
266
|
files: scan.files,
|
|
256
267
|
errorCodes: await appErrorCodes(root),
|
|
257
|
-
|
|
268
|
+
locales: await loadedLocales(root),
|
|
258
269
|
findings,
|
|
259
270
|
};
|
|
260
271
|
}
|
|
261
272
|
|
|
273
|
+
/**
|
|
274
|
+
* After the scan, never before it: the scan imported the catalog module, so the set it exports is
|
|
275
|
+
* what `loadAppCatalogs` recognises. A module that would not import is the scan's finding, not a
|
|
276
|
+
* second one here.
|
|
277
|
+
*/
|
|
278
|
+
async function loadedLocales(root: string): Promise<AppLocaleSet> {
|
|
279
|
+
try {
|
|
280
|
+
return await appLocaleSet(root);
|
|
281
|
+
} catch {
|
|
282
|
+
return UNDECLARED_LOCALES;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
262
286
|
/** No primitive in any registry this scan fills — a process that registered some itself is not empty. */
|
|
263
287
|
const registersNothing = (): boolean =>
|
|
264
288
|
listActions().length === 0 &&
|
package/src/app-manifest.ts
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
import { knownPermissions } from '@ultimat3/policy';
|
|
22
22
|
import { describeQueries } from '@ultimat3/query';
|
|
23
23
|
import type { RouteDescriptor } from '@ultimat3/render';
|
|
24
|
-
import {
|
|
24
|
+
import { describePages } from '@ultimat3/render';
|
|
25
25
|
import { loadApp } from './app-load';
|
|
26
26
|
import { AppPackageInvalidError } from './errors';
|
|
27
27
|
import type { Finding } from './output';
|
|
@@ -105,7 +105,7 @@ type UrlRoute = RouteDescriptor & { readonly surface: 'site' | 'app' | 'api' };
|
|
|
105
105
|
const hasUrl = (route: RouteDescriptor): route is UrlRoute => route.surface !== 'shared';
|
|
106
106
|
|
|
107
107
|
const routeFacts = (): readonly RouteFact[] =>
|
|
108
|
-
|
|
108
|
+
describePages()
|
|
109
109
|
.filter(hasUrl)
|
|
110
110
|
.map((route) => ({
|
|
111
111
|
url: route.path,
|