@ultimat3/cli 29.3.2 → 30.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 +64 -40
- package/README.md +61 -11
- package/package.json +42 -28
- package/src/affected-tests.ts +97 -0
- package/src/affected-unowned.ts +43 -0
- package/src/affected.ts +25 -6
- package/src/app-boundaries.ts +29 -1
- package/src/app-evals.ts +12 -4
- package/src/app-load-unresolved.ts +62 -0
- package/src/app-load.ts +30 -37
- package/src/app-mcp.ts +14 -2
- package/src/app-module-graph.ts +232 -0
- package/src/app-reload-graph.ts +2 -2
- package/src/app-root.ts +37 -1
- package/src/bin.ts +1 -0
- package/src/boundary-findings.ts +6 -0
- package/src/budgets-freshness.ts +32 -0
- package/src/budgets.ts +20 -1
- package/src/build-capture-env.ts +9 -0
- package/src/cdp-shot-a11y.ts +2 -39
- package/src/ci-tested.ts +243 -0
- package/src/client-emit-customer-runtime.ts +139 -0
- package/src/client-emit-customer.ts +108 -0
- package/src/client-emit-drift.ts +72 -0
- package/src/client-emit-header.ts +67 -0
- package/src/client-emit-schemas.ts +175 -0
- package/src/client-emit-types.ts +121 -0
- package/src/client-emit-vendor.ts +146 -0
- package/src/client-errors-routes.ts +229 -0
- package/src/client-errors-script.ts +74 -0
- package/src/client-vitals-route.ts +197 -0
- package/src/client-vitals-script.ts +56 -0
- package/src/cmd-affected.ts +23 -0
- package/src/cmd-build-spec.ts +7 -1
- package/src/cmd-build.ts +35 -9
- package/src/cmd-ci-spec.ts +42 -6
- package/src/cmd-ci.ts +99 -2
- package/src/cmd-client-customer.ts +134 -0
- package/src/cmd-client-source.ts +81 -0
- package/src/cmd-client-spec.ts +48 -0
- package/src/cmd-client-write.ts +41 -0
- package/src/cmd-client.ts +111 -0
- package/src/cmd-db-recount.ts +120 -0
- package/src/cmd-db-spec.ts +36 -6
- package/src/cmd-db.ts +13 -3
- package/src/cmd-deploy-compose.ts +61 -3
- package/src/cmd-deploy-drain.ts +10 -2
- package/src/cmd-deploy-helm.ts +11 -0
- package/src/cmd-deploy-preflight.ts +118 -0
- package/src/cmd-deploy-spec.ts +58 -6
- package/src/cmd-deploy-verify.ts +380 -0
- package/src/cmd-deploy.ts +42 -9
- package/src/cmd-docs.ts +34 -4
- package/src/cmd-doctor.ts +66 -9
- package/src/cmd-drill-spec.ts +25 -0
- package/src/cmd-drill.ts +176 -0
- package/src/cmd-env-init.ts +102 -0
- package/src/cmd-env-spec.ts +10 -3
- package/src/cmd-env.ts +15 -10
- package/src/cmd-errors-recent.ts +60 -0
- package/src/cmd-errors-spec.ts +9 -1
- package/src/cmd-errors.ts +4 -0
- package/src/cmd-exercise-spec.ts +19 -0
- package/src/cmd-exercise.ts +129 -0
- package/src/cmd-generate-spec.ts +9 -1
- package/src/cmd-generate.ts +32 -27
- package/src/cmd-graph-load.ts +97 -0
- package/src/cmd-graph-spec.ts +16 -0
- package/src/cmd-graph.ts +73 -0
- package/src/cmd-i18n.ts +4 -1
- package/src/cmd-impact-spec.ts +13 -0
- package/src/cmd-impact.ts +64 -0
- package/src/cmd-jobs-resolve.ts +76 -0
- package/src/cmd-jobs-spec.ts +21 -2
- package/src/cmd-jobs.ts +10 -6
- package/src/cmd-mail-spec.ts +28 -0
- package/src/cmd-mail.ts +70 -0
- package/src/cmd-mcp-lock.ts +69 -0
- package/src/cmd-mcp-spec.ts +23 -4
- package/src/cmd-mcp.ts +17 -3
- package/src/cmd-new-spec.ts +8 -1
- package/src/cmd-new.ts +61 -9
- package/src/cmd-planned.ts +0 -6
- package/src/cmd-repro-spec.ts +31 -0
- package/src/cmd-repro.ts +174 -0
- package/src/cmd-stop-spec.ts +21 -0
- package/src/cmd-stop.ts +166 -0
- package/src/cmd-test-spec.ts +17 -4
- package/src/cmd-test.ts +70 -14
- package/src/cmd-upgrade-spec.ts +26 -0
- package/src/cmd-upgrade.ts +274 -0
- package/src/cmd-verify-spec.ts +3 -3
- package/src/db-backfill.ts +6 -0
- package/src/db-diagnostics.ts +106 -0
- package/src/db-generate.ts +28 -8
- package/src/db-reset-template.ts +1 -1
- package/src/db-rolling.ts +45 -0
- package/src/db-seed-generate.ts +188 -0
- package/src/deploy-verify-findings.ts +98 -0
- package/src/deploy-verify-flags.ts +109 -0
- package/src/deploy-verify-grade.ts +185 -0
- package/src/deploy-verify-queues.ts +53 -0
- package/src/dev-boot.ts +35 -15
- package/src/dev-child-watch.ts +16 -4
- package/src/dev-dashboard-guard.ts +8 -1
- package/src/dev-dashboard.ts +7 -0
- package/src/dev-lock-identity.ts +61 -0
- package/src/dev-lock-port-holder.ts +80 -0
- package/src/dev-lock-steal.ts +124 -0
- package/src/dev-lock.ts +92 -120
- package/src/dev-mail-templates.ts +51 -0
- package/src/dev-rebuild.ts +26 -2
- package/src/dev-route-table.ts +17 -0
- package/src/dev-supervisor.ts +5 -2
- package/src/dev-watch-tree.ts +11 -10
- package/src/dev-watch.ts +1 -1
- package/src/dispatch.ts +29 -6
- package/src/docs-section.ts +69 -0
- package/src/docs-sources.ts +100 -0
- package/src/doctor-auth.ts +11 -2
- package/src/doctor-chart-alerts.ts +55 -0
- package/src/doctor-pooler.ts +107 -0
- package/src/doctor-rolling.ts +35 -0
- package/src/doctor-row-security.ts +40 -0
- package/src/document-options.ts +13 -0
- package/src/drill-sync-lane.ts +25 -0
- package/src/drill-targets.ts +39 -0
- package/src/duplicate-packages.ts +1 -1
- package/src/env-production.ts +93 -0
- package/src/error-codes-shot.ts +59 -0
- package/src/error-codes-tooling.ts +29 -0
- package/src/error-codes.ts +100 -54
- package/src/error-contract.ts +2 -2
- package/src/errors.ts +22 -3
- package/src/exercise-database.ts +99 -0
- package/src/exercise-findings.ts +159 -0
- package/src/exercise-overrides.ts +42 -0
- package/src/exercise-registration.ts +58 -0
- package/src/exercise-static.ts +88 -0
- package/src/exercise-subject.ts +69 -0
- package/src/failure-capture-boot.ts +30 -0
- package/src/framework-env.ts +7 -1
- package/src/framework-schema.ts +72 -5
- package/src/generate-catalog-locales.ts +19 -0
- package/src/generate-chart-alerts.ts +156 -0
- package/src/generate-feature-required.ts +65 -0
- package/src/generate-feature.ts +83 -6
- package/src/generate-files.ts +24 -0
- package/src/generate-format-written.ts +32 -0
- package/src/generate-kinds.ts +6 -0
- package/src/generate-sign-in-config.ts +100 -0
- package/src/generate-sign-in-manifest.ts +28 -0
- package/src/generate-sign-in-mode.ts +101 -0
- package/src/generate-sign-in.ts +250 -0
- package/src/generate-write.ts +44 -7
- package/src/hand-rolled-socket.ts +72 -0
- package/src/i18n-app-key-shapes.ts +29 -0
- package/src/i18n-audit.ts +10 -3
- package/src/i18n-registration.ts +18 -3
- package/src/index.ts +2 -0
- package/src/island-bundle.ts +4 -3
- package/src/island-disposer.ts +116 -0
- package/src/island-realtime-state.ts +34 -6
- package/src/island-realtime.ts +6 -2
- package/src/island-routes.ts +3 -2
- package/src/isr-query-advice.ts +6 -11
- package/src/isr-query-guard.ts +4 -3
- package/src/job-budget.ts +95 -0
- package/src/jobs-json.ts +11 -0
- package/src/lint-findings.ts +104 -0
- package/src/local-cli.ts +34 -4
- package/src/loopback-host.ts +2 -2
- package/src/mail-preview-findings.ts +24 -0
- package/src/mcp-errors.ts +82 -2
- package/src/mcp-host.ts +11 -1
- package/src/mcp-lock-findings.ts +57 -0
- package/src/measure-paths.ts +12 -4
- package/src/measure-scope.ts +27 -13
- package/src/messages-client.ts +23 -0
- package/src/messages-db.ts +56 -0
- package/src/messages-gate.ts +107 -0
- package/src/messages-repro.ts +11 -0
- package/src/messages-shot.ts +51 -0
- package/src/messages-upgrade.ts +45 -0
- package/src/messages.ts +53 -178
- package/src/metrics-endpoint.ts +23 -5
- package/src/new-shapes-generate.ts +71 -0
- package/src/new-shapes.ts +63 -0
- package/src/openapi-compile-ir.ts +68 -0
- package/src/openapi-compile-schema.ts +284 -0
- package/src/openapi-compile.ts +273 -0
- package/src/openapi-refuse.ts +45 -0
- package/src/otlp-export.ts +2 -1
- package/src/output-findings.ts +123 -0
- package/src/output.ts +32 -8
- package/src/package-boundaries.ts +110 -0
- package/src/page-pins.ts +23 -0
- package/src/plain-routes.ts +13 -0
- package/src/prerender-entries.ts +59 -0
- package/src/prerender-heaviest.ts +27 -0
- package/src/prerender-preload.ts +6 -0
- package/src/prerender-report.ts +12 -0
- package/src/prerender-seo-reads.ts +38 -0
- package/src/prerender-served.ts +22 -7
- package/src/prerender-vitals.ts +42 -0
- package/src/prerender.ts +61 -32
- package/src/pwa-artifacts.ts +4 -3
- package/src/pwa-manifest-locales.ts +1 -1
- package/src/registry.ts +18 -0
- package/src/repro-bundle.ts +65 -0
- package/src/repro-errors.ts +47 -0
- package/src/repro-job.ts +72 -0
- package/src/repro-literal.ts +41 -0
- package/src/repro-request.ts +48 -0
- package/src/repro-write.ts +100 -0
- package/src/retain-sweep.ts +27 -0
- package/src/retained-assets-boot.ts +113 -0
- package/src/retained-assets-layout.ts +110 -0
- package/src/retained-assets-read.ts +142 -0
- package/src/retained-assets-sweep.ts +70 -0
- package/src/retained-assets.ts +171 -0
- package/src/revalidated-response.ts +8 -5
- package/src/role-replicator.ts +7 -1
- package/src/role-start-devices.ts +119 -0
- package/src/role-start-types.ts +6 -0
- package/src/role-start.ts +22 -5
- package/src/role-sync.ts +7 -1
- package/src/route-islands.ts +42 -0
- package/src/route-table.ts +2 -1
- package/src/row-security-boot.ts +58 -0
- package/src/row-security-wanted.ts +32 -0
- package/src/runtime-admin.ts +0 -1
- package/src/runtime-assets-site-cache.ts +55 -0
- package/src/runtime-assets-site.ts +136 -0
- package/src/runtime-assets.ts +99 -55
- package/src/runtime-bindings.ts +19 -3
- package/src/runtime-cache.ts +23 -14
- package/src/runtime-isr-outcome.ts +114 -60
- package/src/runtime-purge.ts +14 -2
- package/src/runtime-queue.ts +31 -4
- package/src/runtime-render-document.ts +204 -0
- package/src/runtime-render.ts +48 -198
- package/src/runtime-replica-position.ts +62 -0
- package/src/runtime-replica.ts +18 -3
- package/src/runtime-s3-path-style.ts +17 -0
- package/src/runtime-services.ts +19 -6
- package/src/runtime-statement-bound.ts +22 -0
- package/src/runtime-storage-upload.ts +9 -0
- package/src/runtime-storage.ts +42 -138
- package/src/schema-diff-views.ts +54 -0
- package/src/schema-diff.ts +21 -3
- package/src/schema-drift.ts +28 -9
- package/src/schema-dump-drift.ts +91 -7
- package/src/script-csp.ts +1 -1
- package/src/seo-cards.ts +26 -0
- package/src/seo-routes.ts +22 -8
- package/src/serve-boot.ts +54 -21
- package/src/serve-drain.ts +1 -0
- package/src/serve-env.ts +26 -8
- package/src/serve-retire.ts +12 -6
- package/src/serve-web.ts +42 -4
- package/src/serve.ts +9 -7
- package/src/site-asset-routes.ts +17 -27
- package/src/site-config.ts +4 -0
- package/src/site-llms-pages.ts +106 -0
- package/src/site-llms.ts +64 -0
- package/src/site-seo-write.ts +4 -2
- package/src/site-seo.ts +55 -6
- package/src/source-mtime.ts +50 -0
- package/src/static-capture-errors.ts +41 -0
- package/src/static-capture.ts +56 -0
- package/src/static-cards-serve.ts +39 -0
- package/src/static-cards.ts +93 -0
- package/src/static-document.ts +14 -13
- package/src/static-images.ts +115 -0
- package/src/stream-hole-scope.ts +55 -0
- package/src/style-bundle.ts +1 -1
- package/src/style-routes.ts +3 -2
- package/src/sw-precache-plan.ts +2 -2
- package/src/sw-routes.ts +2 -2
- package/src/templates/action-tests.ts +306 -0
- package/src/templates/action.ts +81 -269
- package/src/templates/backfill.ts +1 -1
- package/src/templates/entity.ts +21 -5
- package/src/templates/github/ci.yml.ts +8 -9
- package/src/templates/guard-bare-error.ts +2 -2
- package/src/templates/guard-error-declared.ts +263 -0
- package/src/templates/guard-native-confirm.ts +132 -0
- package/src/templates/guard-raw-fetch.ts +151 -0
- package/src/templates/guard-raw-interval.ts +121 -0
- package/src/templates/guard-repo-raw-sql.ts +71 -2
- package/src/templates/job.ts +1 -1
- package/src/templates/naming.ts +5 -0
- package/src/templates/pinned/docker-compose.yml +6 -0
- package/src/templates/pinned-images.ts +8 -0
- package/src/templates/policy.ts +85 -2
- package/src/templates/query.ts +70 -13
- package/src/templates/resource-create.ts +3 -1
- package/src/templates/resource-form-island.ts +15 -4
- package/src/templates/resource-page-db.ts +83 -0
- package/src/templates/resource-page.ts +161 -52
- package/src/templates/route.ts +4 -6
- package/src/templates/scaffold-app.ts +3 -0
- package/src/templates/scaffold-auth.ts +2 -1
- package/src/templates/scaffold-bin.ts +5 -52
- package/src/templates/scaffold-claude-agents.ts +19 -19
- package/src/templates/scaffold-claude-commands.ts +28 -31
- package/src/templates/scaffold-claude.ts +26 -23
- package/src/templates/scaffold-container-compose.ts +6 -1
- package/src/templates/scaffold-container.ts +24 -4
- package/src/templates/scaffold-dashboard-bare.ts +3 -3
- package/src/templates/scaffold-dashboard-example-tests.ts +49 -9
- package/src/templates/scaffold-dashboard-example-view.ts +156 -0
- package/src/templates/scaffold-dashboard-example.ts +43 -131
- package/src/templates/scaffold-dashboard-shared.ts +3 -7
- package/src/templates/scaffold-db-client.ts +20 -6
- package/src/templates/scaffold-db-package.ts +5 -14
- package/src/templates/scaffold-docs.ts +73 -57
- package/src/templates/scaffold-domain-package.ts +18 -14
- package/src/templates/scaffold-entries.ts +7 -2
- package/src/templates/scaffold-exercise.ts +37 -0
- package/src/templates/scaffold-guards.ts +13 -1
- package/src/templates/scaffold-helm-alerts.ts +234 -0
- package/src/templates/scaffold-helm-templates.ts +7 -5
- package/src/templates/scaffold-helm.ts +46 -12
- package/src/templates/scaffold-i18n.ts +6 -2
- package/src/templates/scaffold-mcp-endpoint.ts +182 -0
- package/src/templates/scaffold-mcp-json.ts +31 -0
- package/src/templates/scaffold-mcp-package.ts +59 -56
- package/src/templates/scaffold-repo.ts +7 -2
- package/src/templates/scaffold-shell.ts +45 -79
- package/src/templates/scaffold-test-services.ts +13 -5
- package/src/templates/shapes/api-keys.ts +170 -0
- package/src/templates/shapes/api.ts +101 -0
- package/src/templates/shapes/blog-site.ts +340 -0
- package/src/templates/shapes/blog-slice.ts +265 -0
- package/src/templates/shapes/blog.ts +114 -0
- package/src/templates/shapes/dashboard-detail.ts +242 -0
- package/src/templates/shapes/dashboard.ts +53 -0
- package/src/templates/shapes/index.ts +17 -0
- package/src/templates/shapes/overlay.ts +99 -0
- package/src/templates/shapes/shared-edits.ts +46 -0
- package/src/templates/sign-in-action-tests.ts +237 -0
- package/src/templates/sign-in-authenticator.ts +112 -0
- package/src/templates/sign-in-oidc.ts +189 -0
- package/src/templates/sign-in-pages.ts +148 -0
- package/src/templates/sign-in-password.ts +216 -0
- package/src/templates/sign-in-shared.ts +268 -0
- package/src/templates/slice-foundation.ts +70 -15
- package/src/test-batches.ts +1 -1
- package/src/test-database.ts +1 -1
- package/src/test-shards.ts +7 -1
- package/src/test-workers.ts +18 -12
- package/src/tool-gate-names.ts +34 -0
- package/src/upgrade-agents-link.ts +49 -0
- package/src/upgrade-bun-floor.ts +225 -0
- package/src/upgrade-gate.ts +228 -0
- package/src/upgrade-pins.ts +63 -0
- package/src/upgrade-plan.ts +214 -0
- package/src/upgrade-registry.ts +53 -0
- package/src/upgrade-report.ts +139 -0
- package/src/upgrade-root.ts +74 -0
- package/src/verify-budgets-build.ts +118 -0
- package/src/verify-checks.ts +67 -24
- package/src/verify-coverage-run.ts +1 -1
- package/src/verify-e2e-lane.ts +41 -0
- package/src/verify-exercise.ts +64 -0
- package/src/verify-floor.ts +41 -0
- package/src/verify-merge.ts +19 -4
- package/src/verify-role-load.ts +2 -0
- package/src/verify-run.ts +28 -8
- package/src/verify-shard.ts +12 -2
- package/src/verify-skip-ceiling.ts +87 -0
- package/src/verify-step.ts +5 -0
- package/src/verify-tests.ts +25 -5
- package/src/view-drift.ts +55 -0
- package/src/worker-bundle.ts +3 -2
- package/src/workspace-graph.ts +38 -19
- package/src/write-generated.ts +43 -0
- package/src/zone-boot.ts +20 -0
- package/wiki/Actions.md +342 -0
- package/wiki/Admin-Dashboard.md +432 -0
- package/wiki/Agents.md +958 -0
- package/wiki/Auth.md +195 -0
- package/wiki/Bare-VM.md +111 -0
- package/wiki/Batching-And-Preloading.md +136 -0
- package/wiki/Building-Your-Own-Base.md +203 -0
- package/wiki/CI-Parallel-Gate.md +220 -0
- package/wiki/CLI-Reference.md +1894 -0
- package/wiki/Caching-And-Invalidation.md +255 -0
- package/wiki/Client-Data.md +194 -0
- package/wiki/Client-Navigation.md +513 -0
- package/wiki/Compared.md +93 -0
- package/wiki/Configuration.md +868 -0
- package/wiki/Contributing.md +12 -0
- package/wiki/Deploy-Recipes.md +122 -0
- package/wiki/Deployment.md +508 -0
- package/wiki/Entities-And-Migrations.md +1116 -0
- package/wiki/FAQ.md +175 -0
- package/wiki/Feature-Flags.md +65 -0
- package/wiki/Getting-Started.md +221 -0
- package/wiki/Home.md +160 -0
- package/wiki/I18n.md +239 -0
- package/wiki/Installation.md +206 -0
- package/wiki/Interface-Rules.md +339 -0
- package/wiki/Jobs-And-Workflows.md +783 -0
- package/wiki/Known-Gaps.md +185 -0
- package/wiki/MCP-And-AI.md +753 -0
- package/wiki/Mail.md +138 -0
- package/wiki/Migrating-An-Existing-App.md +594 -0
- package/wiki/Migrating-From-Django.md +54 -0
- package/wiki/Migrating-From-Laravel.md +53 -0
- package/wiki/Migrating-From-Node-Drizzle.md +53 -0
- package/wiki/Migrating-From-Rails.md +53 -0
- package/wiki/Migrations-And-Backfills.md +552 -0
- package/wiki/Money.md +338 -0
- package/wiki/N-Plus-One-Detection.md +197 -0
- package/wiki/Notify.md +208 -0
- package/wiki/Observability.md +245 -0
- package/wiki/Outbound-HTTP.md +109 -0
- package/wiki/PWA-And-Offline.md +343 -0
- package/wiki/Policies-And-Authz.md +251 -0
- package/wiki/Project-Layout.md +150 -0
- package/wiki/Queries-And-Live-Queries.md +429 -0
- package/wiki/Realtime.md +734 -0
- package/wiki/Resource-Management.md +121 -0
- package/wiki/Routes-And-Render-Modes.md +521 -0
- package/wiki/SEO.md +288 -0
- package/wiki/Scheduled-Tasks.md +175 -0
- package/wiki/Scraping.md +493 -0
- package/wiki/Shapes.md +63 -0
- package/wiki/Stability.md +97 -0
- package/wiki/Static-Assets.md +166 -0
- package/wiki/Storage-And-Uploads.md +404 -0
- package/wiki/Testing.md +1313 -0
- package/wiki/The-Eight-Primitives.md +247 -0
- package/wiki/Theming.md +362 -0
- package/wiki/Timezones-And-Dates.md +194 -0
- package/wiki/Troubleshooting.md +163 -0
- package/wiki/Tutorial-01-First-App.md +289 -0
- package/wiki/Tutorial-02-First-Feature.md +339 -0
- package/wiki/Tutorial-03-Auth-And-Admin.md +379 -0
- package/wiki/Tutorial-04-Jobs-And-Realtime.md +263 -0
- package/wiki/Tutorial-05-Deploy-Free.md +231 -0
- package/wiki/Tutorial-06-Growing-Up.md +158 -0
- package/wiki/UI-Components.md +156 -0
- package/wiki/Upgrading.md +3667 -0
- package/src/stored-object-headers.ts +0 -43
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// The checkout's top-level directories that no workspace owns or contains — in this repository,
|
|
2
|
+
// `scripts/`: tests that import packages by relative path and declare no dependency on any, so the
|
|
3
|
+
// workspace graph cannot say which change reaches them. `affected.ts` runs them for every change
|
|
4
|
+
// that selects a workspace at all.
|
|
5
|
+
|
|
6
|
+
// why: Bun ships no directory listing with file types.
|
|
7
|
+
import { readdir } from 'node:fs/promises';
|
|
8
|
+
import type { WorkspaceNode } from './workspace-graph';
|
|
9
|
+
|
|
10
|
+
/** Never a home for a test the run should select: dependencies, git, build state. */
|
|
11
|
+
const SKIPPED = (name: string): boolean => name.startsWith('.') || name === 'node_modules';
|
|
12
|
+
|
|
13
|
+
/** A root-level workspace (`''` or `.`) owns the whole checkout. */
|
|
14
|
+
const isRoot = (dir: string): boolean => dir === '' || dir === '.';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Pure: `entries` (top-level directory names) less any that is, or holds, a workspace — sorted.
|
|
18
|
+
* A directory that holds one (`packages/`, `examples/`) is answered for by the graph.
|
|
19
|
+
*/
|
|
20
|
+
export function unownedDirs(
|
|
21
|
+
graph: readonly WorkspaceNode[],
|
|
22
|
+
entries: readonly string[],
|
|
23
|
+
): readonly string[] {
|
|
24
|
+
if (graph.some((node) => isRoot(node.dir))) return [];
|
|
25
|
+
return entries
|
|
26
|
+
.filter((entry) => !SKIPPED(entry))
|
|
27
|
+
.filter(
|
|
28
|
+
(entry) => !graph.some((node) => node.dir === entry || node.dir.startsWith(`${entry}/`)),
|
|
29
|
+
)
|
|
30
|
+
.sort();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The same, read off the checkout root's own directory listing. */
|
|
34
|
+
export async function readUnownedDirs(
|
|
35
|
+
root: string,
|
|
36
|
+
graph: readonly WorkspaceNode[],
|
|
37
|
+
): Promise<readonly string[]> {
|
|
38
|
+
const listed = await readdir(root, { withFileTypes: true });
|
|
39
|
+
return unownedDirs(
|
|
40
|
+
graph,
|
|
41
|
+
listed.filter((entry) => entry.isDirectory()).map((entry) => entry.name),
|
|
42
|
+
);
|
|
43
|
+
}
|
package/src/affected.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// directory is checkout-relative while a caller's scan yields paths relative to its own root.
|
|
14
14
|
import { join, relative } from 'node:path';
|
|
15
15
|
import { singleLine, UltimateError } from '@ultimat3/core';
|
|
16
|
+
import { readUnownedDirs } from './affected-unowned';
|
|
16
17
|
import { BadFlagError } from './errors';
|
|
17
18
|
import type { ExecResult, Runner } from './exec';
|
|
18
19
|
import { execOutput } from './exec';
|
|
@@ -99,8 +100,21 @@ export interface AffectedPlan {
|
|
|
99
100
|
*/
|
|
100
101
|
readonly rootWide: readonly string[];
|
|
101
102
|
readonly workspaces: readonly WorkspaceNode[];
|
|
103
|
+
/**
|
|
104
|
+
* The root directories no workspace owns (`affected-unowned.ts`) — `scripts/` here — selected
|
|
105
|
+
* whenever any workspace is: their tests import packages by relative path, declare no
|
|
106
|
+
* dependency, and so are reached by a change the graph has no edge for. Empty when nothing is
|
|
107
|
+
* affected, and when the plan is root-wide (which already selects them).
|
|
108
|
+
*/
|
|
109
|
+
readonly unowned: readonly string[];
|
|
102
110
|
}
|
|
103
111
|
|
|
112
|
+
/**
|
|
113
|
+
* The CLI is the tool every test run goes through — `x test`, the gate's steps, the preloads — so a
|
|
114
|
+
* change to its own source is a change to how every workspace is checked, not to one workspace.
|
|
115
|
+
*/
|
|
116
|
+
export const TOOL_WORKSPACE = '@ultimat3/cli';
|
|
117
|
+
|
|
104
118
|
const byName = (a: WorkspaceNode, b: WorkspaceNode): number => (a.name > b.name ? 1 : -1);
|
|
105
119
|
|
|
106
120
|
/**
|
|
@@ -110,6 +124,7 @@ const byName = (a: WorkspaceNode, b: WorkspaceNode): number => (a.name > b.name
|
|
|
110
124
|
export function planAffected(
|
|
111
125
|
graph: readonly WorkspaceNode[],
|
|
112
126
|
changed: readonly string[],
|
|
127
|
+
unownedDirs: readonly string[] = [],
|
|
113
128
|
): AffectedPlan {
|
|
114
129
|
const ignored = changed.filter(isDoc);
|
|
115
130
|
const considered = changed.filter((path) => !isDoc(path));
|
|
@@ -117,18 +132,20 @@ export function planAffected(
|
|
|
117
132
|
const seeds = new Set<string>();
|
|
118
133
|
for (const path of considered) {
|
|
119
134
|
const node = owningWorkspace(graph, path);
|
|
120
|
-
if (node === undefined) rootWide.push(path);
|
|
135
|
+
if (node === undefined || node.name === TOOL_WORKSPACE) rootWide.push(path);
|
|
121
136
|
else seeds.add(node.name);
|
|
122
137
|
}
|
|
123
138
|
if (rootWide.length > 0) {
|
|
124
|
-
return { changed, ignored, rootWide, workspaces: [...graph].sort(byName) };
|
|
139
|
+
return { changed, ignored, rootWide, workspaces: [...graph].sort(byName), unowned: [] };
|
|
125
140
|
}
|
|
126
141
|
const reached = withDependents(graph, seeds);
|
|
142
|
+
const workspaces = graph.filter((node) => reached.has(node.name)).sort(byName);
|
|
127
143
|
return {
|
|
128
144
|
changed,
|
|
129
145
|
ignored,
|
|
130
146
|
rootWide,
|
|
131
|
-
workspaces
|
|
147
|
+
workspaces,
|
|
148
|
+
unowned: workspaces.length === 0 ? [] : [...unownedDirs],
|
|
132
149
|
};
|
|
133
150
|
}
|
|
134
151
|
|
|
@@ -266,8 +283,8 @@ export interface AffectedScope {
|
|
|
266
283
|
export const scopePrefixes = (plan: AffectedPlan, root: string, cwd: string): readonly string[] =>
|
|
267
284
|
plan.rootWide.length > 0
|
|
268
285
|
? ['']
|
|
269
|
-
: plan.workspaces
|
|
270
|
-
.map((
|
|
286
|
+
: [...plan.workspaces.map((workspace) => workspace.dir), ...plan.unowned]
|
|
287
|
+
.map((dir) => relative(cwd, join(root, dir)).split('\\').join('/'))
|
|
271
288
|
.filter((path) => !path.startsWith('..'));
|
|
272
289
|
|
|
273
290
|
export const inScope = (path: string, prefixes: readonly string[]): boolean =>
|
|
@@ -290,9 +307,11 @@ export interface AffectedScopeOptions {
|
|
|
290
307
|
export async function affectedScope(options: AffectedScopeOptions): Promise<AffectedScope> {
|
|
291
308
|
const selection = readAffectedSelection(options.args, options.command);
|
|
292
309
|
const root = await gitRoot(options.runner, options.cwd, options.command);
|
|
310
|
+
const graph = await readWorkspaceGraph(root);
|
|
293
311
|
const plan = planAffected(
|
|
294
|
-
|
|
312
|
+
graph,
|
|
295
313
|
await changedFiles(options.runner, { cwd: root, command: options.command, selection }),
|
|
314
|
+
await readUnownedDirs(root, graph),
|
|
296
315
|
);
|
|
297
316
|
return {
|
|
298
317
|
selection,
|
package/src/app-boundaries.ts
CHANGED
|
@@ -35,6 +35,8 @@ export const BOUNDARY_CODES = [
|
|
|
35
35
|
'X_BOUNDARY_ROUTE_TO_DB',
|
|
36
36
|
'X_BOUNDARY_SERVICE_TO_HTTP',
|
|
37
37
|
'X_BOUNDARY_SURFACE_IMPORT',
|
|
38
|
+
// `package-boundaries.ts`'s: it reads `packages/`, which this module's surface graph never holds.
|
|
39
|
+
'X_BOUNDARY_PACKAGE_TO_APP',
|
|
38
40
|
] as const;
|
|
39
41
|
|
|
40
42
|
export type BoundaryCode = (typeof BOUNDARY_CODES)[number];
|
|
@@ -192,6 +194,32 @@ const generate = (kind: 'query' | 'action', path: string, then: string): string
|
|
|
192
194
|
return `x g ${kind} ${named} # ${then} ${commentSafe(path)}`;
|
|
193
195
|
};
|
|
194
196
|
|
|
197
|
+
/** The slice a route's import lands in, or `undefined` for a package like `@acme/db`. */
|
|
198
|
+
const sliceOfImport = (path: string, specifier: string): string | undefined => {
|
|
199
|
+
// Resolved already where `scan` knew the file, and still relative where it did not.
|
|
200
|
+
const target = specifier.startsWith('.') ? normalize(join(dirname(path), specifier)) : specifier;
|
|
201
|
+
const [, surface, slice, rest] = /^apps\/[^/]+\/([^/]+)\/([^/]+)\/(.+)$/.exec(target) ?? [];
|
|
202
|
+
return surface !== undefined && SURFACE_NAMES.has(surface) && rest !== undefined
|
|
203
|
+
? slice
|
|
204
|
+
: undefined;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* A read through a database import. Into a slice, the query belongs there: without `--feature` a
|
|
209
|
+
* singular name wrote 14 files, a new entity and a table nobody asked for. A route named for its
|
|
210
|
+
* slice takes `<slice>-read`, since a query named as the entity collides with its import. With no
|
|
211
|
+
* slice to read, a new one is what the fix has to name.
|
|
212
|
+
*/
|
|
213
|
+
const routeToDbFix = (path: string, specifier: string): string => {
|
|
214
|
+
const then = `then call it from ${commentSafe(path)}`;
|
|
215
|
+
const subject = subjectOf(path);
|
|
216
|
+
const named = subject === undefined ? '<name>' : quoteArg(subject);
|
|
217
|
+
const slice = sliceOfImport(path, specifier);
|
|
218
|
+
if (slice === undefined) return `x g resource ${named} # ${then}`;
|
|
219
|
+
const query = subject === slice || subject === `${slice}s` ? quoteArg(`${slice}-read`) : named;
|
|
220
|
+
return `x g query ${query} --feature ${quoteArg(slice)} # ${then}`;
|
|
221
|
+
};
|
|
222
|
+
|
|
195
223
|
/**
|
|
196
224
|
* Both fixes are one runnable line, with the rest of the instruction behind a `#` — a fix a
|
|
197
225
|
* caller has to edit before it runs is prose, and prose is what "errors are instructions" bans.
|
|
@@ -204,7 +232,7 @@ function layerFindings(scanned: readonly ScannedFile[]): readonly Finding[] {
|
|
|
204
232
|
findings.push({
|
|
205
233
|
code: 'X_BOUNDARY_ROUTE_TO_DB',
|
|
206
234
|
cause: `route imports the database ("${specifier}")${isRepoSpecifier(specifier) ? ', through a repo' : ''} — routes call actions and queries`,
|
|
207
|
-
fix:
|
|
235
|
+
fix: routeToDbFix(file.path, specifier),
|
|
208
236
|
docs: ERROR_DOCS_URL,
|
|
209
237
|
at: file.path,
|
|
210
238
|
});
|
package/src/app-evals.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
baselinePath,
|
|
10
|
+
describeAgents,
|
|
10
11
|
describeEvals,
|
|
11
12
|
EvalBaselineMissingError,
|
|
12
13
|
EvalMissingError,
|
|
@@ -36,10 +37,17 @@ export async function checkEvalCoverage(root: string): Promise<readonly Finding[
|
|
|
36
37
|
// Loading is idempotent per process: `x verify` loads the app once and every later step,
|
|
37
38
|
// including the manifest, reads the same registries.
|
|
38
39
|
await loadApp(root);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
// An agent's prompt is measured by the agent arm — its tools and turns — so the fix names it.
|
|
41
|
+
const agents = new Map(describeAgents().map((fact) => [fact.prompt, fact.name]));
|
|
42
|
+
return promptsWithoutEvals().map((prompt) => {
|
|
43
|
+
const agent = agents.get(prompt.ref);
|
|
44
|
+
const missing = new EvalMissingError({
|
|
45
|
+
prompt: prompt.ref,
|
|
46
|
+
id: prompt.id,
|
|
47
|
+
...(agent === undefined ? {} : { agent }),
|
|
48
|
+
});
|
|
49
|
+
return { ...findingFrom(missing), at: prompt.ref };
|
|
50
|
+
});
|
|
43
51
|
}
|
|
44
52
|
|
|
45
53
|
/**
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// A module that imports what does not resolve. It was `X_CLI_UNEXPECTED`, once per module reaching
|
|
2
|
+
// it, with `x doctor --json` as the fix, and doctor answered "shippable": the cycle closed on itself.
|
|
3
|
+
// Leaf module: `output.ts` reads it for a throw outside the scan, and the scan for one inside it.
|
|
4
|
+
|
|
5
|
+
// why: a synchronous probe for an installed package's manifest; Bun ships no path helpers.
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { stringField } from '@ultimat3/core';
|
|
9
|
+
import type { Finding } from './output';
|
|
10
|
+
import { posixRelative } from './posix-path';
|
|
11
|
+
import { quoteArg } from './shell-quote';
|
|
12
|
+
|
|
13
|
+
export const IMPORT_UNRESOLVED = 'X_APP_IMPORT_UNRESOLVED';
|
|
14
|
+
|
|
15
|
+
/** npm's own shape, so `@/lib/x` (a path alias) and `node:nope` are not offered to `bun add`. */
|
|
16
|
+
const PACKAGE_NAME = /^(?:@[a-z0-9~-][a-z0-9._~-]*\/)?[a-z0-9~-][a-z0-9._~-]*/;
|
|
17
|
+
|
|
18
|
+
/** `@scope/pkg/sub` → `@scope/pkg`, or `undefined` for what no install can resolve. */
|
|
19
|
+
export const installablePackage = (specifier: string): string | undefined => {
|
|
20
|
+
const name = PACKAGE_NAME.exec(specifier)?.[0];
|
|
21
|
+
if (name === undefined) return undefined;
|
|
22
|
+
const rest = specifier.slice(name.length);
|
|
23
|
+
return rest === '' || rest.startsWith('/') ? name : undefined;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Is `pkg` in a `node_modules` the referrer resolves through, as the resolver walks: up to `/`. */
|
|
27
|
+
const isInstalled = (pkg: string, referrer: string): boolean => {
|
|
28
|
+
for (let dir = dirname(referrer); ; dir = dirname(dir)) {
|
|
29
|
+
if (existsSync(join(dir, 'node_modules', pkg, 'package.json'))) return true;
|
|
30
|
+
if (dirname(dir) === dir) return false;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Bun's `ResolveMessage`, read through `stringField`: a caught value this process did not build. The
|
|
36
|
+
* `specifier` and `referrer` are the failing import's own, however many modules were importing the
|
|
37
|
+
* file that holds it, which is what makes one finding per specifier true.
|
|
38
|
+
*/
|
|
39
|
+
export function unresolvedImportFinding(error: unknown, root: string): Finding | undefined {
|
|
40
|
+
if (stringField(error, 'code') !== 'ERR_MODULE_NOT_FOUND') return undefined;
|
|
41
|
+
const specifier = stringField(error, 'specifier');
|
|
42
|
+
if (specifier === undefined) return undefined;
|
|
43
|
+
const referrer = stringField(error, 'referrer');
|
|
44
|
+
const at = referrer === undefined ? undefined : posixRelative(root, referrer);
|
|
45
|
+
const where = at ?? 'an app module';
|
|
46
|
+
const named = installablePackage(specifier);
|
|
47
|
+
// Installed, and missing only the subpath: `bun add` would exit 0 and change nothing.
|
|
48
|
+
const installed = named !== undefined && referrer !== undefined && isInstalled(named, referrer);
|
|
49
|
+
const pkg = installed ? undefined : named;
|
|
50
|
+
const edit = `grep -nF -- ${quoteArg(specifier)} ${quoteArg(where)} # the import no file resolves: edit it`;
|
|
51
|
+
return {
|
|
52
|
+
code: IMPORT_UNRESOLVED,
|
|
53
|
+
cause: installed
|
|
54
|
+
? `${where} imports "${specifier}", and ${String(named)} is installed with no such subpath`
|
|
55
|
+
: pkg === undefined
|
|
56
|
+
? `${where} imports "${specifier}", which resolves to no file`
|
|
57
|
+
: `an app module imports ${pkg}, which is not installed`,
|
|
58
|
+
fix: pkg === undefined ? edit : `bun add ${quoteArg(pkg)}`,
|
|
59
|
+
...(at === undefined ? {} : { at }),
|
|
60
|
+
meta: { specifier },
|
|
61
|
+
};
|
|
62
|
+
}
|
package/src/app-load.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
// accident one refactor away from compiling every app's `.tsx` to `React.createElement`. The named
|
|
30
30
|
// import is a value import, so the side effect holds without the bare line it replaced.
|
|
31
31
|
import { setStylesheetRoot } from '@ultimat3/render/server';
|
|
32
|
+
import { unresolvedImportFinding } from './app-load-unresolved';
|
|
32
33
|
import {
|
|
33
34
|
evictChanged,
|
|
34
35
|
pinModule,
|
|
@@ -61,6 +62,13 @@ const APP_GLOBS = [
|
|
|
61
62
|
*/
|
|
62
63
|
const ENTRY_POINT = /^apps\/[^/]+\/(?:server|prerender)\.tsx?$/;
|
|
63
64
|
|
|
65
|
+
/**
|
|
66
|
+
* `apps/<app>/exercise.ts` is `x exercise`'s input, read by that command alone. It imports
|
|
67
|
+
* `@ultimat3/testing`, a devDependency a production image does not install, so the boot scan that
|
|
68
|
+
* imported it would fail the module on every start.
|
|
69
|
+
*/
|
|
70
|
+
const EXERCISE_FILE = /^apps\/[^/]+\/exercise\.ts$/;
|
|
71
|
+
|
|
64
72
|
/**
|
|
65
73
|
* A `*.island.tsx` is a CLIENT entry point and is deliberately not imported here. It registers no
|
|
66
74
|
* primitive — a page names it by specifier, never by import — and importing it would put the one
|
|
@@ -138,13 +146,13 @@ export function sortModulePaths(paths: readonly string[]): string[] {
|
|
|
138
146
|
* Every module path the app globs match, SORTED — with the API index first. `Bun.Glob` answers in
|
|
139
147
|
* directory order, which is the filesystem's — ext4 hashes names with a per-filesystem seed — so
|
|
140
148
|
* two pods of one image imported the app in two orders, registered its stylesheets in two orders,
|
|
141
|
-
* and served two different `/styles/<hash>.css` for one page (
|
|
149
|
+
* and served two different `/styles/<hash>.css` for one page (one adopter, 22.3.2). Import order
|
|
142
150
|
* is the stylesheet cascade's order, so it must be the same everywhere.
|
|
143
151
|
*
|
|
144
152
|
* The API index leads because `defineApi({ http: { pathStyle } })` is what every action URL is
|
|
145
153
|
* derived under: sorted, `apps/admin/**` evaluated before it, and a module-level
|
|
146
154
|
* `derivePath('signOut').path` there captured the default style's `/api/outs/sign` — a form
|
|
147
|
-
* posting to a URL no route served (
|
|
155
|
+
* posting to a URL no route served (one adopter, 22.6.0). Anything still evaluated before the
|
|
148
156
|
* declaration (the index's own imports) is refused at it, `X_ACTION_PATH_DERIVED_EARLY`.
|
|
149
157
|
*/
|
|
150
158
|
export async function appModulePaths(root: string): Promise<readonly string[]> {
|
|
@@ -211,32 +219,33 @@ export async function scanAppModules(root: string, options: ScanOptions): Promis
|
|
|
211
219
|
// `~/work/my.test.app` matched `.test.` on every file and loaded none of them.
|
|
212
220
|
const file = posixRelative(root, absolute);
|
|
213
221
|
if (hasPathSegment(absolute, 'node_modules') || isTest(file)) continue;
|
|
214
|
-
if (
|
|
222
|
+
if (
|
|
223
|
+
ENTRY_POINT.test(file) ||
|
|
224
|
+
EXERCISE_FILE.test(file) ||
|
|
225
|
+
CLIENT_ENTRY_POINT.test(file) ||
|
|
226
|
+
STATES_FILE.test(file)
|
|
227
|
+
) {
|
|
215
228
|
continue;
|
|
216
229
|
}
|
|
217
230
|
if (options.include !== undefined && !(await options.include(absolute))) continue;
|
|
218
|
-
// The source is read BEFORE the import, on the file's first pass
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
// no save lands in an image, so a route module's text is read after its import, once.
|
|
231
|
+
// The source is read BEFORE the import, on the file's first pass — a rescan of a registered
|
|
232
|
+
// module reads nothing here. What is recorded is bound to the bytes the module was evaluated
|
|
233
|
+
// from, and a read AFTER the import cannot know which bytes those were: a save landing between
|
|
234
|
+
// the two bound V1's component to V2's hash, so the next scan saw nothing to do and served V1
|
|
235
|
+
// until the save after. Read first, the worst case is one re-import the next tick, of a file
|
|
236
|
+
// that did change. An untracked scan reads nothing here: no save lands in an image.
|
|
225
237
|
const first = !registered.has(absolute) && !failures.has(absolute);
|
|
226
|
-
const snapshot =
|
|
227
|
-
options.track && (first || routeModules.has(absolute))
|
|
228
|
-
? await Bun.file(absolute).text()
|
|
229
|
-
: undefined;
|
|
238
|
+
const snapshot = options.track && first ? await Bun.file(absolute).text() : undefined;
|
|
230
239
|
if (first && snapshot !== undefined) await trackModule(absolute, snapshot, root);
|
|
231
240
|
let module: Record<string, unknown>;
|
|
232
241
|
try {
|
|
233
242
|
module = (await import(absolute)) as Record<string, unknown>;
|
|
234
243
|
} catch (error) {
|
|
235
|
-
findings.push({ ...findingFrom(error), at: file });
|
|
244
|
+
findings.push(unresolvedImportFinding(error, root) ?? { ...findingFrom(error), at: file });
|
|
236
245
|
continue;
|
|
237
246
|
}
|
|
238
247
|
files.push(file);
|
|
239
|
-
const finding = await register(absolute, file, module,
|
|
248
|
+
const finding = await register(absolute, file, module, options.track);
|
|
240
249
|
if (finding !== undefined) findings.push(finding);
|
|
241
250
|
}
|
|
242
251
|
|
|
@@ -303,25 +312,22 @@ const emptyAppFinding = (root: string): Finding => ({
|
|
|
303
312
|
/**
|
|
304
313
|
* Registers a module once; every later call replays whatever the first one reported — except for
|
|
305
314
|
* a route module, which a later call re-registers when the import handed back a new instance.
|
|
306
|
-
* `snapshot` is the source read before this import: always for a first pass and a route module of
|
|
307
|
-
* a TRACKED scan. An untracked one hands none, and a route module's text is read here instead.
|
|
308
315
|
*/
|
|
309
316
|
async function register(
|
|
310
317
|
absolute: string,
|
|
311
318
|
file: string,
|
|
312
319
|
module: Record<string, unknown>,
|
|
313
|
-
snapshot: string | undefined,
|
|
314
320
|
track: boolean,
|
|
315
321
|
): Promise<Finding | undefined> {
|
|
316
322
|
const previous = failures.get(absolute);
|
|
317
323
|
if (previous !== undefined) return previous;
|
|
318
|
-
if (registered.has(absolute)) return reloadRoute(absolute, file, module
|
|
324
|
+
if (registered.has(absolute)) return track ? reloadRoute(absolute, file, module) : undefined;
|
|
319
325
|
registered.add(absolute);
|
|
320
326
|
try {
|
|
321
327
|
const config = module['config'];
|
|
322
328
|
const route = isRouteConfig(config) ? config : undefined;
|
|
323
329
|
if (route !== undefined) {
|
|
324
|
-
registerRouteModule(file, module, route
|
|
330
|
+
registerRouteModule(file, module, route);
|
|
325
331
|
routeModules.set(absolute, module);
|
|
326
332
|
}
|
|
327
333
|
// A pin is the reload graph's: it stops an eviction a process that never rescans never runs,
|
|
@@ -364,18 +370,11 @@ function definesPrimitive(module: Record<string, unknown>, route: boolean): bool
|
|
|
364
370
|
});
|
|
365
371
|
}
|
|
366
372
|
|
|
367
|
-
/**
|
|
368
|
-
* The route half of a registration, and the whole of a re-registration. `source` is the text the
|
|
369
|
-
* module was imported from — read by the caller BEFORE the import, never here after it — and the
|
|
370
|
-
* hash the entry is bound to is that text's. Until 2026-09-07 this read the file again, and a save
|
|
371
|
-
* between the import and that read registered the old component under the new hash: the next
|
|
372
|
-
* scan compared equal, and the page on disk was not served until the save after it.
|
|
373
|
-
*/
|
|
373
|
+
/** The route half of a registration, and the whole of a re-registration. */
|
|
374
374
|
function registerRouteModule(
|
|
375
375
|
file: string,
|
|
376
376
|
module: Record<string, unknown>,
|
|
377
377
|
config: RouteConfig,
|
|
378
|
-
source: string,
|
|
379
378
|
): void {
|
|
380
379
|
// The page component comes from the same module as its config, resolved by render's own
|
|
381
380
|
// rule — the CLI does not decide which export is a page any more than it decides what a
|
|
@@ -384,9 +383,6 @@ function registerRouteModule(
|
|
|
384
383
|
registerRoute({
|
|
385
384
|
file,
|
|
386
385
|
config,
|
|
387
|
-
// The build counts boundaries from the compiled JSX; before a build there is only the
|
|
388
|
-
// source, and `render: 'stream'` is rejected without one — so count them in the text.
|
|
389
|
-
suspenseBoundaries: countSuspense(source),
|
|
390
386
|
...(component === undefined ? {} : { component }),
|
|
391
387
|
});
|
|
392
388
|
}
|
|
@@ -401,16 +397,15 @@ function reloadRoute(
|
|
|
401
397
|
absolute: string,
|
|
402
398
|
file: string,
|
|
403
399
|
module: Record<string, unknown>,
|
|
404
|
-
source: string | undefined,
|
|
405
400
|
): Finding | undefined {
|
|
406
401
|
const held = routeModules.get(absolute);
|
|
407
|
-
if (held === undefined || held === module
|
|
402
|
+
if (held === undefined || held === module) return undefined;
|
|
408
403
|
try {
|
|
409
404
|
const config = module['config'];
|
|
410
405
|
// A file that stopped being a route is not un-registered — the table has no verb for it, and a
|
|
411
406
|
// deleted file needs a restart either way. Its instance is recorded so the same save is not
|
|
412
407
|
// re-registered on every later tick.
|
|
413
|
-
if (isRouteConfig(config)) registerRouteModule(file, module, config
|
|
408
|
+
if (isRouteConfig(config)) registerRouteModule(file, module, config);
|
|
414
409
|
routeModules.set(absolute, module);
|
|
415
410
|
return undefined;
|
|
416
411
|
} catch (error) {
|
|
@@ -418,8 +413,6 @@ function reloadRoute(
|
|
|
418
413
|
}
|
|
419
414
|
}
|
|
420
415
|
|
|
421
|
-
const countSuspense = (source: string): number => source.match(/<Suspense[\s/>]/g)?.length ?? 0;
|
|
422
|
-
|
|
423
416
|
/**
|
|
424
417
|
* `packages/db/src/errors.ts` → `packages/db`; `apps/web/app/posts/errors.ts` → `apps/web` — on
|
|
425
418
|
* either separator. A manifest written on Windows split a `\`-path on `/`, kept the whole file as
|
package/src/app-mcp.ts
CHANGED
|
@@ -13,12 +13,14 @@ import { existsSync } from 'node:fs';
|
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { logger } from '@ultimat3/core';
|
|
15
15
|
import type { Route } from '@ultimat3/http';
|
|
16
|
-
import { jsonResponse, selfOrigin } from '@ultimat3/http';
|
|
16
|
+
import { jsonResponse, originListed, selfOrigin } from '@ultimat3/http';
|
|
17
17
|
import {
|
|
18
18
|
type AppMcp,
|
|
19
|
+
assertRecipeLinks,
|
|
19
20
|
McpAppUnmountedError,
|
|
20
21
|
McpPathDuplicateError,
|
|
21
22
|
PROTECTED_RESOURCE_WELL_KNOWN,
|
|
23
|
+
servedRecipes,
|
|
22
24
|
} from '@ultimat3/mcp';
|
|
23
25
|
import { loadAppConfig } from './app-config-load';
|
|
24
26
|
|
|
@@ -103,6 +105,9 @@ export async function appMcpMount(root: string): Promise<AppMcpMount> {
|
|
|
103
105
|
const module = (await import(join(root, file))) as Record<string, unknown>;
|
|
104
106
|
const endpoints = endpointsOf(module[APP_MCP_EXPORT]);
|
|
105
107
|
if (endpoints === undefined) continue;
|
|
108
|
+
// Every endpoint is built by now: an error's declared `docs://recipes/` link must name a recipe
|
|
109
|
+
// one of them serves (`X_MCP_RECIPE_UNKNOWN`), the union and never one endpoint alone.
|
|
110
|
+
assertRecipeLinks(servedRecipes());
|
|
106
111
|
return mountEndpoints(endpoints, file);
|
|
107
112
|
}
|
|
108
113
|
return {
|
|
@@ -199,8 +204,15 @@ function endpointRoutes(route: Descriptor, path: string, index: number): readonl
|
|
|
199
204
|
route.handle(request.raw, {
|
|
200
205
|
origin: selfOrigin(ctx.url, ctx.https),
|
|
201
206
|
address: ctx.ip ?? undefined,
|
|
207
|
+
// A browser `Origin` is proven the way CSRF proves it: this origin, or one listed exactly.
|
|
208
|
+
listed: (origin) => originListed(ctx.config.cors, origin),
|
|
202
209
|
}),
|
|
203
|
-
|
|
210
|
+
// `authenticate` answers nobody: the route's ONE credential is the bearer `resolveToken`
|
|
211
|
+
// reads, so a session cookie authenticates nothing here. Without it the app's authenticator
|
|
212
|
+
// made a cookie-carrying (or, under `x dev`, any) request a user's, and CSRF then refused a
|
|
213
|
+
// token-less MCP client 403 where it is owed the 401 and its challenge. No cookie identifies
|
|
214
|
+
// anyone, so there is no ambient credential for a forged request to ride.
|
|
215
|
+
meta: { name, auth: 'public', enforcedBy: 'handler', authenticate: () => null },
|
|
204
216
|
},
|
|
205
217
|
...metadata,
|
|
206
218
|
];
|