@crvouga/atlas 0.3.0 → 0.5.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/CHANGELOG.md +12 -0
- package/README.md +178 -604
- package/dist/bun/src/driver.d.ts +41 -7
- package/dist/bun/src/driver.js +47 -6
- package/dist/bun/src/index.d.ts +8 -0
- package/dist/bun/src/index.js +8 -0
- package/dist/core/src/cli.d.ts +14 -1
- package/dist/core/src/cli.js +30 -1
- package/dist/core/src/config.d.ts +70 -4
- package/dist/core/src/config.js +62 -3
- package/dist/core/src/events/cloudevents.d.ts +54 -9
- package/dist/core/src/events/cloudevents.js +60 -9
- package/dist/core/src/graph.d.ts +21 -0
- package/dist/core/src/graph.js +26 -0
- package/dist/core/src/index.d.ts +11 -0
- package/dist/core/src/index.js +11 -0
- package/dist/core/src/lint.d.ts +30 -0
- package/dist/core/src/lint.js +13 -0
- package/dist/core/src/plan.d.ts +39 -1
- package/dist/core/src/plan.js +28 -1
- package/dist/core/src/privacy.d.ts +29 -2
- package/dist/core/src/privacy.js +19 -2
- package/dist/core/src/report/formats.d.ts +34 -4
- package/dist/core/src/report/formats.js +24 -4
- package/dist/core/src/report/markdown.d.ts +15 -0
- package/dist/core/src/report/markdown.js +8 -0
- package/dist/core/src/run/clients.d.ts +44 -5
- package/dist/core/src/run/clients.js +46 -2
- package/dist/core/src/run/execute.d.ts +69 -16
- package/dist/core/src/run/execute.js +67 -16
- package/dist/core/src/run/frames.d.ts +13 -4
- package/dist/core/src/run/frames.js +19 -4
- package/dist/core/src/run/http.d.ts +34 -1
- package/dist/core/src/run/http.js +14 -1
- package/dist/core/src/run/types.d.ts +76 -3
- package/dist/core/src/run/types.js +19 -3
- package/dist/core/src/signals.d.ts +49 -5
- package/dist/core/src/signals.js +22 -5
- package/dist/core/src/spec/compose.d.ts +23 -1
- package/dist/core/src/spec/compose.js +10 -1
- package/dist/core/src/spec/index.d.ts +9 -1
- package/dist/core/src/spec/index.js +9 -1
- package/dist/core/src/spec/load.d.ts +22 -1
- package/dist/core/src/spec/load.js +31 -1
- package/dist/core/src/spec/parse.d.ts +16 -2
- package/dist/core/src/spec/parse.js +25 -2
- package/dist/core/src/spec/scxml.d.ts +25 -4
- package/dist/core/src/spec/scxml.js +36 -4
- package/dist/core/src/spec/types.d.ts +52 -1
- package/dist/core/src/spec/types.js +24 -0
- package/dist/detox/src/index.d.ts +54 -11
- package/dist/detox/src/index.js +45 -7
- package/dist/playwright/src/driver.d.ts +39 -3
- package/dist/playwright/src/driver.js +27 -3
- package/dist/playwright/src/index.d.ts +10 -0
- package/dist/playwright/src/index.js +10 -0
- package/dist/playwright/src/media.d.ts +50 -4
- package/dist/playwright/src/media.js +55 -4
- package/dist/playwright/src/signals.d.ts +44 -5
- package/dist/playwright/src/signals.js +29 -5
- package/dist/reports/src/attachment-files.d.ts +44 -0
- package/dist/reports/src/attachment-files.js +121 -0
- package/dist/reports/src/attachment-response.d.ts +31 -0
- package/dist/reports/src/attachment-response.js +55 -0
- package/dist/reports/src/aws-credentials.d.ts +48 -0
- package/dist/reports/src/aws-credentials.js +221 -0
- package/dist/reports/src/browser-navigation.d.ts +14 -0
- package/dist/reports/src/browser-navigation.js +7 -0
- package/dist/reports/src/bundle.d.ts +34 -2
- package/dist/reports/src/bundle.js +38 -2
- package/dist/reports/src/catalog.d.ts +34 -3
- package/dist/reports/src/catalog.js +17 -3
- package/dist/reports/src/cli-stores.d.ts +36 -0
- package/dist/reports/src/cli-stores.js +73 -0
- package/dist/reports/src/clock.d.ts +7 -2
- package/dist/reports/src/clock.js +5 -2
- package/dist/reports/src/completion-order.d.ts +18 -5
- package/dist/reports/src/completion-order.js +18 -5
- package/dist/reports/src/compose.d.ts +80 -0
- package/dist/reports/src/compose.js +117 -0
- package/dist/reports/src/copy-store.d.ts +29 -0
- package/dist/reports/src/copy-store.js +52 -0
- package/dist/reports/src/directory-outline.d.ts +38 -0
- package/dist/reports/src/directory-outline.js +103 -0
- package/dist/reports/src/directory-store.d.ts +20 -0
- package/dist/reports/src/directory-store.js +166 -0
- package/dist/reports/src/doctor.d.ts +29 -0
- package/dist/reports/src/doctor.js +83 -0
- package/dist/reports/src/effective-delta.d.ts +34 -0
- package/dist/reports/src/effective-delta.js +25 -0
- package/dist/reports/src/evidence-detail.d.ts +64 -0
- package/dist/reports/src/evidence-detail.js +92 -0
- package/dist/reports/src/evidence-live.d.ts +40 -4
- package/dist/reports/src/evidence-live.js +52 -4
- package/dist/reports/src/evidence-types.d.ts +70 -1
- package/dist/reports/src/evidence-types.js +5 -1
- package/dist/reports/src/evidence-view.d.ts +58 -6
- package/dist/reports/src/evidence-view.js +44 -5
- package/dist/reports/src/explorer-types.d.ts +40 -3
- package/dist/reports/src/explorer-types.js +8 -0
- package/dist/reports/src/explorer-versions.d.ts +9 -1
- package/dist/reports/src/explorer-versions.js +18 -1
- package/dist/reports/src/export-evidence.d.ts +16 -1
- package/dist/reports/src/export-evidence.js +52 -1
- package/dist/reports/src/fetcher.d.ts +27 -3
- package/dist/reports/src/fetcher.js +11 -3
- package/dist/reports/src/format-date.d.ts +6 -0
- package/dist/reports/src/format-date.js +6 -0
- package/dist/reports/src/format.d.ts +16 -0
- package/dist/reports/src/format.js +10 -0
- package/dist/reports/src/frames.d.ts +36 -3
- package/dist/reports/src/frames.js +22 -3
- package/dist/reports/src/hash.d.ts +18 -0
- package/dist/reports/src/hash.js +13 -0
- package/dist/reports/src/host.d.ts +29 -0
- package/dist/reports/src/host.js +64 -0
- package/dist/reports/src/html-frame.d.ts +5 -6
- package/dist/reports/src/html-frame.js +12 -1
- package/dist/reports/src/http-store.d.ts +52 -0
- package/dist/reports/src/http-store.js +145 -0
- package/dist/reports/src/http.d.ts +7 -0
- package/dist/reports/src/http.js +6 -0
- package/dist/reports/src/ingest.d.ts +54 -0
- package/dist/reports/src/ingest.js +210 -0
- package/dist/reports/src/init.d.ts +40 -0
- package/dist/reports/src/init.js +364 -0
- package/dist/reports/src/junit.d.ts +33 -6
- package/dist/reports/src/junit.js +87 -21
- package/dist/reports/src/keyboard.d.ts +20 -0
- package/dist/reports/src/keyboard.js +11 -0
- package/dist/reports/src/keys.d.ts +21 -3
- package/dist/reports/src/keys.js +21 -3
- package/dist/reports/src/layer-axes.d.ts +48 -0
- package/dist/reports/src/layer-axes.js +71 -0
- package/dist/reports/src/layer-catalog.d.ts +28 -1
- package/dist/reports/src/layer-catalog.js +19 -2
- package/dist/reports/src/layer-merge.d.ts +98 -9
- package/dist/reports/src/layer-merge.js +154 -33
- package/dist/reports/src/layer-source.d.ts +18 -0
- package/dist/reports/src/layer-source.js +14 -0
- package/dist/reports/src/layer-store.d.ts +42 -0
- package/dist/reports/src/layer-store.js +63 -5
- package/dist/reports/src/layer-types.d.ts +34 -1
- package/dist/reports/src/layers-route.d.ts +47 -4
- package/dist/reports/src/layers-route.js +88 -12
- package/dist/reports/src/media-types.d.ts +11 -0
- package/dist/reports/src/media-types.js +22 -0
- package/dist/reports/src/namespace-guard.d.ts +10 -1
- package/dist/reports/src/namespace-guard.js +10 -1
- package/dist/reports/src/navigation.d.ts +12 -0
- package/dist/reports/src/navigation.js +7 -0
- package/dist/reports/src/object-io.d.ts +11 -4
- package/dist/reports/src/object-io.js +4 -4
- package/dist/reports/src/object-store-conformance.d.ts +27 -0
- package/dist/reports/src/object-store-conformance.js +87 -0
- package/dist/reports/src/object-store.d.ts +33 -24
- package/dist/reports/src/object-store.js +17 -24
- package/dist/reports/src/parse-with.d.ts +16 -0
- package/dist/reports/src/parse-with.js +14 -0
- package/dist/reports/src/performance-part.d.ts +25 -1
- package/dist/reports/src/performance-part.js +15 -1
- package/dist/reports/src/performance.d.ts +22 -2
- package/dist/reports/src/performance.js +12 -2
- package/dist/reports/src/persist.d.ts +12 -3
- package/dist/reports/src/persist.js +12 -3
- package/dist/reports/src/phone.d.ts +18 -1
- package/dist/reports/src/phone.js +18 -1
- package/dist/reports/src/protocol.d.ts +30 -4
- package/dist/reports/src/protocol.js +15 -4
- package/dist/reports/src/publication.d.ts +21 -1
- package/dist/reports/src/publication.js +21 -1
- package/dist/reports/src/publish-checks.d.ts +35 -0
- package/dist/reports/src/publish-checks.js +122 -0
- package/dist/reports/src/publish.d.ts +44 -9
- package/dist/reports/src/publish.js +40 -7
- package/dist/reports/src/read.d.ts +46 -16
- package/dist/reports/src/read.js +38 -14
- package/dist/reports/src/recording-bundle.d.ts +13 -2
- package/dist/reports/src/recording-bundle.js +4 -2
- package/dist/reports/src/recording-keys.d.ts +15 -1
- package/dist/reports/src/recording-keys.js +15 -1
- package/dist/reports/src/remote-report.d.ts +28 -7
- package/dist/reports/src/remote-report.js +15 -7
- package/dist/reports/src/report-cli.d.ts +51 -0
- package/dist/reports/src/report-cli.js +485 -0
- package/dist/reports/src/report-index.d.ts +99 -0
- package/dist/reports/src/report-index.js +337 -0
- package/dist/reports/src/report-loader.d.ts +10 -0
- package/dist/reports/src/report-loader.js +6 -0
- package/dist/reports/src/report-parts.d.ts +29 -3
- package/dist/reports/src/report-parts.js +40 -3
- package/dist/reports/src/report-status.d.ts +48 -0
- package/dist/reports/src/report-status.js +156 -0
- package/dist/reports/src/report-types.d.ts +73 -0
- package/dist/reports/src/report-types.js +3 -0
- package/dist/reports/src/reporter.d.ts +67 -0
- package/dist/reports/src/reporter.js +128 -0
- package/dist/reports/src/reports-explorer-route.d.ts +541 -0
- package/dist/reports/src/reports-explorer-route.js +11 -0
- package/dist/reports/src/reports-list-route.d.ts +9 -0
- package/dist/reports/src/reports-list-route.js +9 -0
- package/dist/reports/src/reports-route.d.ts +561 -22
- package/dist/reports/src/reports-route.js +11 -3
- package/dist/reports/src/reports-store.d.ts +19 -2
- package/dist/reports/src/reports-store.js +10 -2
- package/dist/reports/src/resilient-cache.d.ts +14 -1
- package/dist/reports/src/resilient-cache.js +8 -1
- package/dist/reports/src/retention.d.ts +17 -1
- package/dist/reports/src/retention.js +5 -1
- package/dist/reports/src/run-bundle.d.ts +63 -0
- package/dist/reports/src/run-bundle.js +159 -0
- package/dist/reports/src/s3-store.d.ts +79 -0
- package/dist/reports/src/s3-store.js +295 -0
- package/dist/reports/src/scenario.d.ts +14 -4
- package/dist/reports/src/scenario.js +5 -4
- package/dist/reports/src/sigv4.d.ts +72 -0
- package/dist/reports/src/sigv4.js +96 -0
- package/dist/reports/src/snapshot-bundle.d.ts +24 -1
- package/dist/reports/src/snapshot-bundle.js +27 -2
- package/dist/reports/src/snapshot-surface.d.ts +20 -9
- package/dist/reports/src/snapshot-surface.js +32 -19
- package/dist/reports/src/source-content.d.ts +23 -0
- package/dist/reports/src/source-content.js +30 -1
- package/dist/reports/src/source-entries.d.ts +13 -0
- package/dist/reports/src/source-entries.js +6 -0
- package/dist/reports/src/source-part.d.ts +15 -0
- package/dist/reports/src/source-part.js +8 -0
- package/dist/reports/src/source-reader.d.ts +11 -0
- package/dist/reports/src/source-reader.js +5 -0
- package/dist/reports/src/spec-evidence.d.ts +12 -2
- package/dist/reports/src/spec-evidence.js +6 -2
- package/dist/reports/src/spec-inventory.d.ts +15 -1
- package/dist/reports/src/spec-inventory.js +22 -1
- package/dist/reports/src/spec-link.d.ts +28 -0
- package/dist/reports/src/spec-link.js +70 -0
- package/dist/reports/src/spec-set.d.ts +63 -0
- package/dist/reports/src/spec-set.js +177 -0
- package/dist/reports/src/status.d.ts +10 -0
- package/dist/reports/src/status.js +5 -0
- package/dist/reports/src/store-prefix.d.ts +28 -0
- package/dist/reports/src/store-prefix.js +22 -0
- package/dist/reports/src/store-url.d.ts +89 -0
- package/dist/reports/src/store-url.js +160 -0
- package/dist/reports/src/test-catalog-entity.d.ts +11 -0
- package/dist/reports/src/test-catalog-entity.js +6 -0
- package/dist/reports/src/types.d.ts +6 -1
- package/dist/reports/src/use-report-nav.d.ts +35 -0
- package/dist/reports/src/use-report-nav.js +12 -0
- package/dist/schema/src/api.d.ts +799 -0
- package/dist/schema/src/api.js +201 -0
- package/dist/schema/src/cli-results.d.ts +210 -0
- package/dist/schema/src/cli-results.js +124 -0
- package/dist/schema/src/common.d.ts +53 -0
- package/dist/schema/src/common.js +42 -0
- package/dist/schema/src/index.d.ts +14 -0
- package/dist/schema/src/index.js +14 -0
- package/dist/schema/src/layer.d.ts +2273 -0
- package/dist/schema/src/layer.js +462 -0
- package/dist/schema/src/manifest.d.ts +95 -0
- package/dist/schema/src/manifest.js +84 -0
- package/dist/schema/src/report-sources.d.ts +28 -0
- package/dist/schema/src/report-sources.js +25 -0
- package/dist/schema/src/runs-index.d.ts +16 -0
- package/dist/schema/src/runs-index.js +15 -0
- package/dist/schema/src/server-config.d.ts +93 -0
- package/dist/schema/src/server-config.js +73 -0
- package/dist/schema/src/spec.d.ts +69 -0
- package/dist/schema/src/spec.js +58 -0
- package/dist/server/src/app.d.ts +62 -0
- package/dist/server/src/app.js +419 -0
- package/dist/server/src/auth.d.ts +41 -0
- package/dist/server/src/auth.js +59 -0
- package/dist/server/src/config.d.ts +83 -0
- package/dist/server/src/config.js +187 -0
- package/dist/server/src/dev.d.ts +12 -0
- package/dist/server/src/dev.js +80 -0
- package/dist/server/src/events.d.ts +29 -0
- package/dist/server/src/events.js +101 -0
- package/dist/server/src/host.d.ts +30 -0
- package/dist/server/src/host.js +115 -0
- package/dist/server/src/index.d.ts +16 -0
- package/dist/server/src/index.js +16 -0
- package/dist/server/src/llms.d.ts +19 -0
- package/dist/server/src/llms.js +63 -0
- package/dist/server/src/main.d.ts +30 -0
- package/dist/server/src/main.js +112 -0
- package/dist/server/src/openapi.d.ts +59 -0
- package/dist/server/src/openapi.js +374 -0
- package/dist/server/src/problem.d.ts +8 -0
- package/dist/server/src/problem.js +39 -0
- package/dist/server/src/site.d.ts +13 -0
- package/dist/server/src/site.js +43 -0
- package/dist/server/src/store-api.d.ts +28 -0
- package/dist/server/src/store-api.js +159 -0
- package/dist/server/src/validate.d.ts +71 -0
- package/dist/server/src/validate.js +197 -0
- package/dist/vitest/src/index.d.ts +27 -5
- package/dist/vitest/src/index.js +18 -5
- package/dist/webdriver/src/driver.d.ts +63 -11
- package/dist/webdriver/src/driver.js +57 -6
- package/dist/webdriver/src/index.d.ts +8 -0
- package/dist/webdriver/src/index.js +1 -0
- package/dist/webdriver/src/protocol.d.ts +75 -6
- package/dist/webdriver/src/protocol.js +79 -6
- package/docs/agents.md +137 -0
- package/docs/deploying.md +345 -0
- package/docs/dogfooding.md +175 -0
- package/docs/extending.md +242 -0
- package/docs/integrating.md +485 -0
- package/docs/reference/bun.md +141 -0
- package/docs/reference/cli.md +146 -0
- package/docs/reference/core.md +2426 -0
- package/docs/reference/detox.md +150 -0
- package/docs/reference/http-api.md +303 -0
- package/docs/reference/playwright.md +303 -0
- package/docs/reference/reports.md +7098 -0
- package/docs/reference/schema.md +1651 -0
- package/docs/reference/server.md +685 -0
- package/docs/reference/vitest.md +45 -0
- package/docs/reference/webdriver.md +330 -0
- package/docs/report-backends.md +144 -0
- package/docs/report-contract.md +400 -0
- package/docs/reporting-issues.md +160 -0
- package/docs/server.md +285 -0
- package/docs/statecharts.md +641 -0
- package/llms-full.txt +3506 -0
- package/llms.txt +47 -0
- package/package.json +33 -5
- package/packages/core/README.md +2 -1
- package/packages/reports/README.md +69 -2
- package/packages/reports/STORAGE.md +5 -1
- package/packages/schema/README.md +14 -0
- package/packages/schema/schemas/check-detail.schema.json +1032 -0
- package/packages/schema/schemas/checks-result.schema.json +26 -0
- package/packages/schema/schemas/command-error.schema.json +35 -0
- package/packages/schema/schemas/doctor-result.schema.json +60 -0
- package/packages/schema/schemas/effective-delta.schema.json +490 -0
- package/packages/schema/schemas/effective-evidence.schema.json +471 -0
- package/packages/schema/schemas/evidence-page.schema.json +271 -0
- package/packages/schema/schemas/hello-event.schema.json +22 -0
- package/packages/schema/schemas/init-plan.schema.json +347 -0
- package/packages/schema/schemas/layer-rows.schema.json +199 -0
- package/packages/schema/schemas/layer.schema.json +2888 -0
- package/packages/schema/schemas/merge-result.schema.json +40 -0
- package/packages/schema/schemas/outline.schema.json +62 -0
- package/packages/schema/schemas/problem.schema.json +37 -0
- package/packages/schema/schemas/publish-checks.schema.json +1413 -0
- package/packages/schema/schemas/publish-result.schema.json +168 -0
- package/packages/schema/schemas/ready.schema.json +68 -0
- package/packages/schema/schemas/report-event.schema.json +100 -0
- package/packages/schema/schemas/report-status.schema.json +370 -0
- package/packages/schema/schemas/server-config.schema.json +194 -0
- package/packages/schema/schemas/server-info.schema.json +114 -0
- package/packages/schema/schemas/spec-set.schema.json +103 -0
- package/packages/schema/schemas/store-listing.schema.json +29 -0
- package/packages/schema/schemas/store-write-result.schema.json +28 -0
- package/packages/schema/schemas/stream-result.schema.json +59 -0
- package/packages/server/README.md +43 -0
- package/packages/server/openapi.json +6699 -0
- package/packages/visualizer/README.md +27 -5
- package/packages/visualizer/example/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/example/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/example/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/example/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/example/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/example/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/scripts/make-fixtures.ts +2 -0
- package/packages/visualizer/scripts/publish-data.ts +1 -2
- package/packages/visualizer/src/app/AppShell.tsx +9 -2
- package/packages/visualizer/src/app/report-sources.tsx +6 -4
- package/packages/visualizer/src/app/router.tsx +3 -3
- package/packages/visualizer/src/components/Header.module.css +35 -0
- package/packages/visualizer/src/components/Header.tsx +41 -4
- package/packages/visualizer/src/data/adapters/adapter.ts +5 -17
- package/packages/visualizer/src/data/adapters/index.ts +10 -1
- package/packages/visualizer/src/evidence/api.ts +14 -2
- package/packages/visualizer/src/evidence/artifacts.module.css +213 -0
- package/packages/visualizer/src/evidence/artifacts.tsx +272 -0
- package/packages/visualizer/src/evidence/check-detail.tsx +38 -4
- package/packages/visualizer/src/evidence/evidence-context.tsx +74 -12
- package/packages/visualizer/src/evidence/evidence.module.css +20 -0
- package/packages/visualizer/src/evidence/marks.tsx +2 -0
- package/packages/visualizer/src/evidence/overview.tsx +3 -3
- package/packages/visualizer/src/evidence/sources-panel.tsx +88 -34
- package/packages/visualizer/src/evidence/spec-page.tsx +23 -16
- package/packages/visualizer/src/find/Finder.tsx +1 -7
- package/packages/visualizer/src/find/open.ts +4 -0
- package/packages/visualizer/src/live/live-stream.ts +204 -0
- package/packages/visualizer/src/runtime.ts +19 -0
- package/packages/visualizer/src/state/ui-store.ts +6 -2
- package/packages/visualizer/vite/atlas-server-proxy.ts +36 -0
- package/packages/visualizer/vite/example-reports.ts +475 -0
- package/packages/visualizer/vite.config.ts +18 -2
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Importing checks from existing test reports
|
|
3
|
+
*
|
|
4
|
+
* Turns the output of tools that already exist (CTRF JSON, JUnit XML, an Atlas run `manifest.json`) into checks a
|
|
5
|
+
* layer can hold, so any test suite can publish without a custom producer. It owns the mapping of foreign
|
|
6
|
+
* statuses onto the four Atlas statuses (anything not clearly passed or failed is `not-run`, never a pass) and the
|
|
7
|
+
* rule for check ids: stable across runs, built from the file, suite and name, with a repeated id numbered
|
|
8
|
+
* (`#2`, `#3`) so no check is silently counted as another. It reads text it is given and touches no files.
|
|
9
|
+
*
|
|
10
|
+
* @see docs/report-contract.md (choosing a check id)
|
|
11
|
+
*/
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { parseJunit } from './junit.js';
|
|
14
|
+
import { specPath } from './layer-merge.js';
|
|
15
|
+
function record(value) {
|
|
16
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) ? value : {};
|
|
17
|
+
}
|
|
18
|
+
function text(value) {
|
|
19
|
+
return typeof value === 'string' ? value : '';
|
|
20
|
+
}
|
|
21
|
+
function finite(value) {
|
|
22
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
23
|
+
}
|
|
24
|
+
/** Never silently count two checks as one: the first check with an id keeps it and a repeat becomes `<id> #2`, `<id> #3`, ... in input order. */
|
|
25
|
+
export function uniqueChecks(checks) {
|
|
26
|
+
const seen = new Map();
|
|
27
|
+
return checks.map(check => {
|
|
28
|
+
const count = (seen.get(check.id) ?? 0) + 1;
|
|
29
|
+
seen.set(check.id, count);
|
|
30
|
+
return count === 1 ? check : { ...check, id: `${check.id} #${count}` };
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const CTRF_STATUS = { passed: 'passed', failed: 'failed', skipped: 'not-run', pending: 'not-run', other: 'not-run' };
|
|
34
|
+
/**
|
|
35
|
+
* A [CTRF](https://ctrf.io) report (`results.tests`). A check's id is its file, its suites and its
|
|
36
|
+
* name, so the same test keeps the same id in every run. Skipped, pending and other results are
|
|
37
|
+
* `not-run`: never a pass; a status this function does not know is also `not-run`.
|
|
38
|
+
*
|
|
39
|
+
* Also carries what CTRF has beyond the outcome as evidence: tags, labels (browser, device, flaky, run environment
|
|
40
|
+
* and tool), steps, attachments (files by `path`, `url` as `uri`, inline screenshot, stdout/stderr, trace), retry
|
|
41
|
+
* attempts, a snippet annotation and `extra` under the `ctrf.extra` extension. `recordedAtMs` is `summary.stop`,
|
|
42
|
+
* else `summary.start`. Throws `Not a CTRF report: results.tests is missing` when there is no `results.tests` array.
|
|
43
|
+
*
|
|
44
|
+
* @param report The parsed CTRF JSON.
|
|
45
|
+
*/
|
|
46
|
+
export function checksFromCtrf(report) {
|
|
47
|
+
const results = record(record(report).results);
|
|
48
|
+
const tests = Array.isArray(results.tests) ? results.tests : [];
|
|
49
|
+
if (!Array.isArray(results.tests))
|
|
50
|
+
throw new Error('Not a CTRF report: results.tests is missing');
|
|
51
|
+
const checks = tests.map((raw, index) => {
|
|
52
|
+
const test = record(raw);
|
|
53
|
+
const suite = Array.isArray(test.suite) ? test.suite.map(text) : text(test.suite) ? [text(test.suite)] : [];
|
|
54
|
+
const file = text(test.filePath);
|
|
55
|
+
const name = text(test.name) || `test ${index + 1}`;
|
|
56
|
+
const status = CTRF_STATUS[text(test.status)] ?? 'not-run';
|
|
57
|
+
const message = text(test.message) || text(test.trace);
|
|
58
|
+
const duration = finite(test.duration);
|
|
59
|
+
const tags = Array.isArray(test.tags) ? test.tags.map(text).filter(Boolean) : [];
|
|
60
|
+
const labels = {};
|
|
61
|
+
for (const key of ['browser', 'device', 'type', 'os', 'osPlatform'])
|
|
62
|
+
if (text(test[key]))
|
|
63
|
+
labels[key] = text(test[key]);
|
|
64
|
+
if (test.flaky === true)
|
|
65
|
+
labels.flaky = 'true';
|
|
66
|
+
const steps = Array.isArray(test.steps) ? test.steps.map(step => { const item = record(step); return { name: text(item.name) || 'step', ...(text(item.status) ? { status: text(item.status) } : {}) }; }) : [];
|
|
67
|
+
const attachments = [
|
|
68
|
+
...(Array.isArray(test.attachments) ? test.attachments.flatMap(raw => { const item = record(raw); return text(item.name) || text(item.path) ? [{ name: text(item.name) || path.basename(text(item.path)), ...(text(item.contentType) ? { contentType: text(item.contentType) } : {}), ...(text(item.path) ? { path: text(item.path) } : {}), ...(text(item.url) ? { uri: text(item.url) } : {}) }] : []; }) : []),
|
|
69
|
+
...(text(test.screenshot) ? [{ name: 'screenshot.png', role: 'screenshot', contentType: 'image/png', base64: text(test.screenshot) }] : []),
|
|
70
|
+
...logOf(test.stdout, 'stdout'),
|
|
71
|
+
...logOf(test.stderr, 'stderr'),
|
|
72
|
+
...(text(test.trace) ? [{ name: 'stack trace', role: 'log', contentType: 'text/plain', text: text(test.trace) }] : [])
|
|
73
|
+
];
|
|
74
|
+
const attempts = Array.isArray(test.retryAttempts) ? test.retryAttempts.map((raw, at) => { const item = record(raw); return { index: finite(item.attempt) !== null ? Math.max(0, finite(item.attempt) - 1) : at, status: (CTRF_STATUS[text(item.status)] ?? 'not-run'), ...(finite(item.duration) !== null ? { durationMs: finite(item.duration) } : {}), ...(text(item.message) ? { detail: text(item.message) } : {}) }; }) : [];
|
|
75
|
+
const annotations = text(test.snippet) ? [{ type: 'snippet', message: text(test.snippet), level: 'info', ...(file ? { source: { file, ...(finite(test.line) !== null ? { line: finite(test.line) } : {}) } } : {}) }] : [];
|
|
76
|
+
const extra = record(test.extra);
|
|
77
|
+
return {
|
|
78
|
+
id: [file, ...suite, name].filter(Boolean).join(' > '),
|
|
79
|
+
title: name,
|
|
80
|
+
status,
|
|
81
|
+
detail: status === 'failed' ? (message || 'failed') : test.flaky === true ? 'Passed on retry (flaky)' : message,
|
|
82
|
+
...(file ? { specPath: specPath(file) } : {}),
|
|
83
|
+
measurements: [],
|
|
84
|
+
...(duration === null ? {} : { durationMs: duration }),
|
|
85
|
+
...(tags.length ? { tags } : {}),
|
|
86
|
+
...(Object.keys(labels).length ? { labels } : {}),
|
|
87
|
+
...(steps.length ? { steps } : {}),
|
|
88
|
+
...(attachments.length ? { attachments } : {}),
|
|
89
|
+
...(attempts.length ? { attempts } : {}),
|
|
90
|
+
...(annotations.length ? { annotations } : {}),
|
|
91
|
+
...(Object.keys(extra).length ? { extensions: { 'ctrf.extra': extra } } : {})
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
const summary = record(results.summary);
|
|
95
|
+
const hints = tests.map(raw => {
|
|
96
|
+
const test = record(raw);
|
|
97
|
+
return [...(Array.isArray(test.suite) ? test.suite.map(text) : [text(test.suite)]), text(test.filePath)].filter(Boolean);
|
|
98
|
+
});
|
|
99
|
+
const tool = record(results.tool);
|
|
100
|
+
const labels = {};
|
|
101
|
+
for (const [key, value] of Object.entries(record(results.environment)))
|
|
102
|
+
if (typeof value === 'string' || typeof value === 'number')
|
|
103
|
+
labels[key] = String(value);
|
|
104
|
+
if (text(tool.name))
|
|
105
|
+
labels.tool = text(tool.name);
|
|
106
|
+
if (text(tool.version))
|
|
107
|
+
labels['tool.version'] = text(tool.version);
|
|
108
|
+
return { checks: uniqueChecks(checks), hints, recordedAtMs: finite(summary.stop) ?? finite(summary.start), tool: text(tool.name) || null, ...(Object.keys(labels).length ? { labels } : {}) };
|
|
109
|
+
}
|
|
110
|
+
/** CTRF writes a stream as a list of lines. */
|
|
111
|
+
function logOf(value, name) {
|
|
112
|
+
const lines = Array.isArray(value) ? value.map(text).join('\n') : text(value);
|
|
113
|
+
return lines.trim() ? [{ name, role: 'log', contentType: 'text/plain', text: lines }] : [];
|
|
114
|
+
}
|
|
115
|
+
/** Many JUnit writers omit the offset. Read such a time as UTC, so it is the same wherever Atlas runs. */
|
|
116
|
+
function parseTimestamp(value) {
|
|
117
|
+
return Date.parse(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?$/.test(value) ? `${value}Z` : value);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* A JUnit XML report: one check per `<testcase>`, identified by its file or class and its name (`file or classname > name`).
|
|
121
|
+
*
|
|
122
|
+
* A case with a `<failure>` (an assertion that did not hold) or an `<error>` (an exception) is `failed`, one with
|
|
123
|
+
* `<skipped>` is `not-run`, any other is `passed`. Durations are seconds converted to milliseconds. Properties become labels, `<system-out>`/`<system-err>` become log
|
|
124
|
+
* attachments, and a Jenkins `[[ATTACHMENT|path]]` line in the output attaches that file by `path`. `recordedAtMs`
|
|
125
|
+
* is the newest `<testsuite timestamp>` (a timestamp with no offset is read as UTC). Throws
|
|
126
|
+
* `Not a JUnit report: no <testsuite> found` when the text has no test cases and no `<testsuite`.
|
|
127
|
+
*/
|
|
128
|
+
export function checksFromJunit(xml) {
|
|
129
|
+
const cases = parseJunit(xml);
|
|
130
|
+
if (cases.length === 0 && !/<testsuite/.test(xml))
|
|
131
|
+
throw new Error('Not a JUnit report: no <testsuite> found');
|
|
132
|
+
const checks = cases.map((item) => {
|
|
133
|
+
const where = item.file || item.classname;
|
|
134
|
+
// Jenkins' convention: a line `[[ATTACHMENT|/path/to/file]]` in a test's output attaches that file.
|
|
135
|
+
const jenkins = [...`${item.stdout ?? ''}\n${item.stderr ?? ''}`.matchAll(/\[\[ATTACHMENT\|([^\]]+)\]\]/g)].map(match => ({ name: path.basename(match[1]), path: match[1] }));
|
|
136
|
+
const attachments = [
|
|
137
|
+
...(item.stdout ? [{ name: 'stdout', role: 'log', contentType: 'text/plain', text: item.stdout }] : []),
|
|
138
|
+
...(item.stderr ? [{ name: 'stderr', role: 'log', contentType: 'text/plain', text: item.stderr }] : []),
|
|
139
|
+
...jenkins
|
|
140
|
+
];
|
|
141
|
+
return {
|
|
142
|
+
id: [where, item.name].filter(Boolean).join(' > '),
|
|
143
|
+
title: item.name,
|
|
144
|
+
status: item.status === 'passed' ? 'passed' : item.status === 'failed' ? 'failed' : 'not-run',
|
|
145
|
+
detail: item.failure ?? '',
|
|
146
|
+
...(item.file ? { specPath: specPath(item.file) } : {}),
|
|
147
|
+
measurements: [],
|
|
148
|
+
...(item.time === null ? {} : { durationMs: Math.round(item.time * 1000) }),
|
|
149
|
+
...(item.properties ? { labels: item.properties } : {}),
|
|
150
|
+
...(attachments.length ? { attachments } : {})
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
const hints = cases.map(item => [item.classname, item.suite, item.file].filter(Boolean));
|
|
154
|
+
const stamps = [...xml.matchAll(/<testsuite\b[^>]*\stimestamp="([^"]+)"/g)].map(match => parseTimestamp(match[1])).filter(Number.isFinite);
|
|
155
|
+
return { checks: uniqueChecks(checks), hints, recordedAtMs: stamps.length ? Math.max(...stamps) : null, tool: null };
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* An Atlas run (`manifest.json`): one check per path. A path's key, which the same start and
|
|
159
|
+
* events give in every run, is its id. A flaky path passed. While the run is still going, a
|
|
160
|
+
* running path is `running` and a queued one `not-run`, so a snapshot never reads as finished.
|
|
161
|
+
*
|
|
162
|
+
* Every check's `specPath` is the run's root chart file under its spec directory. A run is still going when
|
|
163
|
+
* `run.finishedAt` is `null`. `recordedAtMs` is `run.finishedAt`, else `run.startedAt` plus `run.durationMs`.
|
|
164
|
+
* Throws `Not an Atlas manifest: run and paths are required` when `paths` or `run.id` is missing.
|
|
165
|
+
*
|
|
166
|
+
* @param manifest The parsed `manifest.json` of an Atlas run.
|
|
167
|
+
*/
|
|
168
|
+
export function checksFromManifest(manifest) {
|
|
169
|
+
const root = record(manifest);
|
|
170
|
+
const run = record(root.run);
|
|
171
|
+
if (!Array.isArray(root.paths) || !run.id)
|
|
172
|
+
throw new Error('Not an Atlas manifest: run and paths are required');
|
|
173
|
+
const charts = Array.isArray(root.charts) ? root.charts.map(record) : [];
|
|
174
|
+
const chart = charts.find(item => item.parentState == null) ?? charts[0];
|
|
175
|
+
const directory = text(run.specDirectory).replace(/\/+$/, '');
|
|
176
|
+
const file = text(chart?.file);
|
|
177
|
+
const path = file ? specPath(directory ? `${directory}/${file}` : file) : null;
|
|
178
|
+
const live = run.finishedAt === null;
|
|
179
|
+
const checks = root.paths.map((raw) => {
|
|
180
|
+
const item = record(raw);
|
|
181
|
+
const status = text(item.status);
|
|
182
|
+
const attempts = Array.isArray(item.attempts) ? item.attempts.map(record) : [];
|
|
183
|
+
const progress = text(item.progress);
|
|
184
|
+
const mapped = live && progress === 'running' ? 'running' : live && progress === 'queued' ? 'not-run' : status === 'passed' || status === 'flaky' ? 'passed' : status === 'failed' ? 'failed' : 'not-run';
|
|
185
|
+
const duration = finite(item.durationMs);
|
|
186
|
+
const steps = Array.isArray(item.steps) ? item.steps.map(raw => {
|
|
187
|
+
const step = record(raw);
|
|
188
|
+
const state = text(step.status);
|
|
189
|
+
return { name: text(step.event) || 'step', kind: 'event', status: state === 'passed' || state === 'flaky' ? 'passed' : state === 'failed' ? 'failed' : 'not-reached', ...(text(step.reason) ? { detail: text(step.reason) } : {}), ...(text(step.transition) ? { extensions: { 'atlas.transition': text(step.transition) } } : {}) };
|
|
190
|
+
}) : [];
|
|
191
|
+
const journeys = Array.isArray(item.journeys) ? item.journeys.map(text).filter(Boolean) : [];
|
|
192
|
+
return {
|
|
193
|
+
id: text(item.key) || `${text(item.kind)}: ${text(item.name)}`,
|
|
194
|
+
title: text(item.name) || text(item.id),
|
|
195
|
+
status: mapped,
|
|
196
|
+
detail: mapped === 'failed' ? text(attempts.at(-1)?.error) || 'failed' : status === 'flaky' ? 'Passed on retry (flaky)' : '',
|
|
197
|
+
...(path ? { specPath: path } : {}),
|
|
198
|
+
measurements: [],
|
|
199
|
+
...(duration === null ? {} : { durationMs: duration }),
|
|
200
|
+
...(steps.length ? { steps } : {}),
|
|
201
|
+
...(text(item.kind) ? { labels: { kind: text(item.kind), ...(text(item.seed) ? { seed: text(item.seed) } : {}) } } : {}),
|
|
202
|
+
...(journeys.length ? { tags: journeys } : {})
|
|
203
|
+
};
|
|
204
|
+
});
|
|
205
|
+
const startedAt = Date.parse(text(run.startedAt));
|
|
206
|
+
const finishedAt = Date.parse(text(run.finishedAt));
|
|
207
|
+
const duration = finite(run.durationMs) ?? 0;
|
|
208
|
+
const at = Number.isFinite(finishedAt) ? finishedAt : Number.isFinite(startedAt) ? startedAt + duration : null;
|
|
209
|
+
return { checks: uniqueChecks(checks), recordedAtMs: at, tool: 'atlas' };
|
|
210
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { InitPlanSchema } from '../../schema/src/cli-results.js';
|
|
2
|
+
import type { z } from 'zod/v4';
|
|
3
|
+
/** The document `atlas init` prints, and what {@link planInit} returns. */
|
|
4
|
+
export type InitPlan = z.infer<typeof InitPlanSchema>;
|
|
5
|
+
/** What `planInit` is told: where the project is, which server, and what to do with the plan. */
|
|
6
|
+
export type InitOptions = {
|
|
7
|
+
/** The project directory. Default: the current directory. */
|
|
8
|
+
readonly directory: string;
|
|
9
|
+
/** The Atlas server clients will send results to (`https://atlas.example.com`). Falls back to `ATLAS_STORE` when that is a server. */
|
|
10
|
+
readonly server?: string;
|
|
11
|
+
/** Write the files of the plan. Without it nothing is changed. */
|
|
12
|
+
readonly write?: boolean;
|
|
13
|
+
/** Ask the server who we are and whether this token may upload. */
|
|
14
|
+
readonly verify?: boolean;
|
|
15
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
16
|
+
/** For tests: git as a function from arguments to output (or null). Default: the `git` command. */
|
|
17
|
+
readonly git?: (args: readonly string[], cwd: string) => string | null;
|
|
18
|
+
readonly fetch?: typeof fetch;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Detects the project in `options.directory` and returns the plan, writing its files when `options.write` is set.
|
|
22
|
+
* Never throws for a project it cannot understand: it says so in `warnings` and plans what it can (the `AGENTS.md`
|
|
23
|
+
* section with no suites, so the next agent knows to add them). Throws only for a directory that does not exist.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* const plan = await planInit({ directory: process.cwd(), server: 'https://atlas.example.com' });
|
|
27
|
+
* for (const suite of plan.suites) console.log(suite.run, '→', suite.publish);
|
|
28
|
+
*/
|
|
29
|
+
export declare function planInit(options: InitOptions): Promise<InitPlan>;
|
|
30
|
+
/** The plan as the text a person reads: what was found, what is planned, and what to do next. */
|
|
31
|
+
export declare function formatPlan(plan: InitPlan): string;
|
|
32
|
+
/** The usage of `atlas init`, printed by `atlas init --help`. */
|
|
33
|
+
export declare const INIT_USAGE = "atlas init \u2014 integrate a project with an Atlas server\n\n atlas init [--dir <project>] [--server <url>] [--write] [--verify] [--format json]\n\n Looks at the project (package manager, test runners, spec files, CI) and prints a plan: for each suite the command\n that runs it and the command that sends its results, a CI workflow, and an AGENTS.md section that tells the next\n agent how to use Atlas here. Nothing is changed until --write; running it again changes nothing more.\n\n --dir <path> the project (default: the current directory)\n --server <url> the Atlas server results go to (default: an http(s) store in ATLAS_STORE)\n --write create or update the files of the plan (AGENTS.md between its atlas markers, .github/workflows/atlas.yml)\n --verify ask the server who this token is and whether it may upload (token: ATLAS_TOKEN)\n --format json print the plan as one JSON document (schema: init-plan.schema.json)\n\n Exit codes: 0 planned (and verified), 1 --verify failed, 2 the command was wrong.\n";
|
|
34
|
+
/** `atlas init`: parses the arguments, plans, prints the plan as text or JSON, and returns the exit code. */
|
|
35
|
+
export declare function initMain(argv: readonly string[], io: {
|
|
36
|
+
readonly out: (text: string) => void;
|
|
37
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
38
|
+
readonly cwd?: string;
|
|
39
|
+
readonly fetch?: typeof fetch;
|
|
40
|
+
}): Promise<number>;
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Project detection and `atlas init`
|
|
3
|
+
*
|
|
4
|
+
* Looks at a project (its package manager, test runners, spec files, CI and git state) and produces a plan to send its
|
|
5
|
+
* test results to an Atlas server: for each suite the command that runs it and the command that publishes (or streams)
|
|
6
|
+
* its results, a CI workflow, and a section of `AGENTS.md` that tells the next agent how to use Atlas in this project.
|
|
7
|
+
* It changes nothing unless asked (`--write`), and applying it twice changes nothing the second time: files are
|
|
8
|
+
* rewritten only between their `atlas:begin` / `atlas:end` markers (or created whole), never merged by guesswork.
|
|
9
|
+
*
|
|
10
|
+
* The plan is one JSON document (`InitPlanSchema`) so an agent can run `atlas init --format json`, read it, and act. See
|
|
11
|
+
* docs/agents.md for the whole flow. Detection is deliberately conservative: it names what it found and why
|
|
12
|
+
* (`evidence`), says what it could not tell, and never invents a suite.
|
|
13
|
+
*/
|
|
14
|
+
import { execFileSync } from 'node:child_process';
|
|
15
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
16
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { InitPlanSchema } from '../../schema/src/cli-results.js';
|
|
19
|
+
import { collectSpecFiles, specKindOf } from './directory-outline.js';
|
|
20
|
+
import { httpStore } from './http-store.js';
|
|
21
|
+
import { serverToken } from './store-url.js';
|
|
22
|
+
const BEGIN = '<!-- atlas:begin -->';
|
|
23
|
+
const END = '<!-- atlas:end -->';
|
|
24
|
+
function defaultGit(args, cwd) {
|
|
25
|
+
try {
|
|
26
|
+
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || null;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function readJson(file) {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** The package manager a project uses, from its lockfile (or `packageManager` in package.json). */
|
|
41
|
+
function packageManagerOf(directory, pkg) {
|
|
42
|
+
if (existsSync(path.join(directory, 'bun.lock')) || existsSync(path.join(directory, 'bun.lockb')))
|
|
43
|
+
return 'bun';
|
|
44
|
+
if (existsSync(path.join(directory, 'pnpm-lock.yaml')))
|
|
45
|
+
return 'pnpm';
|
|
46
|
+
if (existsSync(path.join(directory, 'yarn.lock')))
|
|
47
|
+
return 'yarn';
|
|
48
|
+
if (existsSync(path.join(directory, 'package-lock.json')))
|
|
49
|
+
return 'npm';
|
|
50
|
+
const declared = typeof pkg?.packageManager === 'string' ? pkg.packageManager.split('@')[0] : null;
|
|
51
|
+
return declared === 'bun' || declared === 'pnpm' || declared === 'yarn' || declared === 'npm' ? declared : pkg ? 'npm' : null;
|
|
52
|
+
}
|
|
53
|
+
/** How the package manager runs a package's binary without installing it: `bunx`, `pnpm dlx`, `yarn dlx`, `npx`. */
|
|
54
|
+
const EXEC = { bun: 'bunx', pnpm: 'pnpm dlx', yarn: 'yarn dlx', npm: 'npx' };
|
|
55
|
+
const INSTALL_DEV = { bun: 'bun add -d', pnpm: 'pnpm add -D', yarn: 'yarn add -D', npm: 'npm install -D' };
|
|
56
|
+
/** The spec files of a project by kind, and the folder to pass as `--specs`. */
|
|
57
|
+
async function specsOf(directory) {
|
|
58
|
+
const candidates = ['specs', 'spec', 'features', 'docs/specs', 'test/features', 'tests/features'];
|
|
59
|
+
let best = null;
|
|
60
|
+
for (const candidate of candidates) {
|
|
61
|
+
const full = path.join(directory, candidate);
|
|
62
|
+
if (!existsSync(full) || !statSync(full).isDirectory())
|
|
63
|
+
continue;
|
|
64
|
+
const files = await collectSpecFiles(full).catch(() => []);
|
|
65
|
+
const kinds = {};
|
|
66
|
+
// Prose and data files are specs only because they sit in a specs folder: count the notations that mean something.
|
|
67
|
+
for (const file of files) {
|
|
68
|
+
const kind = specKindOf(file.path);
|
|
69
|
+
if (['gherkin', 'quint', 'dmn', 'alloy', 'openslo', 'scxml'].includes(kind))
|
|
70
|
+
kinds[kind] = (kinds[kind] ?? 0) + 1;
|
|
71
|
+
}
|
|
72
|
+
const total = Object.values(kinds).reduce((sum, count) => sum + count, 0);
|
|
73
|
+
if (total > (best?.total ?? 0))
|
|
74
|
+
best = { dir: candidate, kinds, total };
|
|
75
|
+
}
|
|
76
|
+
return best ? { directory: best.dir, kinds: best.kinds } : { directory: null, kinds: {} };
|
|
77
|
+
}
|
|
78
|
+
/** The test runners a project declares, with where each was seen. Order: the more specific first. */
|
|
79
|
+
function runnersOf(directory, pkg) {
|
|
80
|
+
const found = [];
|
|
81
|
+
const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
|
|
82
|
+
const scripts = Object.values(pkg?.scripts ?? {}).join('\n');
|
|
83
|
+
const dep = (name, runner) => { if (deps[name])
|
|
84
|
+
found.push({ runner, evidence: `package.json dependencies: ${name}` }); };
|
|
85
|
+
dep('vitest', 'vitest');
|
|
86
|
+
dep('@playwright/test', 'playwright');
|
|
87
|
+
dep('@cucumber/cucumber', 'cucumber');
|
|
88
|
+
dep('cypress', 'cypress');
|
|
89
|
+
dep('jest', 'jest');
|
|
90
|
+
dep('mocha', 'mocha');
|
|
91
|
+
if (!found.some(item => item.runner === 'vitest' || item.runner === 'jest' || item.runner === 'mocha') && /\bbun test\b/.test(scripts))
|
|
92
|
+
found.push({ runner: 'bun-test', evidence: 'package.json scripts: bun test' });
|
|
93
|
+
else if (!found.length && pkg && (existsSync(path.join(directory, 'bun.lock')) || existsSync(path.join(directory, 'bunfig.toml'))) && readdirSafe(directory).some(name => /\.(test|spec)\.[cm]?[tj]sx?$/.test(name) || name === 'tests' || name === 'test'))
|
|
94
|
+
found.push({ runner: 'bun-test', evidence: 'bun.lock and test files' });
|
|
95
|
+
const python = ['pyproject.toml', 'pytest.ini', 'tox.ini', 'setup.cfg', 'requirements.txt', 'requirements-dev.txt'].map(name => path.join(directory, name)).filter(existsSync);
|
|
96
|
+
for (const file of python)
|
|
97
|
+
if (/pytest/.test(readFileSync(file, 'utf8'))) {
|
|
98
|
+
found.push({ runner: 'pytest', evidence: `${path.basename(file)} mentions pytest` });
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
if (existsSync(path.join(directory, 'go.mod')))
|
|
102
|
+
found.push({ runner: 'go-test', evidence: 'go.mod' });
|
|
103
|
+
if (existsSync(path.join(directory, 'atlas.config.ts')) || existsSync(path.join(directory, 'atlas.config.js')))
|
|
104
|
+
found.push({ runner: 'atlas-statecharts', evidence: 'atlas.config.ts' });
|
|
105
|
+
return found;
|
|
106
|
+
}
|
|
107
|
+
function readdirSafe(directory) {
|
|
108
|
+
try {
|
|
109
|
+
return readdirSync(directory);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** The suite for a detected runner: how to run it so it writes a JUnit file (or the run manifest), and how to publish it. */
|
|
116
|
+
function suiteOf(detected, context) {
|
|
117
|
+
const specs = context.specs ? ` --specs ${context.specs}` : '';
|
|
118
|
+
const publishCommand = (producer, input, environment) => `${context.exec} @crvouga/atlas report publish --producer ${producer} ${input}${specs}${environment ? ` --environment ${environment}` : ''}`;
|
|
119
|
+
const make = (id, output, run, environment, prerequisites = [], format = 'junit', stream = null) => ({
|
|
120
|
+
id, runner: detected.runner, evidence: detected.evidence, run, output, format, environment,
|
|
121
|
+
publish: publishCommand(id, format === 'manifest' ? `--manifest ${output} --upload-run` : `--${format} ${output}`, environment), stream, prerequisites
|
|
122
|
+
});
|
|
123
|
+
const out = (id) => `atlas-${id}.xml`;
|
|
124
|
+
switch (detected.runner) {
|
|
125
|
+
case 'vitest': return make('vitest', out('vitest'), `${context.exec} vitest run --reporter=default --reporter=junit --outputFile.junit=${out('vitest')}`, null);
|
|
126
|
+
case 'jest': return make('jest', out('jest'), `JEST_JUNIT_OUTPUT_FILE=${out('jest')} ${context.exec} jest --reporters=default --reporters=jest-junit`, null, [`${context.install} jest-junit`]);
|
|
127
|
+
case 'playwright': return make('playwright', out('playwright'), `PLAYWRIGHT_JUNIT_OUTPUT_NAME=${out('playwright')} ${context.exec} playwright test --reporter=list,junit`, 'desktop-web');
|
|
128
|
+
case 'cypress': return make('cypress', out('cypress'), `${context.exec} cypress run --reporter junit --reporter-options mochaFile=${out('cypress')}`, 'desktop-web');
|
|
129
|
+
case 'cucumber': return make('cucumber', out('cucumber'), `${context.exec} cucumber-js --format summary --format junit:${out('cucumber')}`, null);
|
|
130
|
+
case 'mocha': return make('mocha', out('mocha'), `MOCHA_FILE=${out('mocha')} ${context.exec} mocha --reporter mocha-junit-reporter`, null, [`${context.install} mocha-junit-reporter`]);
|
|
131
|
+
case 'bun-test': return make('bun-test', out('bun'), `bun test --reporter=junit --reporter-outfile=${out('bun')}`, null);
|
|
132
|
+
case 'pytest': return make('pytest', out('pytest'), `pytest --junitxml=${out('pytest')}`, null);
|
|
133
|
+
case 'go-test': return make('go-test', out('go'), `go test -v ./... 2>&1 | go-junit-report > ${out('go')}`, null, ['go install github.com/jstemmer/go-junit-report/v2@latest']);
|
|
134
|
+
default: return make('statecharts', 'atlas-runs/latest/manifest.json', `${context.exec} @crvouga/atlas run --mode fast`, null, [], 'manifest');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** A suite that does not set its own exit-code handling must still publish: a failure is evidence. */
|
|
138
|
+
function githubWorkflow(suites, context) {
|
|
139
|
+
const setup = context.manager === 'bun'
|
|
140
|
+
? ' - uses: oven-sh/setup-bun@v2\n - run: bun install --frozen-lockfile\n'
|
|
141
|
+
: ` - uses: actions/setup-node@v4\n with: { node-version: 22 }\n - run: ${context.manager === 'pnpm' ? 'corepack enable && pnpm install --frozen-lockfile' : context.manager === 'yarn' ? 'corepack enable && yarn install --immutable' : 'npm ci'}\n`;
|
|
142
|
+
const exec = EXEC[context.manager ?? 'npm'];
|
|
143
|
+
const jobs = suites.map(suite => ` ${suite.id.replace(/[^a-z0-9]+/g, '-')}:
|
|
144
|
+
runs-on: ubuntu-latest
|
|
145
|
+
steps:
|
|
146
|
+
- uses: actions/checkout@v4
|
|
147
|
+
${setup} # A failing suite must still publish: a failure is evidence. The job's own result is the gate's, below.
|
|
148
|
+
- run: ${suite.run}
|
|
149
|
+
continue-on-error: true
|
|
150
|
+
- run: ${suite.publish}
|
|
151
|
+
if: always()
|
|
152
|
+
`).join('\n');
|
|
153
|
+
return `# Sends every suite's results to the project's Atlas server and gates on the combined report.
|
|
154
|
+
# Generated by \`atlas init\`; safe to edit. Needs the repository variable ATLAS_URL (https://atlas.example.com) and the
|
|
155
|
+
# secrets ATLAS_PUBLISH_TOKEN (a \`publish\` token) and ATLAS_READ_TOKEN (a \`read\` token) of that server.
|
|
156
|
+
name: atlas
|
|
157
|
+
on: [push, pull_request]
|
|
158
|
+
permissions:
|
|
159
|
+
contents: read
|
|
160
|
+
env:
|
|
161
|
+
ATLAS_STORE: \${{ vars.ATLAS_URL }}
|
|
162
|
+
ATLAS_TOKEN: \${{ secrets.ATLAS_PUBLISH_TOKEN }}
|
|
163
|
+
jobs:
|
|
164
|
+
${jobs}
|
|
165
|
+
gate:
|
|
166
|
+
needs: [${suites.map(suite => suite.id.replace(/[^a-z0-9]+/g, '-')).join(', ')}]
|
|
167
|
+
if: always()
|
|
168
|
+
runs-on: ubuntu-latest
|
|
169
|
+
env:
|
|
170
|
+
ATLAS_TOKEN: \${{ secrets.ATLAS_READ_TOKEN }}
|
|
171
|
+
steps:
|
|
172
|
+
- uses: actions/checkout@v4
|
|
173
|
+
- uses: oven-sh/setup-bun@v2
|
|
174
|
+
# This branch laid over ${context.defaultBranch}: what this run did not re-run is inherited from ${context.defaultBranch}'s latest
|
|
175
|
+
# results, and a laptop's work in progress or another branch's results cannot turn this red.
|
|
176
|
+
- run: bunx @crvouga/atlas report status --branch "\${GITHUB_HEAD_REF:-$GITHUB_REF_NAME}" --branch ${context.defaultBranch} --ignore-modified${context.specs ? ' --require-all-specs' : ''}
|
|
177
|
+
`;
|
|
178
|
+
}
|
|
179
|
+
/** The `AGENTS.md` section: what Atlas is in this project and the commands to use, replaced between its markers. */
|
|
180
|
+
function agentsSection(plan) {
|
|
181
|
+
const url = plan.server.url ?? '<the project\'s Atlas server>';
|
|
182
|
+
const lines = [
|
|
183
|
+
BEGIN,
|
|
184
|
+
'## Atlas: test evidence for this project',
|
|
185
|
+
'',
|
|
186
|
+
`This project sends the results of its test suites to an [Atlas](https://github.com/crvouga/atlas) server (${url}), which shows everyone one combined, live report. A check is identified by a stable id; the newest completed result of each check wins; unknown is never a pass. Set \`ATLAS_STORE=${url}\` and \`ATLAS_TOKEN\` (a \`publish\` token for sending, a \`read\` token for asking).`,
|
|
187
|
+
'',
|
|
188
|
+
'- **Machine-readable API:** `' + `${url}/api/openapi.json` + '` (OpenAPI 3.1). **Guide for agents:** `' + `${url}/llms.txt` + '`.',
|
|
189
|
+
'- **After you change behaviour,** run the suite, then send its results (publish even when it fails: a failure is evidence).',
|
|
190
|
+
...plan.suites.flatMap(suite => ['', ` - **${suite.id}**: \`${suite.run}\``, ` then \`${suite.publish}\`` + (suite.stream ? ` (or stream: \`${suite.stream}\`)` : '')]),
|
|
191
|
+
'',
|
|
192
|
+
`- **Is it passing?** \`bunx @crvouga/atlas report status --branch ${plan.git.defaultBranch} --ignore-modified --format json\` (exit 0 passes, 1 does not, 2 means the command was wrong). The failing checks are in \`failing\`.`,
|
|
193
|
+
plan.specs.directory ? `- **Specs** live in \`${plan.specs.directory}/\` (${Object.entries(plan.specs.kinds).map(([kind, count]) => `${count} ${kind}`).join(', ')}). Results are laid over them by path.` : '- No spec files were found. Pass `--spec <path>` or `--specs <dir>` to `publish` to link checks to specs.',
|
|
194
|
+
'- Keep each check\'s `id` stable across runs: that is what lets a new result replace an old one.',
|
|
195
|
+
'- Evidence (screenshots, videos, traces, logs) rides on checks: https://github.com/crvouga/atlas/blob/main/docs/extending.md',
|
|
196
|
+
END
|
|
197
|
+
];
|
|
198
|
+
return `${lines.join('\n')}\n`;
|
|
199
|
+
}
|
|
200
|
+
/** The new text of a file that holds a marked section: the section replaced in place, or appended, or the file created. */
|
|
201
|
+
function withSection(current, section) {
|
|
202
|
+
if (current === null)
|
|
203
|
+
return section;
|
|
204
|
+
const start = current.indexOf(BEGIN);
|
|
205
|
+
const end = current.indexOf(END);
|
|
206
|
+
if (start >= 0 && end > start)
|
|
207
|
+
return `${current.slice(0, start)}${section.trimEnd()}${current.slice(end + END.length)}`;
|
|
208
|
+
return `${current.trimEnd()}\n\n${section}`;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Detects the project in `options.directory` and returns the plan, writing its files when `options.write` is set.
|
|
212
|
+
* Never throws for a project it cannot understand: it says so in `warnings` and plans what it can (the `AGENTS.md`
|
|
213
|
+
* section with no suites, so the next agent knows to add them). Throws only for a directory that does not exist.
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* const plan = await planInit({ directory: process.cwd(), server: 'https://atlas.example.com' });
|
|
217
|
+
* for (const suite of plan.suites) console.log(suite.run, '→', suite.publish);
|
|
218
|
+
*/
|
|
219
|
+
export async function planInit(options) {
|
|
220
|
+
const directory = path.resolve(options.directory);
|
|
221
|
+
if (!existsSync(directory) || !statSync(directory).isDirectory())
|
|
222
|
+
throw new Error(`Not a directory: ${options.directory}`);
|
|
223
|
+
const env = options.env ?? {};
|
|
224
|
+
const git = options.git ?? defaultGit;
|
|
225
|
+
const pkg = readJson(path.join(directory, 'package.json'));
|
|
226
|
+
const manager = packageManagerOf(directory, pkg);
|
|
227
|
+
const specs = await specsOf(directory);
|
|
228
|
+
const configured = env.ATLAS_STORE?.split(/[\n,]+/).map(item => item.trim()).find(item => /^https?:\/\//i.test(item));
|
|
229
|
+
const serverUrl = (options.server ?? configured ?? null)?.replace(/\/+$/, '') ?? null;
|
|
230
|
+
const warnings = [];
|
|
231
|
+
const exec = EXEC[manager ?? 'npm'];
|
|
232
|
+
const context = { exec, install: INSTALL_DEV[manager ?? 'npm'], specs: specs.directory, server: serverUrl };
|
|
233
|
+
const suites = runnersOf(directory, pkg).map(found => suiteOf(found, context));
|
|
234
|
+
if (!suites.length)
|
|
235
|
+
warnings.push('No test runner was recognised (looked for vitest, jest, playwright, cypress, cucumber, mocha, bun test, pytest, go test and Atlas statecharts). Add one suite by hand: run it so it writes a JUnit XML or CTRF file, then `atlas report publish --producer <name> --junit <file>`.');
|
|
236
|
+
if (!serverUrl)
|
|
237
|
+
warnings.push('No server given. Pass --server https://… (or set ATLAS_STORE) so the plan can name it; until then the generated text says "the project\'s Atlas server".');
|
|
238
|
+
if (!specs.directory)
|
|
239
|
+
warnings.push('No spec files (Gherkin, Quint, DMN, Alloy, OpenSLO, SCXML) were found in specs/, features/ or the usual places. Results are still reported; they belong to no spec until `--specs <dir>` or `--spec <path>` links them.');
|
|
240
|
+
if (existsSync(path.join(directory, 'atlas.config.ts')) && suites.some(suite => suite.runner === 'atlas-statecharts'))
|
|
241
|
+
warnings.push('atlas.config.ts found: the statechart suite runs the whole plan (`atlas run --mode fast`); see docs/statecharts.md to choose modes and shards.');
|
|
242
|
+
const branch = git(['branch', '--show-current'], directory);
|
|
243
|
+
const remoteHead = git(['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], directory)?.replace(/^origin\//, '') ?? null;
|
|
244
|
+
const defaultBranch = remoteHead ?? (git(['rev-parse', '--verify', 'main'], directory) ? 'main' : git(['rev-parse', '--verify', 'master'], directory) ? 'master' : 'main');
|
|
245
|
+
const ci = existsSync(path.join(directory, '.github')) || /github\.com/.test(git(['remote', 'get-url', 'origin'], directory) ?? '') ? 'github-actions' : existsSync(path.join(directory, '.gitlab-ci.yml')) ? 'gitlab-ci' : existsSync(path.join(directory, '.circleci')) ? 'circleci' : 'none';
|
|
246
|
+
if (ci === 'gitlab-ci' || ci === 'circleci')
|
|
247
|
+
warnings.push(`${ci} detected: no workflow is generated for it. In its jobs run each suite, then its publish command (with ATLAS_STORE and ATLAS_TOKEN set from secrets), then the gate command from AGENTS.md.`);
|
|
248
|
+
const base = { suites, specs, server: { url: serverUrl, tokenEnv: 'ATLAS_TOKEN' }, git: { branch, defaultBranch } };
|
|
249
|
+
const wanted = [
|
|
250
|
+
{ path: 'AGENTS.md', purpose: 'Tells every agent working in this repository how to send and read test evidence.', next: current => withSection(current, agentsSection(base)) }
|
|
251
|
+
];
|
|
252
|
+
if (ci === 'github-actions' && suites.length)
|
|
253
|
+
wanted.push({ path: '.github/workflows/atlas.yml', purpose: 'Runs each suite in CI, sends its results to the server even when it fails, and gates on the combined report.', next: () => githubWorkflow(suites, { manager, defaultBranch, specs: specs.directory }) });
|
|
254
|
+
const files = [];
|
|
255
|
+
for (const item of wanted) {
|
|
256
|
+
const full = path.join(directory, item.path);
|
|
257
|
+
const current = existsSync(full) ? await readFile(full, 'utf8') : null;
|
|
258
|
+
const content = item.next(current);
|
|
259
|
+
// A workflow that exists and was not generated by this tool is the project's own: never overwritten.
|
|
260
|
+
const foreign = item.path.startsWith('.github/') && current !== null && !current.startsWith('# Sends every suite');
|
|
261
|
+
if (foreign) {
|
|
262
|
+
warnings.push(`${item.path} already exists and was not generated by atlas init; left alone. The generated version is in this plan's files.`);
|
|
263
|
+
files.push({ path: item.path, action: 'unchanged', purpose: item.purpose, content });
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
files.push({ path: item.path, action: current === null ? 'create' : current === content ? 'unchanged' : 'update', purpose: item.purpose, content });
|
|
267
|
+
}
|
|
268
|
+
const written = [];
|
|
269
|
+
if (options.write) {
|
|
270
|
+
for (const file of files) {
|
|
271
|
+
if (file.action === 'unchanged')
|
|
272
|
+
continue;
|
|
273
|
+
await mkdir(path.dirname(path.join(directory, file.path)), { recursive: true });
|
|
274
|
+
await writeFile(path.join(directory, file.path), file.content);
|
|
275
|
+
written.push(file.path);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
let verification = null;
|
|
279
|
+
if (options.verify && serverUrl) {
|
|
280
|
+
const token = serverToken(env);
|
|
281
|
+
const answer = await httpStore({ url: serverUrl, ...(token ? { token } : {}), retries: 1, ...(options.fetch ? { fetch: options.fetch } : {}) }).ping();
|
|
282
|
+
verification = { reachable: answer.ok || /\b(401|403)\b|wants a token|may not do that/.test(answer.error ?? ''), role: answer.role ?? null, canPublish: answer.role === 'publish' || answer.role === 'admin', error: answer.ok ? null : (answer.error ?? 'no answer') };
|
|
283
|
+
if (answer.ok && !verification.canPublish)
|
|
284
|
+
verification = { ...verification, error: `This token's role is "${answer.role}": it can look but not upload. Use a publish token for sending results.` };
|
|
285
|
+
}
|
|
286
|
+
else if (options.verify)
|
|
287
|
+
warnings.push('--verify needs a server: pass --server or set ATLAS_STORE.');
|
|
288
|
+
const nextSteps = [
|
|
289
|
+
...(serverUrl ? [] : ['Decide which server to send results to (run one with `atlas serve`), then run this again with --server <url>.']),
|
|
290
|
+
...(options.write ? [] : ['Review the plan, then run again with --write to create the files in `files`.']),
|
|
291
|
+
...suites.flatMap(suite => suite.prerequisites.map(command => `Install what ${suite.id} needs: ${command}`)),
|
|
292
|
+
...(serverUrl ? [`Set ATLAS_STORE=${serverUrl} and ATLAS_TOKEN (a publish token) in CI and on each machine that should send results.`, 'Run `atlas report doctor` to prove the server can be written to and read.'] : []),
|
|
293
|
+
...suites.map(suite => `Prove ${suite.id}: run \`${suite.run}\`, then \`${suite.publish}\`, then open ${serverUrl ?? 'the server'} and find its results.`)
|
|
294
|
+
];
|
|
295
|
+
const ok = verification ? verification.reachable && verification.canPublish && verification.error === null : true;
|
|
296
|
+
return InitPlanSchema.parse({ ok, directory, project: { name: typeof pkg?.name === 'string' ? pkg.name : path.basename(directory), packageManager: manager, languages: languagesOf(directory, pkg) }, git: base.git, ci, server: base.server, specs, suites, files, written, verification, nextSteps, warnings });
|
|
297
|
+
}
|
|
298
|
+
function languagesOf(directory, pkg) {
|
|
299
|
+
const languages = [];
|
|
300
|
+
if (pkg)
|
|
301
|
+
languages.push(existsSync(path.join(directory, 'tsconfig.json')) ? 'typescript' : 'javascript');
|
|
302
|
+
if (['pyproject.toml', 'requirements.txt', 'setup.py'].some(name => existsSync(path.join(directory, name))))
|
|
303
|
+
languages.push('python');
|
|
304
|
+
if (existsSync(path.join(directory, 'go.mod')))
|
|
305
|
+
languages.push('go');
|
|
306
|
+
return languages;
|
|
307
|
+
}
|
|
308
|
+
/** The plan as the text a person reads: what was found, what is planned, and what to do next. */
|
|
309
|
+
export function formatPlan(plan) {
|
|
310
|
+
const lines = [`Project ${plan.project.name ?? plan.directory}${plan.project.packageManager ? ` (${plan.project.packageManager})` : ''}, CI: ${plan.ci}, server: ${plan.server.url ?? 'not given'}`];
|
|
311
|
+
lines.push(plan.suites.length ? 'Suites:' : 'Suites: none recognised');
|
|
312
|
+
for (const suite of plan.suites)
|
|
313
|
+
lines.push(` ${suite.id} (${suite.evidence})`, ` run: ${suite.run}`, ` publish: ${suite.publish}`);
|
|
314
|
+
lines.push(plan.specs.directory ? `Specs: ${plan.specs.directory}/ (${Object.entries(plan.specs.kinds).map(([kind, count]) => `${count} ${kind}`).join(', ')})` : 'Specs: none found');
|
|
315
|
+
lines.push('Files:');
|
|
316
|
+
for (const file of plan.files)
|
|
317
|
+
lines.push(` ${plan.written.includes(file.path) ? 'wrote ' : file.action.padEnd(9)} ${file.path} ${file.action === 'unchanged' ? '' : `- ${file.purpose}`}`);
|
|
318
|
+
if (plan.verification)
|
|
319
|
+
lines.push(`Server: ${plan.verification.reachable ? 'reachable' : 'NOT reachable'}${plan.verification.role ? `, token role ${plan.verification.role}` : ''}${plan.verification.error ? ` — ${plan.verification.error}` : ''}`);
|
|
320
|
+
for (const warning of plan.warnings)
|
|
321
|
+
lines.push(`! ${warning}`);
|
|
322
|
+
lines.push('Next:');
|
|
323
|
+
plan.nextSteps.forEach((step, i) => lines.push(` ${i + 1}. ${step}`));
|
|
324
|
+
return `${lines.join('\n')}\n`;
|
|
325
|
+
}
|
|
326
|
+
/** The usage of `atlas init`, printed by `atlas init --help`. */
|
|
327
|
+
export const INIT_USAGE = `atlas init — integrate a project with an Atlas server
|
|
328
|
+
|
|
329
|
+
atlas init [--dir <project>] [--server <url>] [--write] [--verify] [--format json]
|
|
330
|
+
|
|
331
|
+
Looks at the project (package manager, test runners, spec files, CI) and prints a plan: for each suite the command
|
|
332
|
+
that runs it and the command that sends its results, a CI workflow, and an AGENTS.md section that tells the next
|
|
333
|
+
agent how to use Atlas here. Nothing is changed until --write; running it again changes nothing more.
|
|
334
|
+
|
|
335
|
+
--dir <path> the project (default: the current directory)
|
|
336
|
+
--server <url> the Atlas server results go to (default: an http(s) store in ATLAS_STORE)
|
|
337
|
+
--write create or update the files of the plan (AGENTS.md between its atlas markers, .github/workflows/atlas.yml)
|
|
338
|
+
--verify ask the server who this token is and whether it may upload (token: ATLAS_TOKEN)
|
|
339
|
+
--format json print the plan as one JSON document (schema: init-plan.schema.json)
|
|
340
|
+
|
|
341
|
+
Exit codes: 0 planned (and verified), 1 --verify failed, 2 the command was wrong.
|
|
342
|
+
`;
|
|
343
|
+
/** `atlas init`: parses the arguments, plans, prints the plan as text or JSON, and returns the exit code. */
|
|
344
|
+
export async function initMain(argv, io) {
|
|
345
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
346
|
+
io.out(INIT_USAGE);
|
|
347
|
+
return 0;
|
|
348
|
+
}
|
|
349
|
+
const flag = (name) => { const i = argv.indexOf(`--${name}`); return i >= 0 ? argv[i + 1] : undefined; };
|
|
350
|
+
const format = flag('format');
|
|
351
|
+
const json = format === 'json' || argv.includes('--json');
|
|
352
|
+
try {
|
|
353
|
+
if (format !== undefined && format !== 'json' && format !== 'text')
|
|
354
|
+
throw new Error(`--format takes text|json (got "${format}")`);
|
|
355
|
+
const plan = await planInit({ directory: path.resolve(io.cwd ?? process.cwd(), flag('dir') ?? '.'), ...(flag('server') ? { server: flag('server') } : {}), write: argv.includes('--write'), verify: argv.includes('--verify'), env: io.env ?? process.env, ...(io.fetch ? { fetch: io.fetch } : {}) });
|
|
356
|
+
io.out(json ? `${JSON.stringify(plan, null, 2)}\n` : formatPlan(plan));
|
|
357
|
+
return plan.ok ? 0 : 1;
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
361
|
+
process.stderr.write(json ? `${JSON.stringify({ ok: false, error: { message, code: 'usage' } })}\n` : `atlas init: ${message}\n`);
|
|
362
|
+
return 2;
|
|
363
|
+
}
|
|
364
|
+
}
|