@ultimat3/cli 23.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 +28 -13
- package/README.md +32 -7
- package/package.json +29 -29
- package/src/admin-catalog-keys.ts +2 -2
- package/src/affected.ts +30 -36
- package/src/api-registration.ts +89 -0
- package/src/app-artifacts.ts +1 -1
- package/src/app-auth.ts +4 -27
- package/src/app-boundaries.ts +17 -4
- package/src/app-config-load.ts +51 -0
- package/src/app-env.ts +26 -25
- package/src/app-load.ts +38 -14
- package/src/app-manifest.ts +4 -10
- package/src/app-mcp.ts +31 -46
- package/src/app-name.ts +52 -0
- package/src/app-permissions-borrowed.ts +123 -0
- package/src/app-permissions-site.ts +48 -0
- package/src/app-permissions.ts +7 -17
- 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 +117 -154
- package/src/cdp-shot-driver.ts +20 -1
- package/src/cdp-shot-watch.ts +35 -31
- package/src/ci-log.ts +0 -0
- package/src/cmd-auth-spec.ts +16 -0
- package/src/cmd-auth.ts +65 -0
- package/src/cmd-build-spec.ts +7 -1
- package/src/cmd-build.ts +151 -22
- package/src/cmd-db-backfill.ts +26 -13
- package/src/cmd-db-branch.ts +2 -2
- package/src/cmd-db.ts +10 -6
- package/src/cmd-deploy-compose.ts +94 -0
- package/src/cmd-deploy-drain.ts +48 -0
- package/src/cmd-deploy-helm.ts +7 -6
- package/src/cmd-deploy-start-first.ts +178 -0
- package/src/cmd-deploy.ts +158 -36
- package/src/cmd-dev.ts +18 -356
- package/src/cmd-doctor-spec.ts +2 -9
- package/src/cmd-doctor.ts +57 -80
- package/src/cmd-env.ts +55 -16
- package/src/cmd-generate.ts +89 -32
- package/src/cmd-help.ts +19 -6
- package/src/cmd-i18n.ts +21 -17
- package/src/cmd-jobs-spec.ts +7 -22
- package/src/cmd-jobs.ts +33 -140
- package/src/cmd-manifest-spec.ts +1 -1
- package/src/cmd-manifest.ts +6 -25
- package/src/cmd-mcp.ts +71 -22
- package/src/cmd-new.ts +36 -8
- package/src/cmd-planned.ts +24 -8
- package/src/cmd-routes.ts +15 -5
- package/src/cmd-secrets.ts +47 -6
- package/src/cmd-shot-matrix.ts +22 -7
- package/src/cmd-shot-spec.ts +10 -2
- package/src/cmd-shot.ts +46 -41
- package/src/cmd-tasks.ts +2 -2
- package/src/cmd-test-spec.ts +1 -7
- package/src/cmd-test.ts +14 -30
- package/src/cmd-verify.ts +4 -4
- 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 +27 -16
- package/src/dev-boot.ts +290 -0
- package/src/dev-child-watch.ts +63 -12
- package/src/dev-dashboard-guard.ts +76 -0
- package/src/dev-dashboard.ts +56 -30
- package/src/dev-lock.ts +100 -31
- 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 +17 -13
- package/src/doctor-app-url.ts +39 -0
- package/src/doctor-auth.ts +95 -0
- package/src/doctor-offline.ts +6 -5
- package/src/document-options.ts +7 -0
- package/src/drift-replica-identity.ts +42 -0
- package/src/drift.ts +29 -6
- package/src/error-catalog.ts +6 -9
- package/src/error-codes.ts +29 -3
- package/src/error-page-csp.ts +14 -15
- package/src/error-unthrown.ts +29 -11
- package/src/errors.ts +54 -83
- 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-apply.ts +78 -0
- package/src/framework-schema-stamp.ts +96 -0
- package/src/framework-schema.ts +25 -3
- package/src/generate-catalog-locales.ts +79 -0
- package/src/generate-files.ts +4 -3
- package/src/generate-kinds.ts +18 -0
- package/src/generate-plural.ts +50 -0
- package/src/generate-shadow.ts +95 -0
- package/src/generate-write.ts +47 -13
- package/src/gh-target.ts +12 -7
- package/src/hold.ts +56 -1
- package/src/i18n-audit.ts +25 -9
- 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 +2 -6
- package/src/island-bundle.ts +57 -79
- 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 +36 -20
- package/src/local-cli-handoff.ts +90 -0
- package/src/loopback-host.ts +13 -0
- package/src/manifest-staleness.ts +62 -0
- package/src/mcp-errors.ts +20 -4
- package/src/mcp-host.ts +22 -14
- package/src/mcp-test-run.ts +38 -0
- 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 +29 -16
- package/src/metrics-endpoint.ts +28 -3
- package/src/migration-extensions.ts +1 -1
- package/src/otlp-export.ts +2 -2
- package/src/output.ts +24 -0
- package/src/page-navigation.ts +7 -23
- package/src/page-speculation.ts +19 -31
- 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-out.ts +34 -7
- package/src/prerender-report.ts +58 -0
- package/src/prerender-served.ts +52 -0
- package/src/prerender.ts +71 -103
- package/src/pwa-artifacts.ts +33 -94
- package/src/pwa-manifest-locales.ts +9 -9
- package/src/registry.ts +2 -0
- package/src/retry-memo.ts +1 -1
- package/src/revalidated-response.ts +4 -3
- package/src/role-realtime.ts +7 -1
- package/src/role-replicator.ts +124 -29
- package/src/role-start.ts +69 -85
- package/src/role-sync.ts +119 -25
- 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 +26 -9
- package/src/runtime-bindings.ts +27 -7
- package/src/runtime-cache.ts +46 -55
- package/src/runtime-hooks.ts +2 -1
- package/src/runtime-idempotency-scope.ts +23 -0
- package/src/runtime-isr-outcome.ts +100 -0
- package/src/runtime-isr.ts +36 -0
- package/src/runtime-jobs.ts +76 -41
- package/src/runtime-live-feed.ts +1 -1
- package/src/runtime-mail.ts +26 -0
- package/src/runtime-mfa-warning.ts +29 -0
- package/src/runtime-notify-retention.ts +10 -46
- package/src/runtime-overrides.ts +4 -4
- package/src/runtime-purge.ts +38 -16
- package/src/runtime-queue.ts +43 -22
- package/src/runtime-realtime.ts +11 -45
- package/src/runtime-render.ts +136 -114
- package/src/runtime-replica.ts +3 -8
- package/src/runtime-services.ts +49 -26
- package/src/runtime-storage-upload.ts +2 -2
- package/src/runtime-storage.ts +13 -8
- package/src/schema-diff.ts +16 -0
- package/src/schema-drift.ts +45 -15
- package/src/secrets-rotation.ts +8 -9
- package/src/seo-routes.ts +80 -9
- package/src/serve-boot.ts +51 -12
- package/src/serve-drain.ts +11 -33
- package/src/serve-env.ts +9 -6
- package/src/serve-retire.ts +95 -0
- package/src/serve-web.ts +31 -15
- 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 -33
- 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-assets.ts +3 -5
- package/src/site-config.ts +9 -48
- package/src/site-seo.ts +7 -5
- package/src/sitemap-lastmod.ts +5 -0
- package/src/solid-loader.ts +12 -3
- package/src/source-files.ts +19 -3
- package/src/static-report.ts +5 -0
- package/src/stored-object-headers.ts +43 -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 +3 -3
- package/src/tasks-facts.ts +3 -29
- package/src/templates/action.ts +7 -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/naming.ts +28 -0
- 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 +26 -14
- package/src/templates/scaffold-api.ts +2 -3
- package/src/templates/scaffold-app.ts +8 -3
- 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-compose.ts +10 -1
- package/src/templates/scaffold-container.ts +16 -8
- 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-client.ts +4 -2
- 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-fences.ts +76 -0
- package/src/templates/scaffold-helm-templates.ts +100 -19
- package/src/templates/scaffold-helm.ts +62 -5
- 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 +27 -16
- package/src/templates/scaffold-shell.ts +55 -37
- package/src/test-counts.ts +2 -6
- package/src/test-ignores.ts +25 -0
- package/src/test-only-shipped.ts +193 -0
- package/src/test-passes.ts +0 -15
- package/src/test-select.ts +7 -15
- package/src/test-shards.ts +40 -95
- package/src/test-workers.ts +26 -4
- package/src/theme-boot.ts +8 -33
- package/src/theme-brand.ts +66 -0
- package/src/trusted-hops-error.ts +17 -0
- package/src/trusted-hops.ts +49 -0
- package/src/unscanned-admin.ts +2 -2
- package/src/unthrown-codes.ts +26 -0
- package/src/verify-checks.ts +29 -48
- package/src/verify-coverage-run.ts +3 -2
- package/src/verify-deadline.ts +207 -33
- package/src/verify-merge.ts +108 -43
- package/src/verify-part-step.ts +98 -0
- package/src/verify-role-load-probe.ts +12 -2
- package/src/verify-role-load.ts +9 -4
- package/src/verify-run.ts +32 -8
- package/src/verify-shard.ts +13 -2
- package/src/verify-stalled.ts +152 -0
- package/src/verify-step.ts +2 -0
- package/src/verify-test-run.ts +3 -2
- package/src/verify-tests.ts +5 -3
- 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/build-errors.ts +0 -54
- package/src/jobs-drain.ts +0 -163
- package/src/scaffold-typecheck.ts +0 -227
- package/src/thrown-by.ts +0 -24
- package/src/verify-errors.ts +0 -58
package/CLAUDE.md
CHANGED
|
@@ -44,25 +44,25 @@ Commands: `bun test packages/cli` (from the repo root — the test preload lives
|
|
|
44
44
|
|---|---|
|
|
45
45
|
| `verify-checks.ts` / `verify-step.ts` / `verify-run.ts` | the step list, the outcome shape, the run order (`BESIDE_SERIAL_SUITES`) |
|
|
46
46
|
| `verify-floor.ts` | `x.verify.json`: a floor step that ran zero tests is `X_VERIFY_SUITE_VANISHED` |
|
|
47
|
-
| `coverage-floor.ts` / `coverage-lcov.ts` / `coverage-source.ts` / `verify-coverage-run.ts` | an APP's `unit` step holds `coverage` in `x.verify.json` over its whole source tree. The suite runs as fixed slices, one plain `bun test` each — never `--parallel --coverage`, whose merge moves with the core count. `coverage-lcov.ts` is the ONE lcov reader; `scripts/coverage-gate.ts` reads through it. A shard defers to `verify-merge.ts` (`coverageRiders`)
|
|
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 |
|
|
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 |
|
|
56
|
-
| `app-permissions.ts` / `permission-grants.ts` | `policy`: every permission granted or required is declared,
|
|
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
|
+
| `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` / `db-destructive.ts` / `db-ungeneratable.ts` | `drift`: snapshot vs declarations, the source hash (core's `canonicalJson`), the header markers |
|
|
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
|
-
| `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,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`), 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) |
|
|
73
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 |
|
|
74
78
|
| `api-registration.ts` | a written job or task is listed in `apps/web/api/index.ts` |
|
|
75
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 |
|
|
@@ -85,23 +89,33 @@ Commands: `bun test packages/cli` (from the repo root — the test preload lives
|
|
|
85
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 |
|
|
86
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` |
|
|
87
91
|
| `module-imports.ts` | the files a module's source imports, resolved once for both graphs: `referenced` (reload) and `evaluated` (document) |
|
|
88
|
-
| `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`) |
|
|
89
93
|
| `runtime-bindings.ts` | which service each binding points at (embedded or external); events follow `realtime.transport` / `urlEnv` |
|
|
90
94
|
| `runtime-queue.ts` / `runtime-services.ts` | the db + queue pair; everything else and every ambient accessor |
|
|
91
|
-
| `
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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) |
|
|
92
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` |
|
|
93
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` |
|
|
94
105
|
| `role-sync.ts` / `role-replicator.ts` / `runtime-live-feed.ts` | the sync node, the change feed |
|
|
95
|
-
| `runtime-render.ts` / `runtime-assets.ts` / `runtime-storage.ts` / `runtime-hooks.ts` / `api-routes.ts` | the HTTP surface: pages, `/icons` + `/media`, `/_storage`, authz, the app's API — plus its bearer mounts (`apiMountRoutes`) and pages' bound `POST`s (`pagePostRoutes`), mounted by BOTH boots |
|
|
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 |
|
|
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 |
|
|
96
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) |
|
|
97
109
|
| `app-openapi.ts` | `openapi.json` (complete when `defineApi({ openapi })` is declared) and each bearer mount's own document; staleness for all of them |
|
|
98
110
|
| `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 |
|
|
99
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 |
|
|
100
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 |
|
|
101
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 |
|
|
102
|
-
| `island-bundle.ts` / `island-store.ts` / `island-realtime.ts` / `solid-loader.ts` | islands: one `Bun.build` each, source-addressed; the image build writes a verified store the container loads |
|
|
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 |
|
|
103
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 |
|
|
104
117
|
| `dev-*.ts` | `x dev` only: dashboard sources, traces, the N+1 ledger, the watcher, the reload, the lock, the port |
|
|
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 |
|
|
105
119
|
|
|
106
120
|
### Build and data
|
|
107
121
|
|
|
@@ -118,7 +132,8 @@ Commands: `bun test packages/cli` (from the repo root — the test preload lives
|
|
|
118
132
|
| File | Job |
|
|
119
133
|
|---|---|
|
|
120
134
|
| `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-
|
|
135
|
+
| `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 |
|
|
136
|
+
| `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
137
|
| `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
138
|
| `cmd-pr.ts` / `cmd-ci.ts` | GitHub through `gh`, parsed against a schema. Not gate steps |
|
|
124
139
|
| `error-catalog.ts` | imports every `@ultimat3/*` package so `x errors` answers for any code |
|
package/README.md
CHANGED
|
@@ -16,14 +16,15 @@ 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)); 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
|
+
| `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 |
|
|
@@ -35,6 +36,31 @@ Everything in [CLI reference](../../wiki/CLI-Reference.md)'s planned table is al
|
|
|
35
36
|
and exits `X_NOT_IMPLEMENTED` with a `fix:` naming the closest shipped command — "not built yet"
|
|
36
37
|
and "not a command" are different facts.
|
|
37
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
|
+
|
|
38
64
|
## Which `x` runs
|
|
39
65
|
|
|
40
66
|
**The app's own.** `x` is a workspace dependency, and `bunx x` / the `package.json` scripts resolve
|
|
@@ -135,7 +161,7 @@ that name where there is one (`x new` writes all seventeen), the blank template
|
|
|
135
161
|
| `cmd-*.ts` | one command group each |
|
|
136
162
|
| `templates/` | scaffolding as typed string modules, not copied fixtures |
|
|
137
163
|
| `app-load.ts` | import an app's modules so the framework registries hold it |
|
|
138
|
-
| `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 |
|
|
139
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 |
|
|
140
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 |
|
|
141
167
|
| `measurement-actor.ts` | the actor a weigh-and-discard render runs as — every permission, never served |
|
|
@@ -145,7 +171,7 @@ that name where there is one (`x new` writes all seventeen), the blank template
|
|
|
145
171
|
| `app-boundaries.ts` | app import boundaries, over `@ultimat3/render`'s surface check |
|
|
146
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` |
|
|
147
173
|
| `app-agents-md.ts` | `AGENTS.md` exists and stays short, over `@ultimat3/manifest`'s check |
|
|
148
|
-
| `serve.ts` | **what a container starts** — `runRole(options)`, the same boot `x dev` runs minus the watcher, `/_x` and `dev: true`. `x new`'s `apps/web/server.ts` is three lines that call it. `ROLE`, `PORT` and `HOST` are read from `env`; `role`, `port` and `hostname` on `ServeOptions` override each |
|
|
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)) |
|
|
149
175
|
| `prerender.ts` | `x build --target static`: which `site/` routes qualify, and where the bytes land |
|
|
150
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 })` |
|
|
151
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 |
|
|
@@ -192,14 +218,13 @@ a job boundary the class is gone and the `code` is what survives — match on th
|
|
|
192
218
|
| Class | Code | Declared in |
|
|
193
219
|
|---|---|---|
|
|
194
220
|
| `BadFlagError` | `X_CLI_BAD_FLAG` | `src/errors.ts` |
|
|
195
|
-
| `BuildEntryMissingError` | `X_BUILD_ENTRY_MISSING` | `src/build
|
|
221
|
+
| `BuildEntryMissingError` | `X_BUILD_ENTRY_MISSING` | `src/cmd-build.ts` |
|
|
196
222
|
| `BunVersionError` | `X_BUN_VERSION` | `src/errors.ts` |
|
|
197
223
|
| `CatalogExistsError` | `X_GENERATE_CONFLICT` | `src/errors.ts` |
|
|
198
224
|
| `CdpBrowserMissingError` | `X_CDP_BROWSER_MISSING` | `src/cdp-errors.ts` |
|
|
199
225
|
| `CdpCallFailedError` | `X_CDP_CALL_FAILED` | `src/cdp-errors.ts` |
|
|
200
226
|
| `CdpLaunchFailedError` | `X_CDP_LAUNCH_FAILED` | `src/cdp-errors.ts` |
|
|
201
227
|
| `CdpTimeoutError` | `X_CDP_TIMEOUT` | `src/cdp-errors.ts` |
|
|
202
|
-
| `CliNotImplementedError` | `X_NOT_IMPLEMENTED` | `src/errors.ts` |
|
|
203
228
|
| `DeclarationUnknownError` | `X_DECLARATION_UNKNOWN` | `src/errors.ts` |
|
|
204
229
|
| `E2eAppFailedError` | `X_E2E_APP_FAILED` | `src/e2e-errors.ts` |
|
|
205
230
|
| `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": "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
|
@@ -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 {
|
|
@@ -191,7 +181,9 @@ export async function gitRoot(runner: Runner, cwd: string, command: string): Pro
|
|
|
191
181
|
throw new UltimateError({
|
|
192
182
|
code: 'X_CLI_UNEXPECTED',
|
|
193
183
|
cause: `x ${command} reads its diff from git and "git rev-parse --show-toplevel" exited ${result.code} in ${cwd}: ${singleLine(execOutput(result))}`,
|
|
194
|
-
|
|
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`,
|
|
195
187
|
});
|
|
196
188
|
}
|
|
197
189
|
|
|
@@ -263,14 +255,20 @@ export interface AffectedScope {
|
|
|
263
255
|
}
|
|
264
256
|
|
|
265
257
|
/**
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
258
|
+
* The plan's scope, relative to the directory the caller scans. A ROOT-WIDE plan is the whole scan
|
|
259
|
+
* root (`''`): every workspace is in it, and so is every file no workspace owns — a `scripts/` test
|
|
260
|
+
* stayed out of the run while `package.json` changed when only workspace directories were listed.
|
|
261
|
+
*
|
|
262
|
+
* Otherwise an empty prefix means the scan root IS an affected workspace, so every path it yields
|
|
263
|
+
* is in scope; a prefix starting with `..` is a workspace outside that root, which has no file there
|
|
264
|
+
* to select and must not be allowed to collapse into a match-everything empty string.
|
|
269
265
|
*/
|
|
270
|
-
const scopePrefixes = (
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
266
|
+
export const scopePrefixes = (plan: AffectedPlan, root: string, cwd: string): readonly string[] =>
|
|
267
|
+
plan.rootWide.length > 0
|
|
268
|
+
? ['']
|
|
269
|
+
: plan.workspaces
|
|
270
|
+
.map((workspace) => relative(cwd, join(root, workspace.dir)).split('\\').join('/'))
|
|
271
|
+
.filter((path) => !path.startsWith('..'));
|
|
274
272
|
|
|
275
273
|
export const inScope = (path: string, prefixes: readonly string[]): boolean =>
|
|
276
274
|
prefixes.some((prefix) => prefix === '' || path === prefix || path.startsWith(`${prefix}/`));
|
|
@@ -300,11 +298,7 @@ export async function affectedScope(options: AffectedScopeOptions): Promise<Affe
|
|
|
300
298
|
selection,
|
|
301
299
|
root,
|
|
302
300
|
plan,
|
|
303
|
-
prefixes: scopePrefixes(
|
|
304
|
-
root,
|
|
305
|
-
options.cwd,
|
|
306
|
-
plan.workspaces.map((workspace) => workspace.dir),
|
|
307
|
-
),
|
|
301
|
+
prefixes: scopePrefixes(plan, root, options.cwd),
|
|
308
302
|
};
|
|
309
303
|
}
|
|
310
304
|
|
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
|
}
|
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
|
}
|