@ultimat3/cli 29.4.0 → 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 +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 +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,274 @@
|
|
|
1
|
+
// `x upgrade`: one major per run. Refuse a dirty tree, write exact pins and the Bun floor, install,
|
|
2
|
+
// then let the CLI just installed run the gate. It never commits: git is the state between runs, so
|
|
3
|
+
// a hop is one reviewable commit. No `await import()` anywhere below — `bun install` replaces this
|
|
4
|
+
// very file under the running process when the app's own CLI is the one that was run.
|
|
5
|
+
|
|
6
|
+
// why: Bun exposes no path-join primitive; the git root and the app root are joined with paths.
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { UltimateError } from '@ultimat3/core';
|
|
9
|
+
import { versionAtLeast } from './app-root';
|
|
10
|
+
import { upgradeSpec } from './cmd-upgrade-spec';
|
|
11
|
+
import type { CliCommand, CommandContext } from './command';
|
|
12
|
+
import { failed, ok } from './command';
|
|
13
|
+
import { BadFlagError, BunVersionError } from './errors';
|
|
14
|
+
import type { Runner } from './exec';
|
|
15
|
+
import { execOutput } from './exec';
|
|
16
|
+
import { msg } from './messages';
|
|
17
|
+
import type { CommandResult } from './output';
|
|
18
|
+
import { flagBool, flagString } from './parse';
|
|
19
|
+
import { posixRelative } from './posix-path';
|
|
20
|
+
import { applyAgentsLink, planAgentsLink } from './upgrade-agents-link';
|
|
21
|
+
import type { BunEdit, BunManual } from './upgrade-bun-floor';
|
|
22
|
+
import { editBunSites, floorOf, isBunSite } from './upgrade-bun-floor';
|
|
23
|
+
import { repoScripts, runHopGate, runRepoScripts } from './upgrade-gate';
|
|
24
|
+
import { readManifests, readPins, rewritePins } from './upgrade-pins';
|
|
25
|
+
import { planHop, releaseOf } from './upgrade-plan';
|
|
26
|
+
import { publishedVersions, releaseBunRange } from './upgrade-registry';
|
|
27
|
+
import type { PlannedWrites } from './upgrade-report';
|
|
28
|
+
import { humanLines, NO_WRITES, report, thenLine } from './upgrade-report';
|
|
29
|
+
import { findUpgradeRoot } from './upgrade-root';
|
|
30
|
+
import { elapsedLabel, HEARTBEAT_MS } from './verify-progress';
|
|
31
|
+
import { writeErrorLine } from './write-line';
|
|
32
|
+
|
|
33
|
+
const readTarget = (ctx: CommandContext): string | undefined => {
|
|
34
|
+
const to = flagString(ctx.args, 'to');
|
|
35
|
+
if (to === undefined || releaseOf(to) !== undefined) return to;
|
|
36
|
+
throw new BadFlagError({
|
|
37
|
+
flag: 'to',
|
|
38
|
+
command: 'upgrade',
|
|
39
|
+
reason: `"${to}" is not an exact release, major.minor.patch`,
|
|
40
|
+
fix: 'bun pm view @ultimat3/cli versions --json # then: x upgrade --to <one of them> --json',
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** The repository's root, or the refusal: git is the only record of where the walk stands. */
|
|
45
|
+
async function requireCleanTree(runner: Runner, root: string): Promise<string> {
|
|
46
|
+
const top = await runner(['git', 'rev-parse', '--show-toplevel'], { cwd: root });
|
|
47
|
+
if (!top.ok) {
|
|
48
|
+
throw new UltimateError({
|
|
49
|
+
code: 'X_UPGRADE_DIRTY_TREE',
|
|
50
|
+
cause: `${root} is not a git repository, and a hop has to be one commit you can review or undo`,
|
|
51
|
+
fix: 'git init && git add -A && git commit -m "before x upgrade" # then: x upgrade --json',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const status = await runner(['git', 'status', '--porcelain'], { cwd: root });
|
|
55
|
+
const changes = status.stdout.split('\n').filter((line) => line.trim().length > 0);
|
|
56
|
+
if (!status.ok || changes.length > 0) {
|
|
57
|
+
throw new UltimateError({
|
|
58
|
+
code: 'X_UPGRADE_DIRTY_TREE',
|
|
59
|
+
cause: `${String(changes.length)} uncommitted change(s), and a hop has to be one reviewable commit: ${changes.slice(0, 5).join('; ')}`,
|
|
60
|
+
fix: 'git status --short # then commit or stash, and re-run: x upgrade --json',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return top.stdout.trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Everything the hop would write, decided before any of it is. `--dry-run` stops here. */
|
|
67
|
+
async function planWrites(input: {
|
|
68
|
+
readonly runner: Runner;
|
|
69
|
+
readonly root: string;
|
|
70
|
+
readonly top: string;
|
|
71
|
+
readonly to: string;
|
|
72
|
+
readonly floor: string | undefined;
|
|
73
|
+
readonly manifests: readonly { readonly file: string; readonly text: string }[];
|
|
74
|
+
}): Promise<PlannedWrites> {
|
|
75
|
+
const { runner, root, top, to, floor, manifests } = input;
|
|
76
|
+
const files = new Map<string, string>();
|
|
77
|
+
for (const { file, text } of manifests) {
|
|
78
|
+
const next = rewritePins(text, to);
|
|
79
|
+
if (next !== text) files.set(join(root, file), next);
|
|
80
|
+
}
|
|
81
|
+
if (floor === undefined) return { files, edited: [], manual: [] };
|
|
82
|
+
|
|
83
|
+
const tracked = await runner(['git', 'ls-files'], { cwd: top });
|
|
84
|
+
const edited: BunEdit[] = [];
|
|
85
|
+
const manual: BunManual[] = [];
|
|
86
|
+
for (const path of tracked.stdout.split('\n').filter(isBunSite)) {
|
|
87
|
+
const absolute = join(top, path);
|
|
88
|
+
// Tracked and absent is a sparse checkout: a site this tree does not hold is not this run's.
|
|
89
|
+
if (!files.has(absolute) && !(await Bun.file(absolute).exists())) continue;
|
|
90
|
+
const source = files.get(absolute) ?? (await Bun.file(absolute).text());
|
|
91
|
+
// Paths as the app reads them: a report's `path` is one its agent opens from the app root.
|
|
92
|
+
const shown = posixRelative(root, absolute);
|
|
93
|
+
const edit = editBunSites(shown, source, floor);
|
|
94
|
+
edited.push(...edit.edited);
|
|
95
|
+
manual.push(...edit.manual);
|
|
96
|
+
if (edit.text !== source) files.set(absolute, edit.text);
|
|
97
|
+
}
|
|
98
|
+
return { files, edited, manual };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** `bun install`, or the refusal carrying the one command that undoes what was just written. */
|
|
102
|
+
async function install(runner: Runner, root: string, to: string): Promise<void> {
|
|
103
|
+
const result = await runner([process.execPath, 'install'], { cwd: root });
|
|
104
|
+
if (result.ok) return;
|
|
105
|
+
throw new UltimateError({
|
|
106
|
+
code: 'X_UPGRADE_INSTALL_FAILED',
|
|
107
|
+
cause: `bun install exited ${String(result.code)} after pinning ${to}: ${execOutput(result).slice(-800) || 'no output'}`,
|
|
108
|
+
fix: 'git restore . # undoes the pins, since the tree was clean; then fix what the cause names and re-run: x upgrade --json',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const stageLine = (stage: string): string => JSON.stringify({ upgrade: stage });
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Runs `work` with a line on stderr naming the stage every `HEARTBEAT_MS`: the gate's own progress
|
|
116
|
+
* is captured with the child, so a harness that kills on silence would otherwise kill a healthy hop.
|
|
117
|
+
*/
|
|
118
|
+
async function withHeartbeat<T>(
|
|
119
|
+
write: (line: string) => void,
|
|
120
|
+
work: (stageReached: (stage: string) => void) => Promise<T>,
|
|
121
|
+
): Promise<T> {
|
|
122
|
+
let stage = 'install';
|
|
123
|
+
let began = performance.now();
|
|
124
|
+
const timer = setInterval(() => {
|
|
125
|
+
try {
|
|
126
|
+
write(
|
|
127
|
+
msg('cli.upgrade.stillRunning', {
|
|
128
|
+
stage,
|
|
129
|
+
elapsed: elapsedLabel(performance.now() - began),
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
132
|
+
} catch {
|
|
133
|
+
// Stderr closed under the hop: nowhere left to say it, and the verdict still reaches stdout.
|
|
134
|
+
}
|
|
135
|
+
}, HEARTBEAT_MS);
|
|
136
|
+
timer.unref();
|
|
137
|
+
try {
|
|
138
|
+
return await work((next) => {
|
|
139
|
+
stage = next;
|
|
140
|
+
began = performance.now();
|
|
141
|
+
write(stageLine(next));
|
|
142
|
+
});
|
|
143
|
+
} finally {
|
|
144
|
+
clearInterval(timer);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export const createUpgradeCommand = (write: (line: string) => void): CliCommand => ({
|
|
149
|
+
spec: upgradeSpec,
|
|
150
|
+
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
151
|
+
const { dir: root, app } = await findUpgradeRoot(ctx.cwd);
|
|
152
|
+
const to = readTarget(ctx);
|
|
153
|
+
const dryRun = flagBool(ctx.args, 'dry-run');
|
|
154
|
+
const top = await requireCleanTree(ctx.runner, root);
|
|
155
|
+
const manifests = await readManifests(root);
|
|
156
|
+
const pins = readPins(manifests);
|
|
157
|
+
const versions = await publishedVersions(ctx.runner, root);
|
|
158
|
+
const hop = planHop({ pins, versions, ...(to === undefined ? {} : { to }) });
|
|
159
|
+
|
|
160
|
+
if (hop.atTarget) {
|
|
161
|
+
const then = thenLine({ hop, dryRun, app, gate: undefined, scripts: [] });
|
|
162
|
+
const summary =
|
|
163
|
+
hop.next === null
|
|
164
|
+
? msg('cli.upgrade.atLatest', { version: hop.to })
|
|
165
|
+
: msg('cli.upgrade.atTarget', { version: hop.to, next: hop.next });
|
|
166
|
+
return ok('upgrade', summary, {
|
|
167
|
+
data: report({
|
|
168
|
+
hop,
|
|
169
|
+
done: true,
|
|
170
|
+
dryRun,
|
|
171
|
+
pins,
|
|
172
|
+
bunFrom: null,
|
|
173
|
+
bunTo: null,
|
|
174
|
+
writes: NO_WRITES,
|
|
175
|
+
app,
|
|
176
|
+
gate: undefined,
|
|
177
|
+
scripts: [],
|
|
178
|
+
then,
|
|
179
|
+
}),
|
|
180
|
+
lines: [msg('cli.upgrade.then', { then })],
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const bunRange = await releaseBunRange(ctx.runner, root, hop.to);
|
|
185
|
+
const floor = floorOf(bunRange);
|
|
186
|
+
// Before any write: the CLI this hop installs refuses this Bun on its first line, so the pins
|
|
187
|
+
// would land and every later step would report the same `X_BUN_VERSION` as a hop gone red.
|
|
188
|
+
if (floor !== undefined && !versionAtLeast(ctx.bunVersion, floor)) {
|
|
189
|
+
throw new BunVersionError({ found: ctx.bunVersion, required: floor });
|
|
190
|
+
}
|
|
191
|
+
const writes = await planWrites({
|
|
192
|
+
runner: ctx.runner,
|
|
193
|
+
root,
|
|
194
|
+
top,
|
|
195
|
+
to: hop.to,
|
|
196
|
+
floor,
|
|
197
|
+
manifests,
|
|
198
|
+
});
|
|
199
|
+
const held = manifests.find((manifest) => manifest.file === 'package.json')?.text ?? '';
|
|
200
|
+
const bunFrom = /"engines"\s*:\s*\{[^}]*?"bun"\s*:\s*"([^"]*)"/.exec(held)?.[1] ?? null;
|
|
201
|
+
const agentsMd = await planAgentsLink(root);
|
|
202
|
+
const shared = { hop, pins, bunFrom, bunTo: bunRange ?? null, writes, app, agentsMd };
|
|
203
|
+
const scriptNames = app ? [] : repoScripts(held);
|
|
204
|
+
|
|
205
|
+
if (dryRun) {
|
|
206
|
+
const named = scriptNames.map((name) => ({ name, ok: null }));
|
|
207
|
+
const then = thenLine({ hop, dryRun: true, app, gate: undefined, scripts: named });
|
|
208
|
+
return ok('upgrade', msg('cli.upgrade.dryRun', { from: hop.from, to: hop.to }), {
|
|
209
|
+
data: report({
|
|
210
|
+
...shared,
|
|
211
|
+
done: false,
|
|
212
|
+
dryRun: true,
|
|
213
|
+
gate: undefined,
|
|
214
|
+
scripts: named,
|
|
215
|
+
then,
|
|
216
|
+
}),
|
|
217
|
+
lines: humanLines({ hop, writes, gate: undefined, scripts: named, then, agentsMd }),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
await Promise.all(
|
|
222
|
+
[...writes.files].map(async ([path, text]) => {
|
|
223
|
+
await Bun.write(path, text);
|
|
224
|
+
}),
|
|
225
|
+
);
|
|
226
|
+
await applyAgentsLink(root, agentsMd);
|
|
227
|
+
const { gate, scripts } = await withHeartbeat(write, async (stageReached) => {
|
|
228
|
+
stageReached('install');
|
|
229
|
+
await install(ctx.runner, root, hop.to);
|
|
230
|
+
if (!app) {
|
|
231
|
+
const names = scriptNames;
|
|
232
|
+
const ran = await runRepoScripts({
|
|
233
|
+
runner: ctx.runner,
|
|
234
|
+
root,
|
|
235
|
+
names,
|
|
236
|
+
onStage: stageReached,
|
|
237
|
+
});
|
|
238
|
+
return { gate: undefined, scripts: ran };
|
|
239
|
+
}
|
|
240
|
+
const hopGate = await runHopGate({ runner: ctx.runner, root, onStage: stageReached });
|
|
241
|
+
return { gate: hopGate, scripts: [] };
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const redScripts = scripts.filter((script) => script.ok !== true).map((script) => script.name);
|
|
245
|
+
// With no script, nothing was checked, and that is never a green.
|
|
246
|
+
const done =
|
|
247
|
+
gate !== undefined ? gate.verify.ok : scripts.length > 0 && redScripts.length === 0;
|
|
248
|
+
const then = thenLine({ hop, dryRun: false, app, gate, scripts });
|
|
249
|
+
const extra = {
|
|
250
|
+
data: report({ ...shared, done, dryRun: false, gate, scripts, then }),
|
|
251
|
+
lines: humanLines({ hop, writes, gate, scripts, then, agentsMd }),
|
|
252
|
+
};
|
|
253
|
+
const span = { from: hop.from, to: hop.to };
|
|
254
|
+
if (gate !== undefined) {
|
|
255
|
+
return done
|
|
256
|
+
? ok('upgrade', msg('cli.upgrade.done', span), extra)
|
|
257
|
+
: failed(
|
|
258
|
+
'upgrade',
|
|
259
|
+
msg('cli.upgrade.red', { ...span, summary: gate.verify.summary }),
|
|
260
|
+
extra,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
if (scripts.length === 0) return failed('upgrade', msg('cli.upgrade.noApp.none', span), extra);
|
|
264
|
+
return done
|
|
265
|
+
? ok('upgrade', msg('cli.upgrade.noApp.done', { ...span, count: scripts.length }), extra)
|
|
266
|
+
: failed(
|
|
267
|
+
'upgrade',
|
|
268
|
+
msg('cli.upgrade.noApp.red', { ...span, names: redScripts.join(', ') }),
|
|
269
|
+
extra,
|
|
270
|
+
);
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
export const upgradeCommand: CliCommand = createUpgradeCommand(writeErrorLine);
|
package/src/cmd-verify-spec.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// read it without loading `cmd-verify.ts`, which `registry.ts` imports only when the command runs.
|
|
3
3
|
|
|
4
4
|
import type { CommandSpec } from './parse';
|
|
5
|
-
import { WORKER_BYTES, WORKER_CEILING, WORKER_FLOOR } from './test-workers';
|
|
5
|
+
import { BUDGET_RULE, WORKER_BYTES, WORKER_CEILING, WORKER_FLOOR } from './test-workers';
|
|
6
6
|
|
|
7
7
|
/** `run` is the gate (and what a bare `x verify` means); `merge` folds CI parts into its verdict. */
|
|
8
8
|
export const VERIFY_SUBCOMMANDS = ['run', 'merge'] as const;
|
|
@@ -24,7 +24,7 @@ export const verifySpec: CommandSpec = {
|
|
|
24
24
|
name: 'workers',
|
|
25
25
|
type: 'string',
|
|
26
26
|
subcommands: ['run'],
|
|
27
|
-
summary: `test processes per parallel step (default: one per ${WORKER_BYTES / 2 ** 30} GiB of a
|
|
27
|
+
summary: `test processes per parallel step (default: one per ${WORKER_BYTES / 2 ** 30} GiB of a ${BUDGET_RULE} budget, at most one per core; ULTIMATE_TEST_MEMORY_BUDGET and ULTIMATE_TEST_MAX_WORKERS override; min ${WORKER_FLOOR}, max ${WORKER_CEILING})`,
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
name: 'only',
|
|
@@ -38,7 +38,7 @@ export const verifySpec: CommandSpec = {
|
|
|
38
38
|
type: 'string',
|
|
39
39
|
subcommands: ['run'],
|
|
40
40
|
summary:
|
|
41
|
-
'with --only unit|contract|job: run shard i of an n-way split of each step’s sorted file list (round-robin), for one CI job; `x verify merge` checks all n',
|
|
41
|
+
'with --only unit|contract|job|live|e2e: run shard i of an n-way split of each step’s sorted file list (round-robin), for one CI job; `x verify merge` checks all n',
|
|
42
42
|
},
|
|
43
43
|
{
|
|
44
44
|
name: 'timings',
|
package/src/db-backfill.ts
CHANGED
|
@@ -271,9 +271,15 @@ export async function runBackfills(input: BackfillRunInput): Promise<readonly Ba
|
|
|
271
271
|
// mapped its rows instead, `--all` would have found zero targets, exited 0 and reported that
|
|
272
272
|
// nothing needed sweeping — the silent success this slice exists to remove, reintroduced by an
|
|
273
273
|
// identity check nobody could see from here.
|
|
274
|
+
//
|
|
275
|
+
// A scoped pass's rows are `<name>:<scope>`, and no declaration carries that name: the input its
|
|
276
|
+
// scope came from is on that run's queue row, so this command cannot enqueue it again — the
|
|
277
|
+
// retry is `x jobs retry <id>`, which `--pending` names by `last-run-id`. Kept out rather than
|
|
278
|
+
// reported as `X_BACKFILL_UNKNOWN` about a sweep that exists.
|
|
274
279
|
const targets =
|
|
275
280
|
input.names === 'all'
|
|
276
281
|
? report.rows
|
|
282
|
+
.filter((row) => byName.get(row.name)?.scoped === false)
|
|
277
283
|
.filter((row) =>
|
|
278
284
|
input.force ? row.state !== 'excluded' : isPendingBackfillState(row.state),
|
|
279
285
|
)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// `x db diagnostics`: opens the app's database the way every role does, asks `@ultimat3/db`'s
|
|
2
|
+
// `dbDiagnostics()` and renders one table per answered section. Facts, never a verdict: exit 0
|
|
3
|
+
// whatever they say, and the one failure is a connection that never opens (`X_DB_UNAVAILABLE`).
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type DbDiagnostics,
|
|
7
|
+
type DiagnosticSection,
|
|
8
|
+
dbDiagnostics,
|
|
9
|
+
} from '@ultimat3/db/diagnostics';
|
|
10
|
+
import type { CommandContext } from './command';
|
|
11
|
+
import { DevAlreadyRunningError, liveDevLock } from './dev-lock';
|
|
12
|
+
import { readIntFlag } from './flag-number';
|
|
13
|
+
import { msg } from './messages';
|
|
14
|
+
import type { CommandResult, JsonValue } from './output';
|
|
15
|
+
import { resolveServices } from './runtime-bindings';
|
|
16
|
+
import { startDb } from './runtime-queue';
|
|
17
|
+
import { renderTable } from './table';
|
|
18
|
+
|
|
19
|
+
type Cell = string | number | boolean | null;
|
|
20
|
+
|
|
21
|
+
/** A row is a flat record of scalars by construction (`db-diagnostics-rows.ts`); read it as one. */
|
|
22
|
+
const cells = (row: object): Readonly<Record<string, Cell>> =>
|
|
23
|
+
Object.fromEntries(
|
|
24
|
+
Object.entries(row).map(([key, value]) => [
|
|
25
|
+
key,
|
|
26
|
+
typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'
|
|
27
|
+
? value
|
|
28
|
+
: null,
|
|
29
|
+
]),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
/** `--json`'s `data`: the report as `dbDiagnostics` built it, re-typed for the JSON renderer. */
|
|
33
|
+
export function diagnosticsJson(report: DbDiagnostics): JsonValue {
|
|
34
|
+
return Object.fromEntries(
|
|
35
|
+
Object.entries(report).map(([name, section]: [string, DiagnosticSection<object>]) => [
|
|
36
|
+
name,
|
|
37
|
+
section.available
|
|
38
|
+
? { available: true, rows: section.rows.map((row) => ({ ...cells(row) })) }
|
|
39
|
+
: { available: false, code: section.code, reason: section.reason, fix: section.fix },
|
|
40
|
+
]),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** One heading and one table per answered section; an unavailable one is its reason and fix. */
|
|
45
|
+
export function renderDiagnostics(report: DbDiagnostics): readonly string[] {
|
|
46
|
+
return Object.entries(report).flatMap(([name, section]: [string, DiagnosticSection<object>]) => {
|
|
47
|
+
if (!section.available) {
|
|
48
|
+
const { code, reason, fix } = section;
|
|
49
|
+
return [msg('cli.db.diagnostics.unavailable', { section: name, code, reason, fix }), ''];
|
|
50
|
+
}
|
|
51
|
+
const rows = section.rows.map(cells);
|
|
52
|
+
const header = Object.keys(rows[0] ?? {});
|
|
53
|
+
const table = renderTable(
|
|
54
|
+
header,
|
|
55
|
+
rows.map((row) => header.map((key) => String(row[key] ?? ''))),
|
|
56
|
+
);
|
|
57
|
+
const body = rows.length === 0 ? [] : table.map((line) => ` ${line}`);
|
|
58
|
+
return [msg('cli.db.diagnostics.section', { section: name, count: rows.length }), ...body, ''];
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* `startDb`, not `startQueue`: the queue's boot applies the framework tables, and a diagnostic
|
|
64
|
+
* must write nothing. An embedded database `x dev` holds is refused rather than opened twice:
|
|
65
|
+
* PGlite's data directory has one writer.
|
|
66
|
+
*/
|
|
67
|
+
export async function runDiagnostics(ctx: CommandContext, root: string): Promise<CommandResult> {
|
|
68
|
+
const limit = readIntFlag(ctx.args, {
|
|
69
|
+
name: 'limit',
|
|
70
|
+
command: 'db diagnostics',
|
|
71
|
+
min: 1,
|
|
72
|
+
max: 1_000,
|
|
73
|
+
example: 'x db diagnostics --limit 20 --json',
|
|
74
|
+
});
|
|
75
|
+
const services = resolveServices(root, ctx.env);
|
|
76
|
+
if (services.db.mode === 'embedded') {
|
|
77
|
+
// Check-then-open, as `x db reset` does: an `x dev` starting between this read and `startDb`
|
|
78
|
+
// below is not seen. Narrow, and the same window every embedded `x db` subcommand has.
|
|
79
|
+
const holder = liveDevLock(services.stateDir);
|
|
80
|
+
if (holder !== undefined) {
|
|
81
|
+
throw new DevAlreadyRunningError({
|
|
82
|
+
lock: holder,
|
|
83
|
+
stateDir: services.stateDir,
|
|
84
|
+
embeddedDb: true,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const started = startDb(services, ctx.env);
|
|
89
|
+
try {
|
|
90
|
+
const report = await dbDiagnostics({ client: started.client, limit });
|
|
91
|
+
const sections = Object.values(report);
|
|
92
|
+
return {
|
|
93
|
+
ok: true,
|
|
94
|
+
command: 'db',
|
|
95
|
+
summary: msg('cli.db.diagnostics.done', {
|
|
96
|
+
available: sections.filter((section) => section.available).length,
|
|
97
|
+
total: sections.length,
|
|
98
|
+
}),
|
|
99
|
+
lines: renderDiagnostics(report),
|
|
100
|
+
data: diagnosticsJson(report),
|
|
101
|
+
};
|
|
102
|
+
} finally {
|
|
103
|
+
await started.client.close();
|
|
104
|
+
await started.replica?.close();
|
|
105
|
+
}
|
|
106
|
+
}
|
package/src/db-generate.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
declaredSchema,
|
|
11
11
|
generateMigration,
|
|
12
12
|
migrationSnapshotMissing,
|
|
13
|
+
ROLLING_UNSAFE_MARKER,
|
|
13
14
|
snapshotJson,
|
|
14
15
|
} from '@ultimat3/db';
|
|
15
16
|
import { describeEntities } from '@ultimat3/entity';
|
|
@@ -19,11 +20,16 @@ import { replicaIdentityTables } from './db-subscribes';
|
|
|
19
20
|
import { reconcileSchemaHash, writeSchemaHash } from './drift';
|
|
20
21
|
import { hashFileName, MIGRATIONS_DIR, readMigrations, snapshotFileName } from './migrations';
|
|
21
22
|
import type { Finding } from './output';
|
|
23
|
+
import { appRowSecurity, type RowSecurityWanted } from './row-security-wanted';
|
|
22
24
|
|
|
23
25
|
export interface GenerateMigrationOptions {
|
|
24
26
|
readonly name: string;
|
|
25
27
|
/** `x db gen "…" --allow-destructive`. A DROP whose `down` cannot restore the rows. */
|
|
26
28
|
readonly allowDestructive?: boolean | undefined;
|
|
29
|
+
/** `x db gen "…" --rolling-unsafe`. A drop never retired, for an app that stops before it starts. */
|
|
30
|
+
readonly rollingUnsafe?: boolean | undefined;
|
|
31
|
+
/** `database.rowSecurity` and the tenant columns. The app's own unless a test injects one. */
|
|
32
|
+
readonly rowSecurity?: RowSecurityWanted | undefined;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
/**
|
|
@@ -68,6 +74,7 @@ export function migrationSql(migration: GeneratedMigration): string {
|
|
|
68
74
|
"-- GENERATED by `x db gen` from the app's entities — do not edit.",
|
|
69
75
|
'-- Editing an applied migration changes its checksum: X_MIGRATION_CONFLICT on the next apply.',
|
|
70
76
|
...(migration.destructive ? [DESTRUCTIVE_MARKER] : []),
|
|
77
|
+
...(migration.rollingUnsafe === true ? [ROLLING_UNSAFE_MARKER] : []),
|
|
71
78
|
'',
|
|
72
79
|
migration.up,
|
|
73
80
|
'',
|
|
@@ -124,7 +131,9 @@ export async function generateAppMigration(
|
|
|
124
131
|
current,
|
|
125
132
|
name: options.name,
|
|
126
133
|
replicaIdentityFull,
|
|
134
|
+
rowSecurity: await (options.rowSecurity ?? appRowSecurity)(root),
|
|
127
135
|
...(options.allowDestructive === true ? { allowDestructive: true } : {}),
|
|
136
|
+
...(options.rollingUnsafe === true ? { rollingUnsafe: true } : {}),
|
|
128
137
|
});
|
|
129
138
|
// An empty diff writes no MIGRATION — one with no statement still takes a ledger row, a checksum
|
|
130
139
|
// and a place in the apply order, a permanent record that nothing changed. It re-records the
|
package/src/db-reset-template.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
// The migrate that follows still runs and finds every migration in the ledger — so a template
|
|
6
6
|
// that is missing something is completed by it, never trusted over it. Measured on
|
|
7
|
-
//
|
|
7
|
+
// one adopter's app (33 migrations, a loaded 12-core box, 3 runs each): `x db reset` 8.7 s → 7.5 s.
|
|
8
8
|
// What is left is the CLI's boot, the migrator's no-op pass and the schema dump's own replay (~3 s,
|
|
9
9
|
// `db-schema-refresh.ts`). The e2e step's app boots through exactly this reset, once per gate.
|
|
10
10
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// The rolling-deploy rail at the gate: an `up` old pods would trip over must have retired first, or
|
|
2
|
+
// say it stops before it starts. Files, not the database, so it fires in CI. `@ultimat3/db` owns the
|
|
3
|
+
// classifier and the markers; this file owns the walk and the bound an app sets in `x.verify.json`.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
hasRollingUnsafeMarker,
|
|
7
|
+
migrationRollingUnsafe,
|
|
8
|
+
retiredColumnsIn,
|
|
9
|
+
rollingUnsafeStatements,
|
|
10
|
+
unretiredColumnsIn,
|
|
11
|
+
} from '@ultimat3/db';
|
|
12
|
+
import { MIGRATIONS_DIR, readMigrations } from './migrations';
|
|
13
|
+
import { type Finding, findingFrom } from './output';
|
|
14
|
+
import { readVerifyFloor } from './verify-floor';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Unset is silent and that is the promise: an applied migration cannot gain a marker (checksum), so
|
|
18
|
+
* an existing app could only ever clear this by rewriting history. `x doctor` reminds it instead.
|
|
19
|
+
*
|
|
20
|
+
* Every migration is walked for its retires, the bounded ones too: a retire shipped before the bound
|
|
21
|
+
* still earns the drop after it, and one taken back earns nothing. One finding per migration, as
|
|
22
|
+
* `checkDestructiveMigrations`.
|
|
23
|
+
*/
|
|
24
|
+
export async function checkRollingMigrations(root: string): Promise<readonly Finding[]> {
|
|
25
|
+
const since = (await readVerifyFloor(root))?.rollingSince;
|
|
26
|
+
if (since === undefined) return [];
|
|
27
|
+
const findings: Finding[] = [];
|
|
28
|
+
const retired = new Set<string>();
|
|
29
|
+
for (const migration of await readMigrations(root)) {
|
|
30
|
+
if (migration.id > since && !hasRollingUnsafeMarker(migration.up)) {
|
|
31
|
+
const [first, ...rest] = rollingUnsafeStatements(migration.up, retired);
|
|
32
|
+
if (first !== undefined) {
|
|
33
|
+
const file = `${MIGRATIONS_DIR}/${migration.id}.sql`;
|
|
34
|
+
findings.push({
|
|
35
|
+
...findingFrom(migrationRollingUnsafe(file, first, rest.length)),
|
|
36
|
+
at: file,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// After the judging: a retire in the same file lands in the same deploy, so it is not earlier.
|
|
41
|
+
for (const key of retiredColumnsIn(migration.up)) retired.add(key);
|
|
42
|
+
for (const key of unretiredColumnsIn(migration.up)) retired.delete(key);
|
|
43
|
+
}
|
|
44
|
+
return findings;
|
|
45
|
+
}
|