@ultimat3/cli 29.4.0 → 30.1.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 +11 -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 +9 -0
- 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 +41 -81
- 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 +7 -1
- 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 +1314 -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,289 @@
|
|
|
1
|
+
# Tutorial 1 — your first app
|
|
2
|
+
|
|
3
|
+
One command scaffolds a monorepo that already runs. No Docker daemon, no database to provision, no `.env` to fill in.
|
|
4
|
+
|
|
5
|
+
`As of 2026-08-23`, with every gate row re-measured `As of 2026-09-12`. **This page documents `main`, not a published release.** Every file count, gate result and command output below was re-measured on this tree: `x new` → `bun install` → the command, in a directory outside the checkout. Rows that still describe a published version say so in their own line.
|
|
6
|
+
|
|
7
|
+
Series: **1** · [2 — first feature](Tutorial-02-First-Feature) · [3 — auth and admin](Tutorial-03-Auth-And-Admin) · [4 — jobs and realtime](Tutorial-04-Jobs-And-Realtime) · [5 — deploy free](Tutorial-05-Deploy-Free) · [6 — growing up](Tutorial-06-Growing-Up)
|
|
8
|
+
|
|
9
|
+
## Prerequisites
|
|
10
|
+
|
|
11
|
+
| Need | Version | Check |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| Bun | ≥ 1.4.4 | `bun --version` |
|
|
14
|
+
| Docker | only for [tutorial 5](Tutorial-05-Deploy-Free) | `docker --version` |
|
|
15
|
+
| Postgres | never, locally | — |
|
|
16
|
+
|
|
17
|
+
## Scaffold
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
cd $(mktemp -d)
|
|
21
|
+
bunx create-ultimate myapp
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Unpinned on purpose.** Pinning the tutorial to a version pins readers to a scaffold that stops matching this page on the next tag; `README.md` and `llms.txt` drop the pin for the same reason. To reproduce a specific release, add `@<version>` and read that release's tag of this page.
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
213 files in /tmp/tmp.XXXXXXXX/myapp
|
|
28
|
+
✓ created myapp — next: cd myapp && bun run setup && bun run dev
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`As of 2026-11-03`; `--no-example` writes 181.
|
|
32
|
+
|
|
33
|
+
Counts are a derived fact — `x new --dry-run --json` lists every file, and one added template moves the number.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
cd myapp && bun run setup
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
setup complete — next: bun run dev
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`bun run setup` is `bun install`, an `.env.development.local` touch, `x db gen "initial"`, `x db migrate`, `x db seed`, then `x manifest` — so the database is migrated and `x.manifest.json` exists before the gate asks for either. `x new` installs nothing, so this is not optional.
|
|
44
|
+
|
|
45
|
+
`x` is a workspace dependency, not a global install. `bunx x <command>` resolves `node_modules/.bin/x`; the `package.json` scripts (`bun run verify`, `bun run dev`) resolve the same binary. A globally installed `x` (`bun link`, `bun add -g`) run inside the app **hands over** to that same file, `As of 2026-09-05`, and says so on stderr: a global copy carries its own `@ultimat3/entity` with an empty registry, and it wrote a zero-entity manifest under a green exit code before it did. `ULTIMATE_KEEP_GLOBAL_CLI=1` is the opt-out — [`@ultimat3/cli`'s README](https://github.com/developerz-ai/ultimate/blob/main/packages/cli/README.md) has the measurement.
|
|
46
|
+
|
|
47
|
+
### Two shapes, one generator
|
|
48
|
+
|
|
49
|
+
Counts are derived, never quoted here — `x new --dry-run --json | jq '.data.files | length'`, per
|
|
50
|
+
shape. Measured on fresh scaffolds of this tree, `As of 2026-11-05`:
|
|
51
|
+
|
|
52
|
+
| Invocation | `bun run setup && bun run verify` on run one |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `bunx create-ultimate myapp` | **green, 20 of 21 steps pass** — `budgets` among them; `roadmap` skipped (a framework-repo step) |
|
|
55
|
+
| `bunx create-ultimate myapp --no-example` | **green, 17 of 21 steps pass** — `live`, `job`, `e2e` skipped (no such test yet) and `roadmap` |
|
|
56
|
+
|
|
57
|
+
`--no-example` omits the `post` feature slice and its route. Pick it when an agent is about to write the real feature anyway — [tutorial 2](Tutorial-02-First-Feature) starts there. Pick the default when you want a worked example to read.
|
|
58
|
+
|
|
59
|
+
## The gate, first thing
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
bun run verify
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The app's own gate is `x verify`, alone. Its `budgets` step weighs `.x/build-stats.json`, and on a
|
|
66
|
+
fresh scaffold no such file exists yet, so the step runs `x build --target static` itself first
|
|
67
|
+
(into `.x/verify-static`, never over `.x/static`).
|
|
68
|
+
|
|
69
|
+
The default scaffold, after `bun run setup`, with no `.x/build-stats.json` beforehand,
|
|
70
|
+
`As of 2026-11-03` (`exercise` measured 2026-11-05): the whole gate, in the order it runs:
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
✓ typecheck 3064ms
|
|
74
|
+
✓ lint 232ms
|
|
75
|
+
✓ boundaries 844ms
|
|
76
|
+
✓ filesize 906ms
|
|
77
|
+
✓ package-shape 402ms
|
|
78
|
+
✓ errors 951ms
|
|
79
|
+
✓ unit 2790ms 3 workers (budget 4.0 GB)
|
|
80
|
+
✓ contract 1233ms 3 workers (budget 4.0 GB)
|
|
81
|
+
✓ live 840ms serial
|
|
82
|
+
✓ job 345ms 1 of 3 workers (budget 4.0 GB)
|
|
83
|
+
✓ exercise 1952ms
|
|
84
|
+
✓ e2e 12124ms serial
|
|
85
|
+
✓ eval 91ms
|
|
86
|
+
✓ drift 1486ms
|
|
87
|
+
✓ contract-diff 78ms
|
|
88
|
+
✓ budgets 6336ms
|
|
89
|
+
✓ seo 40ms
|
|
90
|
+
✓ i18n 63ms
|
|
91
|
+
✓ policy 75ms
|
|
92
|
+
✓ manifest 1131ms
|
|
93
|
+
- roadmap 0ms
|
|
94
|
+
✓ 20 of 21 steps passed in …ms — 1 skipped: roadmap
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`-` is skipped, not passed: `roadmap` is a framework-repo step. The default scaffold ships its
|
|
98
|
+
example slice with a live test, a job test and an e2e test, so `live`, `job` and `e2e` are ticks on
|
|
99
|
+
run one; **with `--no-example`** there is no such test yet, and those three are dashes too. `contract` is **not** among them — the scaffold
|
|
100
|
+
ships `apps/web/api/health.contract.test.ts`, which is what makes it a tick on run one. Nor is
|
|
101
|
+
`exercise`: it needs no test file — it runs every action, query and job the app registers, once,
|
|
102
|
+
against seeded rows — and both shapes write the `apps/web/exercise.ts` it reads. The summary
|
|
103
|
+
names every skip, so a gate that is green because a suite does not exist says so on the one line you
|
|
104
|
+
read. [Tutorial 2](Tutorial-02-First-Feature) turns more of those dashes into ticks.
|
|
105
|
+
|
|
106
|
+
The list is
|
|
107
|
+
`VERIFY_STEP_NAMES` ([`packages/cli/src/verify-step.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/cli/src/verify-step.ts)), whole or not at all: the same steps run here and in the framework repo.
|
|
108
|
+
|
|
109
|
+
### `budgets` builds before it weighs
|
|
110
|
+
|
|
111
|
+
`budgets` took 6.3 s above because it built first. Delete the stats and `--verbose` prints the line
|
|
112
|
+
that says so; run it again and it reads the stats it just wrote:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
rm .x/build-stats.json
|
|
116
|
+
bunx x verify --only budgets --verbose
|
|
117
|
+
bunx x verify --only budgets
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```text
|
|
121
|
+
✓ budgets 7320ms
|
|
122
|
+
| built first: build stats were missing — x build --target static --out .x/verify-static (6968ms)
|
|
123
|
+
✓ NOT A GATE RUN — all 1 steps passed in …ms
|
|
124
|
+
✓ budgets 324ms
|
|
125
|
+
✓ NOT A GATE RUN — all 1 steps passed in …ms
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
It builds when `.x/build-stats.json` is missing, written by an earlier measurement rule, or older
|
|
129
|
+
than the newest app source file (tests excluded), and never otherwise: run the gate again with
|
|
130
|
+
nothing edited and the step reads the stats it has. After an edit, the gate pays one static build.
|
|
131
|
+
A build that fails is the step's verdict, under the build's own code and `fix:`.
|
|
132
|
+
|
|
133
|
+
CI asserts the result: `scaffold-smoke` runs a fresh app's own scripts **once, with no waiver and no fix-follow**, and `budgets` must come back **green** rather than merely not-red — a skip would mean the static build bought nothing ([`scripts/scaffold-gate.ts`](https://github.com/developerz-ai/ultimate/blob/main/scripts/scaffold-gate.ts)).
|
|
134
|
+
|
|
135
|
+
### The invariant block is typed from your columns
|
|
136
|
+
|
|
137
|
+
`invariants` is one callback over the whole list, and `c` is typed from the `columns` above it:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
invariants: (c) => [
|
|
141
|
+
invariant('post_title_not_blank', c.title.trimmed().minLength(1)),
|
|
142
|
+
invariant('post_price_non_negative', c.price.minor.atLeast(0)),
|
|
143
|
+
],
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`c.titel` is a compile error that names `title`, not a runtime surprise. Everything `x new` and `x g` write typechecks as generated — no `!`, no edit before the first `x verify`.
|
|
147
|
+
|
|
148
|
+
What still ships broken: [Known gaps](Known-Gaps).
|
|
149
|
+
|
|
150
|
+
## Run it
|
|
151
|
+
|
|
152
|
+
<!-- no-run: a server that runs until stopped; the --once fence below is its smoke test -->
|
|
153
|
+
```bash
|
|
154
|
+
bunx x dev
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`--once` boots, reports and exits — the shape to paste into a smoke test:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
bunx x dev --once --port 3100
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```text
|
|
164
|
+
{"ts":"2026-11-03T16:48:57.124Z","level":"info","msg":"ultimate web listening on http://localhost:3100"}
|
|
165
|
+
{"ts":"2026-11-03T16:48:57.118Z","level":"info","msg":"sync node ready","buildId":"e6af64a2077e5d60","path":"/_x/sync"}
|
|
166
|
+
{"ts":"2026-11-03T16:48:57.134Z","level":"info","msg":"jobs.worker.started","workerId":"worker-01a1a250-…","queues":["myapp-default","default"]}
|
|
167
|
+
{"ts":"2026-11-03T16:48:57.137Z","level":"info","msg":"jobs.scheduler.started","tasks":1}
|
|
168
|
+
{"ts":"2026-11-03T16:48:57.142Z","level":"info","msg":"live feed in-process","detail":"the embedded database has no walsender; repository writes in this process reach subscribers"}
|
|
169
|
+
roles web, sync, worker, scheduler
|
|
170
|
+
panels routes, timeline, live, jobs, db, mail, cache, policy, manifest, services, boundaries
|
|
171
|
+
manifest /tmp/…/myapp/x.manifest.json
|
|
172
|
+
introspect http://localhost:3100/_x
|
|
173
|
+
mcp POST /mcp
|
|
174
|
+
✓ dev ready on http://localhost:3100 — /_x mounted (11 panels), db=embedded events=embedded storage=embedded mail=embedded cdn=none live=in-process
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
An excerpt, `As of 2026-11-03`: the boot also logs the dev viewer warning, the metrics port and the drain `--once` ends with.
|
|
178
|
+
|
|
179
|
+
Four roles in one process, isolation simulated rather than skipped. `replicator` is opt-in via `--role`; `migrate` is not a dev role.
|
|
180
|
+
|
|
181
|
+
### Why no Docker
|
|
182
|
+
|
|
183
|
+
`.env.development` ships every service key **empty**, and empty means embedded:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
DATABASE_URL=
|
|
187
|
+
NATS_URL=
|
|
188
|
+
S3_ENDPOINT=
|
|
189
|
+
PORT=3000
|
|
190
|
+
ROLE=web
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
| Key | Unset means | Set means |
|
|
194
|
+
|---|---|---|
|
|
195
|
+
| `DATABASE_URL` | PGlite in this process, under `.x/pgdata` | that Postgres |
|
|
196
|
+
| `NATS_URL` | in-process fanout | that NATS server |
|
|
197
|
+
| `S3_ENDPOINT` | `.x/storage` on disk | that S3 |
|
|
198
|
+
|
|
199
|
+
Nothing in the framework branches on `if (dev)` — only the driver behind the seam differs. Same code path, same `db()`, same `enqueue()`. Full table: [CLI reference § x dev](CLI-Reference).
|
|
200
|
+
|
|
201
|
+
## What each directory is
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
myapp/
|
|
205
|
+
├── app.config.ts the one config file — names env KEYS, never values
|
|
206
|
+
├── apps/
|
|
207
|
+
│ ├── web/
|
|
208
|
+
│ │ ├── site/ static, 0kb JS, SEO-critical. May NOT import app/
|
|
209
|
+
│ │ ├── app/ authed, streaming, realtime. Feature slices live here
|
|
210
|
+
│ │ ├── api/ actions only — no rendering, no components
|
|
211
|
+
│ │ ├── shared/ actor type, tokens, primitives. A leaf both surfaces import
|
|
212
|
+
│ │ ├── server.ts the production entry: ROLE + PORT, nothing else
|
|
213
|
+
│ │ └── prerender.ts the static entry: one HTML file per `render: 'static'` route,
|
|
214
|
+
│ │ plus sitemap.xml and robots.txt
|
|
215
|
+
│ ├── admin/ the admin app, gated on `admin:read`
|
|
216
|
+
│ ├── desktop/ mobile/ README stubs, no code
|
|
217
|
+
├── packages/
|
|
218
|
+
│ ├── db/ schema.ts (the entity export list — the package's public
|
|
219
|
+
│ │ surface, not the generator's input) + migrations/
|
|
220
|
+
│ ├── domain/ pure types, no I/O
|
|
221
|
+
│ ├── i18n/ catalogs/<locale>.json — one flat file per locale
|
|
222
|
+
│ ├── mcp/ the app's own MCP surface over its actions
|
|
223
|
+
│ └── ui/ app components on semantic tokens
|
|
224
|
+
├── docker/ Dockerfile, .dockerignore, dev + prod compose, helm/
|
|
225
|
+
├── guards/ one rule per file, run by `x verify`'s boundaries step.
|
|
226
|
+
│ Every name in SHIPPED_GUARD_NAMES ships; `x doctor`
|
|
227
|
+
│ lists any missing. Delete one by deleting its file
|
|
228
|
+
├── bin/ setup.ts — `bun run setup`; `dev` and `verify` are x's own
|
|
229
|
+
└── CLAUDE.md AGENTS.md what an agent reads first: one file, AGENTS.md a symlink
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Boundaries are a build error, not a note — but `boundaries` is a **step of `x verify`**, not a top-level command. `bunx x boundaries` exits `X_CLI_UNKNOWN_COMMAND`. Run `bunx x verify` and read the `boundaries` line. Full model: [Project layout](Project-Layout).
|
|
233
|
+
|
|
234
|
+
### The database, first run
|
|
235
|
+
|
|
236
|
+
`packages/db/migrations` starts **empty**, and `x db gen` is its only writer. Two commands, in this order:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
bunx x db gen "initial" # entities → <id>.sql, <id>.snapshot.json, <id>.hash
|
|
240
|
+
bunx x db migrate # applies them, then diffs the live schema against the ledger it wrote
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
`bun run setup` runs both — `bun install`, an `.env.development.local` touch, `x db gen "initial"` when the directory holds no `.sql`, `x db migrate`, `x db seed`, then `x manifest`. `x db migrate` is `@ultimat3/db`'s own migrator, the one `ROLE=migrate` runs, so nothing extra has to be installed.
|
|
244
|
+
|
|
245
|
+
Until the generate has run, `x verify` is **red on its `drift` step** — correct behaviour with a runnable fix, not a defect:
|
|
246
|
+
|
|
247
|
+
```text
|
|
248
|
+
X_DB_DRIFT: schema differs from migrations
|
|
249
|
+
cause: packages/db has a schema but no migration recorded it
|
|
250
|
+
fix: x db gen "initial"
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
That is the scaffold **with** the example slice, which declares one entity. `x new --no-example` declares none, and zero entities against zero migrations is agreement rather than drift, so its `drift` step is green from the first run and goes red the moment you write your first `entity()`.
|
|
254
|
+
|
|
255
|
+
`x new` hand-wrote a `0000_initial.sql` until 2026-08 and that was the defect — a second writer of a directory the generator owns, whose file carried no `.snapshot.json`, so `x db migrate` answered `X_DB_DRIFT` naming `x db gen` and `x db gen` answered `X_MIGRATION_SNAPSHOT_MISSING` naming version control for a file version control never had. Neither `fix:` now names the command that raises the other ([Known gaps](Known-Gaps)).
|
|
256
|
+
|
|
257
|
+
## The routes are clean
|
|
258
|
+
|
|
259
|
+
`x routes` on a fresh scaffold reports four file routes and no findings, plus the admin's
|
|
260
|
+
screens. `As of 2026-10`, read off the templates `x new` writes:
|
|
261
|
+
|
|
262
|
+
| path | surface | render | hydrate | offline | declared by |
|
|
263
|
+
|---|---|---|---|---|---|
|
|
264
|
+
| `/` | site | static | never | precache | `apps/web/site/page.tsx` |
|
|
265
|
+
| `/offline` | site | static | never | precache | `apps/web/site/offline/page.tsx` |
|
|
266
|
+
| `/dashboard` | app | ssr | visible | runtime | `apps/web/app/dashboard/page.tsx` |
|
|
267
|
+
| `/posts` | app | ssr | visible | runtime | `apps/web/app/posts/page.tsx` |
|
|
268
|
+
|
|
269
|
+
The admin has no page file: `apps/admin/app/admin/admin.ts` declares `defineAdmin()`, which mounts
|
|
270
|
+
`/admin` and a list, detail and form per entity. Those rows name the call and the permissions that
|
|
271
|
+
gate them instead of a file — `defineAdmin() · admin:read + posts:read` — and their `--json` rows
|
|
272
|
+
carry `mount`. `--no-example` drops `/posts`. `x routes` is not a gate step either way, so a finding
|
|
273
|
+
here never fails `x verify` — run it after scaffolding and after every `x g route`.
|
|
274
|
+
|
|
275
|
+
A read that should not hold up the page goes in a `<Hole>` on a `stream` page ([Routes and render modes](Routes-And-Render-Modes#what-a-hole-does)).
|
|
276
|
+
|
|
277
|
+
## The five commands worth memorising
|
|
278
|
+
|
|
279
|
+
| Want | Command |
|
|
280
|
+
|---|---|
|
|
281
|
+
| is it shippable | `bun run verify` (add `--json`) |
|
|
282
|
+
| what is wrong with my environment | `bunx x doctor --json` |
|
|
283
|
+
| what does this error code mean | `bunx x errors explain X_FORBIDDEN` |
|
|
284
|
+
| what does this app contain | `bunx x manifest --json` |
|
|
285
|
+
| what commands exist | `bunx x help` — planned ones are marked `(planned)` and throw `X_NOT_IMPLEMENTED` |
|
|
286
|
+
|
|
287
|
+
## Next
|
|
288
|
+
|
|
289
|
+
[Tutorial 2 — your first feature](Tutorial-02-First-Feature): one `entity`, one `policy`, one `action`, one `route`, one test, and the five artifacts a single declaration projects.
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
# Tutorial 2 — your first feature
|
|
2
|
+
|
|
3
|
+
Scaffold the slice, register it, migrate it, name the tests. One `action` declaration becomes an HTTP route, an OpenAPI operation, a typed client method, a job handle, an MCP tool and three contract assertions — with no second file to keep in step.
|
|
4
|
+
|
|
5
|
+
`As of 2026-11-03`. Every command and every output on this page was re-run in the app [tutorial 1](Tutorial-01-First-App) leaves — `bunx create-ultimate myapp`, example slice included, built from this tree's packages (`scripts/scaffold-smoke-overrides.ts`) — by hand, in that order. `scripts/tutorial-run.ts` runs tutorials 1–4 the same way. Timings are that machine's; yours differ.
|
|
6
|
+
|
|
7
|
+
Series: [1 — first app](Tutorial-01-First-App) · **2** · [3 — auth and admin](Tutorial-03-Auth-And-Admin) · [4 — jobs and realtime](Tutorial-04-Jobs-And-Realtime) · [5 — deploy free](Tutorial-05-Deploy-Free) · [6 — growing up](Tutorial-06-Growing-Up)
|
|
8
|
+
|
|
9
|
+
## Never hand-write a primitive
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bunx x g resource todo --dry-run
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
+ apps/web/app/todo/entity.ts
|
|
17
|
+
+ apps/web/app/todo/entity.test.ts
|
|
18
|
+
+ apps/web/app/todo/repo.ts
|
|
19
|
+
+ apps/web/app/todo/repo.test.ts
|
|
20
|
+
+ apps/web/app/todo/policy.ts
|
|
21
|
+
+ apps/web/app/todo/policy.test.ts
|
|
22
|
+
+ apps/web/app/todo/actions/create-todo.ts
|
|
23
|
+
+ apps/web/app/todo/actions/create-todo.test.ts
|
|
24
|
+
+ apps/web/app/todo/actions/create-todo.contract.test.ts
|
|
25
|
+
+ apps/web/app/todo/errors.ts
|
|
26
|
+
+ apps/web/app/todo/actions/archive-todo.ts
|
|
27
|
+
+ apps/web/app/todo/actions/archive-todo.test.ts
|
|
28
|
+
+ apps/web/app/todo/actions/archive-todo.contract.test.ts
|
|
29
|
+
+ packages/i18n/catalogs/en.json
|
|
30
|
+
+ apps/web/app/todo/live/todo-list.ts
|
|
31
|
+
+ apps/web/app/todo/live/todo-list.live.test.ts
|
|
32
|
+
+ apps/web/app/todo/live/todo-list.test.ts
|
|
33
|
+
+ apps/web/app/todo/jobs/reindex-todo.ts
|
|
34
|
+
+ apps/web/app/todo/jobs/reindex-todo.job.test.ts
|
|
35
|
+
+ apps/web/app/todo/jobs/reindex-todo.test.ts
|
|
36
|
+
+ apps/web/app/todo/service.ts
|
|
37
|
+
+ apps/web/app/todo/service.test.ts
|
|
38
|
+
+ apps/web/app/todo/ui.tsx
|
|
39
|
+
+ apps/web/app/todo/ui.test.ts
|
|
40
|
+
+ apps/web/app/todo/ui.module.scss
|
|
41
|
+
+ apps/web/app/todo/ui/todo-card.tsx
|
|
42
|
+
+ apps/web/app/todo/todo-form.island.tsx
|
|
43
|
+
+ apps/web/app/todo/todo-form.island.states.ts
|
|
44
|
+
+ apps/web/app/todo/todo-form.island.test.ts
|
|
45
|
+
+ apps/web/app/todos/page.module.scss
|
|
46
|
+
+ apps/web/app/todos/page.e2e.test.ts
|
|
47
|
+
+ apps/web/app/todos/page.tsx
|
|
48
|
+
+ apps/web/app/todos/page.test.ts
|
|
49
|
+
+ apps/web/app/todos/page-db.test.ts
|
|
50
|
+
+ apps/web/shared/roles.ts
|
|
51
|
+
+ apps/web/api/index.ts
|
|
52
|
+
+ packages/db/src/client.ts
|
|
53
|
+
✓ would write 37 file(s) for resource todo — nothing written
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**The form is an island, not a `ui/` component** — `todo-form.island.tsx` beside the slice rather than under `ui/`. A `createSignal` needs the browser, `.island.tsx` is what puts a module in the client bundle graph, and the route's `budget.js` is what bounds it. Everything under `ui/` renders on the server and ships no JS.
|
|
57
|
+
|
|
58
|
+
Drop `--dry-run` to write them:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
bunx x g resource todo
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```text
|
|
65
|
+
✓ wrote 39 file(s) for resource todo — next: …
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`x g` never clobbers — an existing file is `X_GENERATE_CONFLICT`; the i18n catalog is merged key-by-key rather than overwritten; and a **slice module** (`entity.ts`, `repo.ts`, `policy.ts`, `errors.ts`) the slice already has is skipped, `--force` included, because it belongs to the slice rather than to the generator that needed it. The last three lines are the wiring: the grants in `roles.ts`, the slice's actions, query and job in `api/index.ts`, and its entity in the db client (`packages/db/src/client.ts`). A file the run would leave unchanged — a catalog merge that gains no key, wiring a previous slice already wrote — is skipped rather than counted. Here `bun run setup` has written `x.manifest.json` and `openapi.json` ([tutorial 1](Tutorial-01-First-App)), so the run re-projects both and prints 39; in an app with neither it prints 37. In a `x new --no-example` app, whose two `package.json`s do not yet name the workspace packages, the same runs print 39 and 41 (42 with `--admin`).
|
|
69
|
+
|
|
70
|
+
**No migration is in that list.** `x db gen` is the only writer of `packages/db/migrations`, so a new slice is `x g resource todo` and then the two steps below.
|
|
71
|
+
|
|
72
|
+
| Generator | Emits |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `x g resource <n>` | the whole slice above — 37 files on a dry run in tutorial 1's app, 40 with `--admin` (`As of 2026-11-04`); a real run after `bun run setup` adds the two re-projected artifacts |
|
|
75
|
+
| `x g entity` / `policy` / `action` / `mutator` / `query` / `job` / `task` | that primitive plus its test — **and the slice modules its own source imports**, when the slice has none: `x g job` is 5 files into a bare slice, `x g action` 8. Which ones differ per generator, so a job plants no `policy.ts` ([CLI reference § x g](CLI-Reference)) |
|
|
76
|
+
| `x g route <path> --surface site\|app` | `page.tsx`, its stylesheet, its test, its catalog keys |
|
|
77
|
+
|
|
78
|
+
`--surface site` on a `resource` is refused: a slice ships a live query and a form with a signal, and `site/` is the never-hydrated surface. Full flag table: [CLI reference § x g](CLI-Reference).
|
|
79
|
+
|
|
80
|
+
## The layer rule
|
|
81
|
+
|
|
82
|
+
The slice enforces one call direction, and `x verify`'s `boundaries` step is what makes it real.
|
|
83
|
+
|
|
84
|
+
| Layer | File | May call |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| route | `apps/web/app/todos/page.tsx` | actions, queries |
|
|
87
|
+
| action / query | `actions/*.ts`, `live/*.ts` | services |
|
|
88
|
+
| service | `service.ts` | the repo |
|
|
89
|
+
| repo | `repo.ts` | `db()` — the **only** module that may touch the table |
|
|
90
|
+
|
|
91
|
+
## The entity
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
export const todo = entity('todos', {
|
|
95
|
+
// Naming the tenant column is what turns tenancy on: a read with no org predicate then fails
|
|
96
|
+
// with X_TENANCY_UNSCOPED instead of leaking another org's rows.
|
|
97
|
+
tenant: 'orgId',
|
|
98
|
+
columns: {
|
|
99
|
+
id: uuid().primaryKey(),
|
|
100
|
+
orgId: uuid(),
|
|
101
|
+
title: text({ max: 200 }),
|
|
102
|
+
price: money(), // two physical columns: price_minor bigint + price_currency char(3)
|
|
103
|
+
createdAt: timestamp().defaultNow(),
|
|
104
|
+
},
|
|
105
|
+
invariants: (c) => [
|
|
106
|
+
invariant('todo_title_not_blank', c.title.trimmed().minLength(1)),
|
|
107
|
+
invariant('todo_price_non_negative', c.price.minor.atLeast(0)),
|
|
108
|
+
],
|
|
109
|
+
indexes: [{ on: ['orgId', 'createdAt'] }],
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`c` is typed from the `columns` above it, so `c.titel` is a compile error naming `title` — see [tutorial 1](Tutorial-01-First-App#the-invariant-block-is-typed-from-your-columns).
|
|
114
|
+
|
|
115
|
+
Each invariant runs twice from one declaration — in the app on every write, and as a Postgres `CHECK` in the migration. Details: [Entities and migrations](Entities-And-Migrations).
|
|
116
|
+
|
|
117
|
+
## The policy decides once, everywhere
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
export const canTodoWrite = can<TodoScope>(
|
|
121
|
+
'todo:write',
|
|
122
|
+
({ actor, input }) => actor !== null && actor.orgId === input.orgId,
|
|
123
|
+
);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`can()` checks the grant first and the predicate second, so a denial distinguishes *you may never do this* from *you may, but not in that org*. The generated `policy.test.ts` pins the second gate with an actor who **holds** the grant and is still denied — delete the predicate and that test fails.
|
|
127
|
+
|
|
128
|
+
`canTodoCreate` is the same grant with the one tenancy rule a create can have — the actor **has** an org — because the row is written under the actor's org, never one named in the body.
|
|
129
|
+
|
|
130
|
+
The generator also **grants** what it declares, in `apps/web/shared/roles.ts`: `todo:read` to `member`, `todo:write` to `admin` (the dev actor's role). A permission an action requires and no role grants is `X_PERMISSION_UNGRANTED` on `x verify`'s `policy` step — every request to it would answer 403.
|
|
131
|
+
|
|
132
|
+
Full model: [Policies and authz](Policies-And-Authz).
|
|
133
|
+
|
|
134
|
+
## The action, and what it projects
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
export const CreateTodoInput = todo.$view(['title', 'price']);
|
|
138
|
+
|
|
139
|
+
export const createTodo = action({
|
|
140
|
+
input: CreateTodoInput,
|
|
141
|
+
output: TodoView,
|
|
142
|
+
policy: canTodoCreate,
|
|
143
|
+
cache: { invalidates: [todoTag] },
|
|
144
|
+
// No `mcp` until you write a real description: an agent reads it to decide to call the tool.
|
|
145
|
+
async handle({ input, ctx }) { … return service.create({ ...input, orgId }); },
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
A real insert. The input is the entity's own view of the columns a caller supplies, so a column added to `entity.ts` is one edit to that list; `orgId` comes from `ctx.actor`, never from the body.
|
|
150
|
+
|
|
151
|
+
<!-- no-run: needs `bunx x dev` serving on :3000 -->
|
|
152
|
+
```bash
|
|
153
|
+
curl -X POST localhost:3000/api/todos/create -H 'content-type: application/json' \
|
|
154
|
+
-H 'sec-fetch-site: same-origin' -d '{"title":"Ship it","price":{"minor":1250,"currency":"USD"}}'
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Five artifacts, read off the real registry with `createTodo.describe()`, `.openapi()`, `.job()`, `.contract()` and `@ultimat3/mcp`'s `toolFrom(createTodo)`:
|
|
158
|
+
|
|
159
|
+
| Projection | Value, verbatim |
|
|
160
|
+
|---|---|
|
|
161
|
+
| HTTP route | `POST /api/todos/create`, capability `todo:write` |
|
|
162
|
+
| OpenAPI operation | `operationId: "createTodo"`, `summary` from `mcp.description` |
|
|
163
|
+
| MCP tool | none until `mcp: { expose: true, description }` is written; then `createTodo` — the export name verbatim — and every call goes through `createTodo`'s own `invoke`, so its policy is the one authz object, not a copy |
|
|
164
|
+
| Job handle | `action:createTodo` — the same handler, run through the queue |
|
|
165
|
+
| Contract tests | 3 generated assertions: garbage input rejected, anonymous denied, operation present in the spec |
|
|
166
|
+
| Typed client | `.client({ baseUrl })` derives the path by string math, so the browser imports no server code |
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
bunx x actions list
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
```text
|
|
173
|
+
name verb resource path capability mcp
|
|
174
|
+
archivePost archive posts /api/posts/archive post:write no
|
|
175
|
+
archiveTodo archive todos /api/todos/archive todo:write no
|
|
176
|
+
createPost create posts /api/posts/create post:write no
|
|
177
|
+
createTodo create todos /api/todos/create todo:write no
|
|
178
|
+
health invoke healths /api/healths/invoke public yes
|
|
179
|
+
✓ 5 actions
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`archivePost` and `createPost` are the example `post` slice tutorial 1's app ships with. Rename a column in the entity and every consumer fails typecheck. One rename, N errors, all real work. Every field: [Actions](Actions).
|
|
183
|
+
|
|
184
|
+
## Registered for you
|
|
185
|
+
|
|
186
|
+
`x new` writes `apps/web/api/index.ts`, and every `x g` adds the primitives it wrote to it. A job no `defineApi` lists keeps the positional name `job()` gave it — `anonymous-job-2` — and `x verify`'s `manifest` step refuses that as `X_JOB_UNREGISTERED`. After `x g resource todo`:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
// apps/web/api/index.ts, in an app made with `x new --no-example` — importing it IS the boot
|
|
190
|
+
import { defineApi } from '@ultimat3/action';
|
|
191
|
+
import * as archiveTodo from '../app/todo/actions/archive-todo';
|
|
192
|
+
import * as createTodo from '../app/todo/actions/create-todo';
|
|
193
|
+
import * as reindexTodo from '../app/todo/jobs/reindex-todo';
|
|
194
|
+
import * as todoList from '../app/todo/live/todo-list';
|
|
195
|
+
import * as health from './health';
|
|
196
|
+
|
|
197
|
+
export const api = defineApi({
|
|
198
|
+
actions: [health, createTodo, archiveTodo],
|
|
199
|
+
queries: [todoList],
|
|
200
|
+
jobs: [reindexTodo],
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Tutorial 1's app lists its example `post` slice the same way, ahead of the `todo` lines. The jobs are the half an app cannot skip: the module scan registers actions and queries by export name on its own, and registers no job. Actions and queries are listed for the type — `Api` is what the browser's typed client is shaped from. Each primitive is imported as a **file**, never a directory. Two features exporting one name collide with `X_ACTION_DUPLICATE` rather than merging in silence.
|
|
205
|
+
|
|
206
|
+
## Migrations
|
|
207
|
+
|
|
208
|
+
`x g resource` writes no migration and does not touch the entity export list. Two steps.
|
|
209
|
+
|
|
210
|
+
**1. Export the entity.** `packages/db/src/schema.ts` is the db package's public surface — what the
|
|
211
|
+
rest of the app imports a `todo` from:
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
export { todo } from '@myapp/web/app/todo/entity';
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
It is **not** what the migration generator reads. `x db gen` diffs the entity **registry**
|
|
218
|
+
(`describeEntities()`), and `x verify`'s `drift` step hashes that same registry alongside the text
|
|
219
|
+
under `packages/db/src/**` — so the entity is already in the diff the moment its own file exists.
|
|
220
|
+
`loadApp` imports every module under `apps/*/{site,app,api,shared}/**` and `packages/*/src/**`, and
|
|
221
|
+
`entity()` registers on import. `examples/dummy` is the proof: it has entities, migrations and a
|
|
222
|
+
green `drift` step, and no `packages/db/src/schema.ts` at all.
|
|
223
|
+
Adding the export line moves the text half of the hash too, which is why the drift below is the same
|
|
224
|
+
either way. `bunx x verify --only drift`:
|
|
225
|
+
|
|
226
|
+
```text
|
|
227
|
+
✗ drift 2161ms
|
|
228
|
+
X_DB_SCHEMA_UNMIGRATED (packages/db/migrations)
|
|
229
|
+
cause: table "todos" is declared by the entities and no migration recorded it
|
|
230
|
+
fix: bunx x db gen "record todos"
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
The newest migration is tutorial 1's `initial`, which recorded the example `posts` table; `todos` is
|
|
234
|
+
in no migration yet. The slug is yours to choose; the fix only suggests one.
|
|
235
|
+
|
|
236
|
+
**2. Generate the migration.** `x db gen` diffs the entity registry against the newest
|
|
237
|
+
migration's snapshot and writes three files — `<id>.sql`, `<id>.snapshot.json` and `<id>.hash` —
|
|
238
|
+
and refreshes the schema dump under `packages/db/schema/` (`todos.sql` under `04_tables/` and
|
|
239
|
+
`05_indexes/`). A migration id is `<stamp>_<slug>`; the stamp is `x db gen`'s own clock:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
bunx x db gen "add todos"
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
```text
|
|
246
|
+
✓ migration …_add_todos generated
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The 1.1.0 workaround this step used to carry (a hand-written `db-gen.ts` script, and two manual
|
|
250
|
+
edits to the emitted SQL) is gone: all three defects it worked around were fixed in 2.0.0, and
|
|
251
|
+
`scaffold-smoke` in CI runs `x db gen` then `x db migrate` on a fresh scaffold on every push.
|
|
252
|
+
|
|
253
|
+
Apply them the way production does — same code path, no toolchain:
|
|
254
|
+
|
|
255
|
+
<!-- no-run: the release-phase path; the x db migrate fence below applies the same migrations -->
|
|
256
|
+
```bash
|
|
257
|
+
ROLE=migrate bun apps/web/server.ts
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
```text
|
|
261
|
+
{"ts":"2026-11-03T15:52:26.924Z","level":"info","msg":"ultimate migrate applied","applied":1,"available":1,"appVersion":"dev"}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
`bunx x db migrate` is that same code path with a drift check after it. Both read
|
|
265
|
+
`packages/db/migrations` and write one `x_migrations` ledger:
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
bunx x db migrate
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
```text
|
|
272
|
+
✓ migrations applied
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The `.hash` sidecar beside each migration is what `drift` compares against, so a fresh clone detects drift with no database and no local state.
|
|
276
|
+
|
|
277
|
+
## Name the tests after their step
|
|
278
|
+
|
|
279
|
+
A test's type is its **filename suffix**, not the helper it calls. `contractTest()` inside `create-todo.test.ts` runs under `unit`, and `x test contract` reports `X_TEST_NO_FILES`. `x g` already writes the typed names — `create-todo.contract.test.ts`, `todo-list.live.test.ts`, `reindex-todo.job.test.ts`, `page.e2e.test.ts` in the list above — so this is the rule for a test you add by hand:
|
|
280
|
+
|
|
281
|
+
| Suffix | Runs in step |
|
|
282
|
+
|---|---|
|
|
283
|
+
| `*.contract.test.ts` | `contract` |
|
|
284
|
+
| `*.live.test.ts` | `live` |
|
|
285
|
+
| `*.job.test.ts` | `job` |
|
|
286
|
+
| `*.e2e.test.ts`, anything under `e2e/` | `e2e` |
|
|
287
|
+
| `*.eval.test.ts` | `eval` |
|
|
288
|
+
|
|
289
|
+
## The gate, green
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
bunx x verify
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Timings, worker counts and the `slowest:` lines trimmed:
|
|
296
|
+
|
|
297
|
+
```text
|
|
298
|
+
✓ typecheck
|
|
299
|
+
✓ lint
|
|
300
|
+
✓ boundaries
|
|
301
|
+
✓ filesize
|
|
302
|
+
✓ package-shape
|
|
303
|
+
✓ errors
|
|
304
|
+
✓ unit
|
|
305
|
+
✓ contract
|
|
306
|
+
✓ live
|
|
307
|
+
✓ job
|
|
308
|
+
✓ exercise
|
|
309
|
+
✓ e2e
|
|
310
|
+
✓ eval
|
|
311
|
+
✓ drift
|
|
312
|
+
✓ contract-diff
|
|
313
|
+
✓ budgets
|
|
314
|
+
✓ seo
|
|
315
|
+
✓ i18n
|
|
316
|
+
✓ policy
|
|
317
|
+
✓ manifest
|
|
318
|
+
- roadmap
|
|
319
|
+
✓ 20 of 21 steps passed in …ms — 1 skipped: roadmap
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
`live`, `job` and `e2e` were dashes in [tutorial 1](Tutorial-01-First-App); the slice's own typed tests are what they now run. `budgets` builds the site first when no current measurement exists, so it is green on the first run. `e2e` drives a real Chrome; on a machine without one its browser cases skip (`E2E_BROWSER_REQUIRED=1` makes that a refusal). `roadmap` reads `docs/idea/14-roadmap.md`, which an app does not have.
|
|
323
|
+
|
|
324
|
+
## Drive it from an agent
|
|
325
|
+
|
|
326
|
+
<!-- no-run: a server that runs until stopped -->
|
|
327
|
+
```bash
|
|
328
|
+
bunx x mcp serve --transport http --port 9229
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
19 tools, one catalog, the same on `stdio` and `http` — 14 framework tools (`routes.list`, `schema.describe`, `policies.list`, `actions.describe`, `jobs.inspect`, `queue.depth`, `manifest.read`, `errors.explain`, `db.query`, `db.migrate`, `tests.run`, `verify.run`, `logs.tail`, `graph.query`) and five that look at the UI (`ui.shot`, `ui.island`, `ui.inspect`, `ui.interact`, `ui.diff`). `bunx x mcp tools` prints them with their scopes.
|
|
332
|
+
|
|
333
|
+
`createTodo` reaches an agent's tool list through the app's own surface in `packages/mcp/src/index.ts` (`defineAppMcp({ include: 'exposed' })`), carrying `mcp: { expose: true }` and **the action's own policy** as its authorization. Full model: [MCP and AI](MCP-And-AI).
|
|
334
|
+
|
|
335
|
+
## Next
|
|
336
|
+
|
|
337
|
+
[Tutorial 3 — auth and admin](Tutorial-03-Auth-And-Admin): who the actor is, which roles grant which permissions, and the admin surface over the actions you just declared.
|
|
338
|
+
|
|
339
|
+
Related: [Actions](Actions) · [Entities and migrations](Entities-And-Migrations) · [Testing](Testing) · [Known gaps](Known-Gaps)
|