@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
package/src/cmd-repro.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// `x repro <id>` — a production failure becomes a failing test. A dead job (its row, its steps) or
|
|
2
|
+
// a captured 5xx (`x_failures`, when `failureCapture` is on) is read, `--write` puts the replay in
|
|
3
|
+
// `repro/`, and `--verify` runs it: `{ reproduced, code, fingerprint }`. `--list` shows what is held.
|
|
4
|
+
// CLI wiring only: the reads are `repro-bundle.ts`, the sources `repro-job.ts`/`repro-request.ts`.
|
|
5
|
+
|
|
6
|
+
import { postgresErrorGroupsStore } from '@ultimat3/admin/error-groups';
|
|
7
|
+
import { postgresFailureStore } from '@ultimat3/db';
|
|
8
|
+
import { loadAppConfig } from './app-config-load';
|
|
9
|
+
import { loadApp } from './app-load';
|
|
10
|
+
import { requireAppRoot } from './app-root';
|
|
11
|
+
import { reproSpec } from './cmd-repro-spec';
|
|
12
|
+
import type { CliCommand, CommandContext } from './command';
|
|
13
|
+
import { MissingPositionalError } from './errors';
|
|
14
|
+
import { withJobDriver } from './jobs-driver';
|
|
15
|
+
import { msg } from './messages';
|
|
16
|
+
import type { CommandResult, JsonValue } from './output';
|
|
17
|
+
import { findingFrom } from './output';
|
|
18
|
+
import { flagBool, flagString } from './parse';
|
|
19
|
+
import type { ReproSubject } from './repro-bundle';
|
|
20
|
+
import { resolveSubject, subjectFromFile } from './repro-bundle';
|
|
21
|
+
import { ReproNotReproducedError } from './repro-errors';
|
|
22
|
+
import { jobFingerprint } from './repro-job';
|
|
23
|
+
import { reproFile, reproSource, verifyRepro, writeRepro } from './repro-write';
|
|
24
|
+
|
|
25
|
+
/** What every answer carries, whichever kind: the code, the group, the build and what was masked. */
|
|
26
|
+
function factsOf(subject: ReproSubject): {
|
|
27
|
+
code: string | null;
|
|
28
|
+
fingerprint: string | null;
|
|
29
|
+
build: string | null;
|
|
30
|
+
masked: readonly string[];
|
|
31
|
+
} {
|
|
32
|
+
if (subject.kind === 'job') {
|
|
33
|
+
const { failure } = subject;
|
|
34
|
+
return {
|
|
35
|
+
code: failure.code,
|
|
36
|
+
fingerprint: jobFingerprint(failure),
|
|
37
|
+
build: null,
|
|
38
|
+
masked: failure.masked,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const { bundle } = subject;
|
|
42
|
+
return {
|
|
43
|
+
code: bundle.code,
|
|
44
|
+
fingerprint: bundle.fingerprint,
|
|
45
|
+
build: bundle.build,
|
|
46
|
+
masked: bundle.masked,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const summaryOf = (subject: ReproSubject): string =>
|
|
51
|
+
subject.kind === 'job'
|
|
52
|
+
? msg('cli.repro.job', {
|
|
53
|
+
name: subject.failure.name,
|
|
54
|
+
id: subject.failure.jobId,
|
|
55
|
+
code: subject.failure.code ?? 'no code',
|
|
56
|
+
step: subject.failure.step ?? '(the body)',
|
|
57
|
+
})
|
|
58
|
+
: msg('cli.repro.request', {
|
|
59
|
+
primitive: subject.bundle.primitive,
|
|
60
|
+
name: subject.bundle.name,
|
|
61
|
+
code: subject.bundle.code,
|
|
62
|
+
fingerprint: subject.bundle.fingerprint,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
async function answer(
|
|
66
|
+
root: string,
|
|
67
|
+
ctx: CommandContext,
|
|
68
|
+
id: string,
|
|
69
|
+
subject: ReproSubject,
|
|
70
|
+
): Promise<CommandResult> {
|
|
71
|
+
const facts = factsOf(subject);
|
|
72
|
+
const payload = (subject.kind === 'job'
|
|
73
|
+
? subject.failure
|
|
74
|
+
: subject.bundle) as unknown as JsonValue;
|
|
75
|
+
const base = {
|
|
76
|
+
kind: subject.kind,
|
|
77
|
+
id,
|
|
78
|
+
code: facts.code,
|
|
79
|
+
fingerprint: facts.fingerprint,
|
|
80
|
+
build: facts.build,
|
|
81
|
+
masked: [...facts.masked],
|
|
82
|
+
[subject.kind === 'job' ? 'failure' : 'bundle']: payload,
|
|
83
|
+
};
|
|
84
|
+
const verify = flagBool(ctx.args, 'verify');
|
|
85
|
+
if (!verify && !flagBool(ctx.args, 'write')) {
|
|
86
|
+
return { ok: true, command: 'repro', summary: summaryOf(subject), data: base };
|
|
87
|
+
}
|
|
88
|
+
const file = reproFile(subject);
|
|
89
|
+
const runningBuild = ctx.env['BUILD_ID'] ?? null;
|
|
90
|
+
await writeRepro(root, file, await reproSource(root, subject, id, runningBuild), ctx.runner);
|
|
91
|
+
const lines = [msg('cli.repro.written', { file })];
|
|
92
|
+
if (!verify) {
|
|
93
|
+
return {
|
|
94
|
+
ok: true,
|
|
95
|
+
command: 'repro',
|
|
96
|
+
summary: summaryOf(subject),
|
|
97
|
+
lines,
|
|
98
|
+
data: { ...base, file },
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const verdict = await verifyRepro(root, file, facts.code, ctx.runner, ctx.env);
|
|
102
|
+
const data = { ...base, file, reproduced: verdict.reproduced, got: verdict.got };
|
|
103
|
+
if (verdict.reproduced) {
|
|
104
|
+
lines.push(msg('cli.repro.reproduced', { file, code: facts.code ?? '' }));
|
|
105
|
+
return { ok: true, command: 'repro', summary: summaryOf(subject), lines, data };
|
|
106
|
+
}
|
|
107
|
+
lines.push(msg('cli.repro.notReproduced', { file, got: verdict.got ?? 'a pass' }));
|
|
108
|
+
const refusal = new ReproNotReproducedError({
|
|
109
|
+
id,
|
|
110
|
+
expected: facts.code ?? 'its original failure',
|
|
111
|
+
got: verdict.got,
|
|
112
|
+
masked: facts.masked,
|
|
113
|
+
file,
|
|
114
|
+
build: facts.build,
|
|
115
|
+
});
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
command: 'repro',
|
|
119
|
+
summary: summaryOf(subject),
|
|
120
|
+
lines,
|
|
121
|
+
findings: [findingFrom(refusal)],
|
|
122
|
+
data,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The held bundles, each with its group's count and last sighting from `x_error_groups`. */
|
|
127
|
+
async function list(ctx: CommandContext): Promise<CommandResult> {
|
|
128
|
+
const code = flagString(ctx.args, 'code');
|
|
129
|
+
const bundles = await postgresFailureStore().list(code === undefined ? {} : { code });
|
|
130
|
+
const groups = postgresErrorGroupsStore();
|
|
131
|
+
const rows: JsonValue[] = [];
|
|
132
|
+
for (const bundle of bundles) {
|
|
133
|
+
const group = await groups.get(bundle.fingerprint).catch(() => undefined);
|
|
134
|
+
rows.push({
|
|
135
|
+
...bundle,
|
|
136
|
+
count: group?.count ?? null,
|
|
137
|
+
lastSeen: group?.lastSeen ?? null,
|
|
138
|
+
repro: `x repro ${bundle.id} --write --verify`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
ok: true,
|
|
143
|
+
command: 'repro',
|
|
144
|
+
summary: msg('cli.repro.listed', { count: rows.length }),
|
|
145
|
+
lines: bundles.map(
|
|
146
|
+
(bundle) => ` ${bundle.at} ${bundle.code.padEnd(28)} ${bundle.name} ${bundle.id}`,
|
|
147
|
+
),
|
|
148
|
+
data: { bundles: rows },
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export const reproCommand: CliCommand = {
|
|
153
|
+
spec: reproSpec,
|
|
154
|
+
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
155
|
+
const root = requireAppRoot('repro', ctx.cwd).dir;
|
|
156
|
+
const from = flagString(ctx.args, 'from');
|
|
157
|
+
const id = ctx.args.positionals[0];
|
|
158
|
+
if (from === undefined && id === undefined && !flagBool(ctx.args, 'list')) {
|
|
159
|
+
throw new MissingPositionalError({
|
|
160
|
+
command: 'repro',
|
|
161
|
+
positional: 'id',
|
|
162
|
+
example: 'x repro --list --json',
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
// The app's declarations are what mask a job's input and what a written test replays against.
|
|
166
|
+
await loadApp(root);
|
|
167
|
+
if (from !== undefined) return answer(root, ctx, id ?? from, await subjectFromFile(from));
|
|
168
|
+
const captureOn = (await loadAppConfig(root))?.failureCapture.enabled === true;
|
|
169
|
+
return withJobDriver(root, ctx, async (driver) => {
|
|
170
|
+
if (id === undefined) return list(ctx);
|
|
171
|
+
return answer(root, ctx, id, await resolveSubject(driver, id, captureOn));
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// `x stop`'s declaration, apart from its body: the parser, `x help` and the `errors` step read it
|
|
2
|
+
// without loading `cmd-stop.ts`, which `registry.ts` imports only when the command runs.
|
|
3
|
+
|
|
4
|
+
import type { CommandSpec } from './parse';
|
|
5
|
+
|
|
6
|
+
export const stopSpec: CommandSpec = {
|
|
7
|
+
name: 'stop',
|
|
8
|
+
summary:
|
|
9
|
+
"stop this checkout's x dev — the pid its .x/dev.lock names, and nothing else — and return once x dev can boot again",
|
|
10
|
+
usage: 'x stop [--timeout 30s] [--json]',
|
|
11
|
+
requiresApp: true,
|
|
12
|
+
flags: [
|
|
13
|
+
{
|
|
14
|
+
name: 'timeout',
|
|
15
|
+
type: 'string',
|
|
16
|
+
// No `default:` — the default is the drain's own deadline plus margin, read at run time.
|
|
17
|
+
summary:
|
|
18
|
+
'how long to wait for the drain: 30s, 2m, PT1M (default: the drain deadline plus the release floor plus 5s)',
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
};
|
package/src/cmd-stop.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// `x stop` — ends this checkout's `x dev`, and returns only once the next `x dev` can boot. The
|
|
2
|
+
// lock is the whole authority: the pid `.x/dev.lock` names is the only process signalled, never
|
|
3
|
+
// one found by port or by name, so agents sharing a box cannot end each other's servers.
|
|
4
|
+
|
|
5
|
+
import { drainDeadlineMs, renderThrowable, stringField, UltimateError } from '@ultimat3/core';
|
|
6
|
+
import { parseDuration } from '@ultimat3/time';
|
|
7
|
+
import { requireAppRoot } from './app-root';
|
|
8
|
+
import { stopSpec } from './cmd-stop-spec';
|
|
9
|
+
import type { CliCommand, CommandContext } from './command';
|
|
10
|
+
import { clearLock, isProcessAlive, liveDevLock, lockPath, parseLock, reusedPid } from './dev-lock';
|
|
11
|
+
import { BadFlagError } from './errors';
|
|
12
|
+
import { MIN_RELEASE_MS } from './hold';
|
|
13
|
+
import { msg } from './messages';
|
|
14
|
+
import type { CommandResult } from './output';
|
|
15
|
+
import { flagString } from './parse';
|
|
16
|
+
import { resolveServices } from './runtime-bindings';
|
|
17
|
+
|
|
18
|
+
/** How often the wait asks whether the pid has left. */
|
|
19
|
+
const POLL_MS = 50;
|
|
20
|
+
|
|
21
|
+
/** The child's own hard exit (`devHardExitMs`) plus margin, so its self-kill lands inside the wait. */
|
|
22
|
+
export const defaultStopTimeoutMs = (): number => drainDeadlineMs() + MIN_RELEASE_MS + 5_000;
|
|
23
|
+
|
|
24
|
+
/** The pid belongs to another user: SIGTERM is refused with EPERM, so nothing was stopped. */
|
|
25
|
+
export class DevStopDeniedError extends UltimateError {
|
|
26
|
+
constructor(input: { readonly pid: number }) {
|
|
27
|
+
super({
|
|
28
|
+
code: 'X_DEV_STOP_DENIED',
|
|
29
|
+
cause: `x stop may not signal pid ${String(input.pid)}, the x dev this checkout's lock names: another user owns it (EPERM)`,
|
|
30
|
+
fix: `ps -o user= -p ${String(input.pid)} # then, as that user: x stop --json`,
|
|
31
|
+
meta: { pid: input.pid },
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The pid outlived the wait. Its lock stays: the process still holds the directory. */
|
|
37
|
+
export class DevStopTimeoutError extends UltimateError {
|
|
38
|
+
constructor(input: { readonly pid: number; readonly waitedMs: number }) {
|
|
39
|
+
super({
|
|
40
|
+
code: 'X_DEV_STOP_TIMEOUT',
|
|
41
|
+
cause: `x stop sent SIGTERM to pid ${String(input.pid)}, the x dev this checkout's lock names, and it was still running ${String(input.waitedMs)}ms later — a drain past its deadline, or a process that ignores SIGTERM`,
|
|
42
|
+
// A hard kill leaves the lock behind, and `preflight` clears a lock whose pid is dead.
|
|
43
|
+
fix: `kill -9 ${String(input.pid)} # then: x dev --json`,
|
|
44
|
+
meta: { pid: input.pid, waitedMs: input.waitedMs },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type StopResult =
|
|
50
|
+
/** `stale`: the lock names a live pid that is not the process that wrote it (a reused pid). */
|
|
51
|
+
| { readonly stopped: false; readonly stale?: true }
|
|
52
|
+
| {
|
|
53
|
+
readonly stopped: true;
|
|
54
|
+
readonly pid: number;
|
|
55
|
+
readonly port: number;
|
|
56
|
+
readonly waitedMs: number;
|
|
57
|
+
/** False on Windows, where `kill` is TerminateProcess and no drain runs. */
|
|
58
|
+
readonly drained: boolean;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export interface StopInput {
|
|
62
|
+
readonly stateDir: string;
|
|
63
|
+
readonly timeoutMs: number;
|
|
64
|
+
/** Test seams: the defaults are this process's platform, `process.kill` and signal 0. */
|
|
65
|
+
readonly platform?: NodeJS.Platform;
|
|
66
|
+
readonly kill?: (pid: number) => void;
|
|
67
|
+
readonly alive?: (pid: number) => boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** SIGTERM, with the one refusal a reader can act on coded; a pid already gone is no refusal. */
|
|
71
|
+
function signal(pid: number, kill: (pid: number) => void): void {
|
|
72
|
+
try {
|
|
73
|
+
kill(pid);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
// `stringField`, never a cast: the rule `dev-lock.ts`'s `isProcessAlive` states.
|
|
76
|
+
const code = stringField(error, 'code');
|
|
77
|
+
if (code === 'EPERM') throw new DevStopDeniedError({ pid });
|
|
78
|
+
if (code !== 'ESRCH') {
|
|
79
|
+
throw new UltimateError({
|
|
80
|
+
code: 'X_CLI_UNEXPECTED',
|
|
81
|
+
cause: `x stop could not signal pid ${String(pid)}: ${renderThrowable(error)}`,
|
|
82
|
+
fix: 'x doctor --json',
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Whether the lock on disk still names `pid`: a drain removes it before the process exits. */
|
|
89
|
+
const lockNames = async (stateDir: string, pid: number): Promise<boolean> => {
|
|
90
|
+
const file = Bun.file(lockPath(stateDir));
|
|
91
|
+
if (!(await file.exists())) return false;
|
|
92
|
+
return parseLock(await file.text())?.pid === pid;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Signal the lock's holder and wait for both facts the next boot reads: the pid gone and the lock
|
|
97
|
+
* no longer naming it. A dead pid whose lock survived was terminated without a drain (Windows, or a
|
|
98
|
+
* crash mid-drain), and its lock is cleared here so `x stop` returning means `x dev` boots.
|
|
99
|
+
* No live holder is not a failure: a stale lock is left for `preflight`, which owns that clear.
|
|
100
|
+
*/
|
|
101
|
+
export async function stopDev(input: StopInput): Promise<StopResult> {
|
|
102
|
+
const alive = input.alive ?? isProcessAlive;
|
|
103
|
+
const lock = liveDevLock(input.stateDir);
|
|
104
|
+
if (lock === undefined || !alive(lock.pid)) return { stopped: false };
|
|
105
|
+
// A pid alone is not the holder: a kill -9 leaves the lock and the kernel reuses the number.
|
|
106
|
+
// Nothing is signalled and nothing cleared; the next `x dev`'s preflight clears it (`reusedPid`).
|
|
107
|
+
if (await reusedPid(lock)) return { stopped: false, stale: true };
|
|
108
|
+
const windows = (input.platform ?? process.platform) === 'win32';
|
|
109
|
+
const started = performance.now();
|
|
110
|
+
signal(lock.pid, input.kill ?? ((pid: number) => process.kill(pid, 'SIGTERM')));
|
|
111
|
+
const waited = (): number => Math.round(performance.now() - started);
|
|
112
|
+
// The drain removes the lock BEFORE it stops the server (`cmd-dev.ts`), so a pid that is gone
|
|
113
|
+
// is a lock already cleared — unless no drain ran, which the clear below finishes.
|
|
114
|
+
while (alive(lock.pid)) {
|
|
115
|
+
if (waited() >= input.timeoutMs) {
|
|
116
|
+
throw new DevStopTimeoutError({ pid: lock.pid, waitedMs: waited() });
|
|
117
|
+
}
|
|
118
|
+
await Bun.sleep(POLL_MS);
|
|
119
|
+
}
|
|
120
|
+
if (await lockNames(input.stateDir, lock.pid)) clearLock(input.stateDir);
|
|
121
|
+
return { stopped: true, pid: lock.pid, port: lock.port, waitedMs: waited(), drained: !windows };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** `--timeout` through `@ultimat3/time`'s grammar, the one a job's `timeout:` is read with. */
|
|
125
|
+
function readTimeout(raw: string | undefined): number {
|
|
126
|
+
if (raw === undefined) return defaultStopTimeoutMs();
|
|
127
|
+
let ms = Number.NaN;
|
|
128
|
+
try {
|
|
129
|
+
ms = parseDuration(raw);
|
|
130
|
+
} catch {
|
|
131
|
+
// Refused below with the flag named.
|
|
132
|
+
}
|
|
133
|
+
if (ms > 0) return ms;
|
|
134
|
+
throw new BadFlagError({
|
|
135
|
+
flag: 'timeout',
|
|
136
|
+
command: 'stop',
|
|
137
|
+
reason: 'it is not a positive duration such as 30s, 2m or PT1M',
|
|
138
|
+
fix: 'x stop --timeout 30s --json',
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const stopCommand: CliCommand = {
|
|
143
|
+
spec: stopSpec,
|
|
144
|
+
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
145
|
+
const root = requireAppRoot('stop', ctx.cwd).dir;
|
|
146
|
+
const timeoutMs = readTimeout(flagString(ctx.args, 'timeout'));
|
|
147
|
+
const { stateDir } = resolveServices(root, ctx.env);
|
|
148
|
+
const result = await stopDev({ stateDir, timeoutMs });
|
|
149
|
+
if (!result.stopped) {
|
|
150
|
+
const stale = result.stale === true;
|
|
151
|
+
return {
|
|
152
|
+
ok: true,
|
|
153
|
+
command: 'stop',
|
|
154
|
+
summary: msg(stale ? 'cli.stop.stale' : 'cli.stop.none'),
|
|
155
|
+
data: { stopped: false, ...(stale ? { stale: true } : {}) },
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const params = { pid: result.pid, port: result.port, ms: result.waitedMs };
|
|
159
|
+
return {
|
|
160
|
+
ok: true,
|
|
161
|
+
command: 'stop',
|
|
162
|
+
summary: msg(result.drained ? 'cli.stop.stopped' : 'cli.stop.terminated', params),
|
|
163
|
+
data: { ...result },
|
|
164
|
+
};
|
|
165
|
+
},
|
|
166
|
+
};
|
package/src/cmd-test-spec.ts
CHANGED
|
@@ -4,13 +4,19 @@
|
|
|
4
4
|
import { TEST_TYPES } from '@ultimat3/testing/test-types';
|
|
5
5
|
import { DEFAULT_BASE } from './affected';
|
|
6
6
|
import type { CommandSpec } from './parse';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
BUDGET_RULE,
|
|
9
|
+
SERIAL_TYPES,
|
|
10
|
+
WORKER_BYTES,
|
|
11
|
+
WORKER_CEILING,
|
|
12
|
+
WORKER_FLOOR,
|
|
13
|
+
} from './test-workers';
|
|
8
14
|
|
|
9
15
|
export const testSpec: CommandSpec = {
|
|
10
16
|
name: 'test',
|
|
11
17
|
summary:
|
|
12
18
|
'run one test type — or the whole suite — across N workers, one isolated database per worker',
|
|
13
|
-
usage: `x test [${TEST_TYPES.join('|')}] [--filter path[,path…]] [--allow-empty] [--sample N] [--affected [--base ref] [--dirty]] [--workers N] [--isolate] [--json] [-- <bun test flags>]`,
|
|
19
|
+
usage: `x test [${TEST_TYPES.join('|')}] [--filter path[,path…]] [--allow-empty] [--sample N] [--affected [--base ref] [--dirty]] [--workers N] [--isolate] [--seed N] [--json] [-- <bun test flags>]`,
|
|
14
20
|
positionalChoices: TEST_TYPES,
|
|
15
21
|
// The one command that hands a tail to another tool — `bun test` — and the reason
|
|
16
22
|
// `CommandSpec.passthrough` exists: `x test unit -- --coverage --bail` parsed both flags and
|
|
@@ -20,7 +26,7 @@ export const testSpec: CommandSpec = {
|
|
|
20
26
|
{
|
|
21
27
|
name: 'workers',
|
|
22
28
|
type: 'string',
|
|
23
|
-
summary: `bun worker count (default: one per ${WORKER_BYTES / 2 ** 30} GiB of a
|
|
29
|
+
summary: `bun worker count (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}); clamped to the file count, and to 1 for ${SERIAL_TYPES.join(' and ')}`,
|
|
24
30
|
},
|
|
25
31
|
{
|
|
26
32
|
name: 'isolate',
|
|
@@ -48,13 +54,20 @@ export const testSpec: CommandSpec = {
|
|
|
48
54
|
{
|
|
49
55
|
name: 'affected',
|
|
50
56
|
type: 'boolean',
|
|
51
|
-
summary:
|
|
57
|
+
summary:
|
|
58
|
+
'only the tests a diff can break: in an app root, the test files whose import closure meets it (data.affected.tests says which and why); elsewhere, the workspaces it touches and everything depending on them',
|
|
52
59
|
},
|
|
53
60
|
{
|
|
54
61
|
name: 'base',
|
|
55
62
|
type: 'string',
|
|
56
63
|
summary: `--affected: git ref to diff against, merge-base style (default: ${DEFAULT_BASE})`,
|
|
57
64
|
},
|
|
65
|
+
{
|
|
66
|
+
name: 'seed',
|
|
67
|
+
type: 'string',
|
|
68
|
+
summary:
|
|
69
|
+
'replay one simulateJobs seed — the N an X_JOB_SIM_INVARIANT names — in every file the run selects (ULTIMATE_JOB_SIM_SEED)',
|
|
70
|
+
},
|
|
58
71
|
{
|
|
59
72
|
name: 'dirty',
|
|
60
73
|
type: 'boolean',
|
package/src/cmd-test.ts
CHANGED
|
@@ -3,21 +3,26 @@
|
|
|
3
3
|
// spawned is test-shards.ts — this file only turns argv into their inputs, so a parsing bug can
|
|
4
4
|
// never be read as a sharding one. `--affected` is the one narrowing decided here rather than
|
|
5
5
|
// there, because it is a fact about a git diff and not about a path: what the diff touches is
|
|
6
|
-
// `affected.ts
|
|
6
|
+
// `affected.ts` (workspaces) and, in an app root, `affected-tests.ts` (import closure); this file
|
|
7
|
+
// only maps that answer onto the paths discovery yields.
|
|
7
8
|
|
|
8
|
-
import { TEST_TYPES } from '@ultimat3/testing';
|
|
9
|
+
import { JOB_SIM_SEED_ENV, TEST_TYPES } from '@ultimat3/testing';
|
|
9
10
|
import type { AffectedScope } from './affected';
|
|
10
11
|
import { affectedScope, affectedScopeJson, DEFAULT_BASE, inScope } from './affected';
|
|
12
|
+
import type { AppNarrowing } from './affected-tests';
|
|
13
|
+
import { appTestScope, narrowToApp } from './affected-tests';
|
|
11
14
|
import { testSpec } from './cmd-test-spec';
|
|
12
15
|
import type { CliCommand, CommandContext } from './command';
|
|
13
16
|
import { ok } from './command';
|
|
14
17
|
import { BadFlagError, NoTestFilesError } from './errors';
|
|
18
|
+
import type { Runner } from './exec';
|
|
15
19
|
import { readIntFlag } from './flag-number';
|
|
16
20
|
import { msg } from './messages';
|
|
17
21
|
import type { CommandResult, JsonValue } from './output';
|
|
18
22
|
import type { ParsedArgs } from './parse';
|
|
19
23
|
import { flagBool, flagString } from './parse';
|
|
20
24
|
import { quoteArg } from './shell-quote';
|
|
25
|
+
import type { TestFile } from './test-select';
|
|
21
26
|
import {
|
|
22
27
|
discoverTests,
|
|
23
28
|
missingSelection,
|
|
@@ -56,6 +61,29 @@ const readWorkers = (args: ParsedArgs): number | undefined =>
|
|
|
56
61
|
example: 'x test --workers 1',
|
|
57
62
|
});
|
|
58
63
|
|
|
64
|
+
/**
|
|
65
|
+
* `--seed`: the one `simulateJobs` seed to replay, the number an `X_JOB_SIM_INVARIANT` printed. A
|
|
66
|
+
* seed is a uint32 (`seededRandom` reads `seed >>> 0`), so a larger one would replay another.
|
|
67
|
+
*/
|
|
68
|
+
const readSeed = (args: ParsedArgs): number | undefined =>
|
|
69
|
+
readIntFlag(args, {
|
|
70
|
+
name: 'seed',
|
|
71
|
+
command: 'test',
|
|
72
|
+
min: 0,
|
|
73
|
+
max: 2 ** 32 - 1,
|
|
74
|
+
example: 'x test job --seed 7',
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The runner every `bun test` child is spawned through, with the seed in its environment: the
|
|
79
|
+
* child's `simulateJobs` reads `ULTIMATE_JOB_SIM_SEED` and runs that seed alone.
|
|
80
|
+
*/
|
|
81
|
+
const seeded = (runner: Runner, seed: number | undefined): Runner =>
|
|
82
|
+
seed === undefined
|
|
83
|
+
? runner
|
|
84
|
+
: (command, options) =>
|
|
85
|
+
runner(command, { ...options, env: { ...options.env, [JOB_SIM_SEED_ENV]: String(seed) } });
|
|
86
|
+
|
|
59
87
|
/**
|
|
60
88
|
* One positional, and it is the type. `x test contract live` used to run `contract` and drop
|
|
61
89
|
* `live` on the floor, so a caller reading "contract passed" believed two suites had run. A path
|
|
@@ -106,14 +134,41 @@ const asObject = (value: JsonValue | undefined): Readonly<Record<string, JsonVal
|
|
|
106
134
|
: {};
|
|
107
135
|
|
|
108
136
|
/**
|
|
109
|
-
* The scope
|
|
110
|
-
* narrowed run that does not say what it narrowed
|
|
137
|
+
* The scope as `--json` carries it: the workspace answer, plus — in an app root — which test files
|
|
138
|
+
* the import closure selected and why (`tests`). A narrowed run that does not say what it narrowed
|
|
139
|
+
* to is indistinguishable from a full one.
|
|
111
140
|
*/
|
|
112
|
-
const
|
|
141
|
+
const scopeJson = (scope: AffectedScope, app: AppNarrowing<TestFile> | undefined): JsonValue =>
|
|
142
|
+
app === undefined
|
|
143
|
+
? affectedScopeJson(scope)
|
|
144
|
+
: { ...asObject(affectedScopeJson(scope)), tests: app.json };
|
|
145
|
+
|
|
146
|
+
/** The scope, carried onto whatever the shards reported. */
|
|
147
|
+
const withScope = (result: CommandResult, scope: JsonValue): CommandResult => ({
|
|
113
148
|
...result,
|
|
114
|
-
data: { ...asObject(result.data), affected:
|
|
149
|
+
data: { ...asObject(result.data), affected: scope },
|
|
115
150
|
});
|
|
116
151
|
|
|
152
|
+
/**
|
|
153
|
+
* In an app root the diff narrows by import closure — `selectTests`, the rule `x affected` reports
|
|
154
|
+
* as `data.tests` — since every slice shares the `apps/web` workspace. Outside one, by workspace.
|
|
155
|
+
*/
|
|
156
|
+
async function narrowAffected(
|
|
157
|
+
discovered: readonly TestFile[],
|
|
158
|
+
cwd: string,
|
|
159
|
+
scope: AffectedScope,
|
|
160
|
+
): Promise<{ readonly files: readonly TestFile[]; readonly json: JsonValue }> {
|
|
161
|
+
const inApp = await appTestScope(cwd, scope.root, scope.plan.changed);
|
|
162
|
+
if (inApp === undefined) {
|
|
163
|
+
return {
|
|
164
|
+
files: discovered.filter((file) => inScope(file.path, scope.prefixes)),
|
|
165
|
+
json: scopeJson(scope, undefined),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const narrowed = narrowToApp(discovered, cwd, inApp);
|
|
169
|
+
return { files: narrowed.kept, json: scopeJson(scope, narrowed) };
|
|
170
|
+
}
|
|
171
|
+
|
|
117
172
|
/** The selection that matched nothing, as one phrase for `cli.test.empty`. */
|
|
118
173
|
const describeSelection = (missing: { type?: string; filter?: string }): string =>
|
|
119
174
|
[
|
|
@@ -130,6 +185,7 @@ export const testCommand: CliCommand = {
|
|
|
130
185
|
const filter = flagString(ctx.args, 'filter');
|
|
131
186
|
const filters = readFilters(filter);
|
|
132
187
|
const sample = readSample(ctx.args);
|
|
188
|
+
const seed = readSeed(ctx.args);
|
|
133
189
|
const scope = await readAffectedScope(ctx);
|
|
134
190
|
const discovered = await discoverTests(ctx.cwd, filters, type);
|
|
135
191
|
if (discovered.length === 0) {
|
|
@@ -146,11 +202,10 @@ export const testCommand: CliCommand = {
|
|
|
146
202
|
}
|
|
147
203
|
throw new NoTestFilesError({ root: ctx.cwd, ...missing });
|
|
148
204
|
}
|
|
149
|
-
const
|
|
150
|
-
scope === undefined
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (scope !== undefined && selected.length === 0) {
|
|
205
|
+
const narrowed =
|
|
206
|
+
scope === undefined ? undefined : await narrowAffected(discovered, ctx.cwd, scope);
|
|
207
|
+
const selected = narrowed === undefined ? discovered : narrowed.files;
|
|
208
|
+
if (scope !== undefined && narrowed !== undefined && selected.length === 0) {
|
|
154
209
|
// Green, and it spawns nothing — a `.md`-only diff genuinely re-checks nothing, and failing
|
|
155
210
|
// a build for editing a doc is the wrong answer. It never reads as "the suite passed": the
|
|
156
211
|
// summary counts the files that ran (zero) and `data.affected` names the diff it asked about,
|
|
@@ -161,7 +216,7 @@ export const testCommand: CliCommand = {
|
|
|
161
216
|
data: {
|
|
162
217
|
...(type === undefined ? {} : { type }),
|
|
163
218
|
files: 0,
|
|
164
|
-
affected:
|
|
219
|
+
affected: narrowed.json,
|
|
165
220
|
},
|
|
166
221
|
});
|
|
167
222
|
}
|
|
@@ -179,11 +234,12 @@ export const testCommand: CliCommand = {
|
|
|
179
234
|
const workers = Math.max(1, Math.min(requested, ceiling));
|
|
180
235
|
const result = await runShards({
|
|
181
236
|
root: ctx.cwd,
|
|
182
|
-
runner: ctx.runner,
|
|
237
|
+
runner: seeded(ctx.runner, seed),
|
|
183
238
|
env: ctx.env,
|
|
184
239
|
files,
|
|
185
240
|
workers,
|
|
186
241
|
...(isolate ? { isolate } : {}),
|
|
242
|
+
...(seed === undefined ? {} : { seed }),
|
|
187
243
|
// A default-width run leases its workers from the machine pool, so a second `x test` or
|
|
188
244
|
// gate on the same box runs narrower instead of doubling its memory.
|
|
189
245
|
...(plan === undefined ? {} : withLease(machineLease(plan.workers, ctx.env))),
|
|
@@ -200,7 +256,7 @@ export const testCommand: CliCommand = {
|
|
|
200
256
|
// changes what a run measures, and the reproduce line carries it back out.
|
|
201
257
|
...(ctx.args.passthrough.length === 0 ? {} : { passthrough: ctx.args.passthrough }),
|
|
202
258
|
});
|
|
203
|
-
return
|
|
259
|
+
return narrowed === undefined ? result : withScope(result, narrowed.json);
|
|
204
260
|
},
|
|
205
261
|
};
|
|
206
262
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// `x upgrade`'s declaration, apart from its body: the parser, `x help` and the `errors` step read it
|
|
2
|
+
// without loading `cmd-upgrade.ts`, which `registry.ts` imports only when the command runs.
|
|
3
|
+
|
|
4
|
+
import type { CommandSpec } from './parse';
|
|
5
|
+
|
|
6
|
+
export const upgradeSpec: CommandSpec = {
|
|
7
|
+
name: 'upgrade',
|
|
8
|
+
summary:
|
|
9
|
+
'move every @ultimat3/* one major in lockstep, raise the Bun floor, install, gate — and name the section to read',
|
|
10
|
+
usage: 'x upgrade [--to <version>] [--dry-run] [--json]',
|
|
11
|
+
// The one command a repository with pins and no `app.config.ts` can still use: `upgrade-root.ts`
|
|
12
|
+
// finds its root, and its report says which check it could run.
|
|
13
|
+
flags: [
|
|
14
|
+
{
|
|
15
|
+
name: 'to',
|
|
16
|
+
type: 'string',
|
|
17
|
+
summary:
|
|
18
|
+
'an exact release inside the next major, or the higher version a mixed set was told to align to (default: the next major’s newest)',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'dry-run',
|
|
22
|
+
type: 'boolean',
|
|
23
|
+
summary: 'report the pins, the Bun floor edits and the section; write nothing, run no gate',
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
};
|