@repo-os/repoos 0.5.45
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/LICENSE.md +110 -0
- package/README.md +272 -0
- package/dist/.build-info.json +4 -0
- package/dist/.build-stamp.json +3 -0
- package/dist/cli/colors.d.ts +13 -0
- package/dist/cli/colors.js +37 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +221 -0
- package/dist/commands/check.d.ts +170 -0
- package/dist/commands/check.js +1227 -0
- package/dist/commands/docs.d.ts +2 -0
- package/dist/commands/docs.js +70 -0
- package/dist/commands/gc.d.ts +1 -0
- package/dist/commands/gc.js +57 -0
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.js +699 -0
- package/dist/commands/screenshots.d.ts +7 -0
- package/dist/commands/screenshots.js +77 -0
- package/dist/commands/serve.d.ts +55 -0
- package/dist/commands/serve.js +204 -0
- package/dist/commands/status.d.ts +140 -0
- package/dist/commands/status.js +569 -0
- package/dist/commands/stop.d.ts +1 -0
- package/dist/commands/stop.js +59 -0
- package/dist/commands/tasks.d.ts +47 -0
- package/dist/commands/tasks.js +334 -0
- package/dist/commands/tunnel.d.ts +1 -0
- package/dist/commands/tunnel.js +1271 -0
- package/dist/commands/ui-harness.d.ts +53 -0
- package/dist/commands/ui-harness.js +62 -0
- package/dist/commands/ui-smoke.d.ts +6 -0
- package/dist/commands/ui-smoke.js +220 -0
- package/dist/commands/upgrade.d.ts +15 -0
- package/dist/commands/upgrade.js +188 -0
- package/dist/core/auth-store.d.ts +81 -0
- package/dist/core/auth-store.js +530 -0
- package/dist/core/auth.d.ts +75 -0
- package/dist/core/auth.js +124 -0
- package/dist/core/bootstrap.d.ts +41 -0
- package/dist/core/bootstrap.js +236 -0
- package/dist/core/build.d.ts +92 -0
- package/dist/core/build.js +249 -0
- package/dist/core/canary.d.ts +10 -0
- package/dist/core/canary.js +14 -0
- package/dist/core/check-failure-summary.d.ts +10 -0
- package/dist/core/check-failure-summary.js +34 -0
- package/dist/core/config.d.ts +160 -0
- package/dist/core/config.js +1288 -0
- package/dist/core/context-pack.d.ts +74 -0
- package/dist/core/context-pack.js +778 -0
- package/dist/core/db.d.ts +236 -0
- package/dist/core/db.js +747 -0
- package/dist/core/detect.d.ts +69 -0
- package/dist/core/detect.js +251 -0
- package/dist/core/docs.d.ts +76 -0
- package/dist/core/docs.js +225 -0
- package/dist/core/frontmatter.d.ts +29 -0
- package/dist/core/frontmatter.js +215 -0
- package/dist/core/git.d.ts +505 -0
- package/dist/core/git.js +1453 -0
- package/dist/core/indexer.d.ts +33 -0
- package/dist/core/indexer.js +282 -0
- package/dist/core/input.d.ts +66 -0
- package/dist/core/input.js +222 -0
- package/dist/core/logger.d.ts +70 -0
- package/dist/core/logger.js +138 -0
- package/dist/core/models.d.ts +45 -0
- package/dist/core/models.js +291 -0
- package/dist/core/net-probe.d.ts +4 -0
- package/dist/core/net-probe.js +20 -0
- package/dist/core/providers/deepinfra.d.ts +23 -0
- package/dist/core/providers/deepinfra.js +44 -0
- package/dist/core/providers/index.d.ts +24 -0
- package/dist/core/providers/index.js +59 -0
- package/dist/core/providers/openrouter.d.ts +28 -0
- package/dist/core/providers/openrouter.js +82 -0
- package/dist/core/providers/spend.d.ts +86 -0
- package/dist/core/providers/spend.js +286 -0
- package/dist/core/providers/types.d.ts +56 -0
- package/dist/core/providers/types.js +39 -0
- package/dist/core/repoos.d.ts +59 -0
- package/dist/core/repoos.js +221 -0
- package/dist/core/runtime.d.ts +35 -0
- package/dist/core/runtime.js +167 -0
- package/dist/core/skills-registry.d.ts +39 -0
- package/dist/core/skills-registry.js +148 -0
- package/dist/core/skills-registry.test.d.ts +1 -0
- package/dist/core/skills-registry.test.js +52 -0
- package/dist/core/sysmem.d.ts +7 -0
- package/dist/core/sysmem.js +55 -0
- package/dist/core/tailscale.d.ts +2 -0
- package/dist/core/tailscale.js +19 -0
- package/dist/core/task.d.ts +53 -0
- package/dist/core/task.js +310 -0
- package/dist/core/toml-line.d.ts +20 -0
- package/dist/core/toml-line.js +52 -0
- package/dist/core/toml-validate.d.ts +44 -0
- package/dist/core/toml-validate.js +335 -0
- package/dist/core/tunnel-assistant.d.ts +30 -0
- package/dist/core/tunnel-assistant.js +65 -0
- package/dist/core/tunnel-registry.d.ts +96 -0
- package/dist/core/tunnel-registry.js +267 -0
- package/dist/core/tunnel-registry.test.d.ts +1 -0
- package/dist/core/tunnel-registry.test.js +309 -0
- package/dist/core/tunnel.d.ts +103 -0
- package/dist/core/tunnel.js +320 -0
- package/dist/core/types.d.ts +811 -0
- package/dist/core/types.js +27 -0
- package/dist/core/worktree-gc.d.ts +57 -0
- package/dist/core/worktree-gc.js +194 -0
- package/dist/server/__tests__/integration-job.test.d.ts +1 -0
- package/dist/server/__tests__/integration-job.test.js +129 -0
- package/dist/server/agents.d.ts +1020 -0
- package/dist/server/agents.js +4070 -0
- package/dist/server/attachments.d.ts +51 -0
- package/dist/server/attachments.js +130 -0
- package/dist/server/auto-engineering.d.ts +45 -0
- package/dist/server/auto-engineering.js +275 -0
- package/dist/server/branchless-release.d.ts +22 -0
- package/dist/server/branchless-release.js +67 -0
- package/dist/server/built-in-agents.d.ts +305 -0
- package/dist/server/built-in-agents.js +1838 -0
- package/dist/server/cto-monitor.d.ts +35 -0
- package/dist/server/cto-monitor.js +281 -0
- package/dist/server/cto.d.ts +125 -0
- package/dist/server/cto.js +590 -0
- package/dist/server/deployments.d.ts +119 -0
- package/dist/server/deployments.js +325 -0
- package/dist/server/diff-snapshot.d.ts +20 -0
- package/dist/server/diff-snapshot.js +73 -0
- package/dist/server/done.d.ts +160 -0
- package/dist/server/done.js +488 -0
- package/dist/server/freeform.d.ts +55 -0
- package/dist/server/freeform.js +179 -0
- package/dist/server/handoff.d.ts +99 -0
- package/dist/server/handoff.js +575 -0
- package/dist/server/hetzner.d.ts +45 -0
- package/dist/server/hetzner.js +117 -0
- package/dist/server/icons.d.ts +8 -0
- package/dist/server/icons.js +195 -0
- package/dist/server/integration-job.d.ts +75 -0
- package/dist/server/integration-job.js +144 -0
- package/dist/server/integration-orchestrator.d.ts +211 -0
- package/dist/server/integration-orchestrator.js +1299 -0
- package/dist/server/integration-status.d.ts +48 -0
- package/dist/server/integration-status.js +71 -0
- package/dist/server/live-index.d.ts +295 -0
- package/dist/server/live-index.js +452 -0
- package/dist/server/model-test.d.ts +20 -0
- package/dist/server/model-test.js +100 -0
- package/dist/server/ntfy.d.ts +57 -0
- package/dist/server/ntfy.js +112 -0
- package/dist/server/pm-attachments.d.ts +35 -0
- package/dist/server/pm-attachments.js +181 -0
- package/dist/server/pm-runs.d.ts +58 -0
- package/dist/server/pm-runs.js +80 -0
- package/dist/server/preview.d.ts +174 -0
- package/dist/server/preview.js +641 -0
- package/dist/server/release.d.ts +88 -0
- package/dist/server/release.js +449 -0
- package/dist/server/reload.d.ts +194 -0
- package/dist/server/reload.js +586 -0
- package/dist/server/remote-validation.d.ts +132 -0
- package/dist/server/remote-validation.js +478 -0
- package/dist/server/repo-lock.d.ts +36 -0
- package/dist/server/repo-lock.js +141 -0
- package/dist/server/review-guard.d.ts +14 -0
- package/dist/server/review-guard.js +159 -0
- package/dist/server/review.d.ts +243 -0
- package/dist/server/review.js +1211 -0
- package/dist/server/routes/agents.d.ts +6 -0
- package/dist/server/routes/agents.js +28 -0
- package/dist/server/routes/auth.d.ts +60 -0
- package/dist/server/routes/auth.js +755 -0
- package/dist/server/routes/config.d.ts +20 -0
- package/dist/server/routes/config.js +347 -0
- package/dist/server/routes/debugger.d.ts +33 -0
- package/dist/server/routes/debugger.js +360 -0
- package/dist/server/routes/deployments.d.ts +3 -0
- package/dist/server/routes/deployments.js +16 -0
- package/dist/server/routes/docs.d.ts +8 -0
- package/dist/server/routes/docs.js +135 -0
- package/dist/server/routes/helpers.d.ts +21 -0
- package/dist/server/routes/helpers.js +171 -0
- package/dist/server/routes/index.d.ts +19 -0
- package/dist/server/routes/index.js +18 -0
- package/dist/server/routes/info.d.ts +17 -0
- package/dist/server/routes/info.js +177 -0
- package/dist/server/routes/inputs.d.ts +7 -0
- package/dist/server/routes/inputs.js +125 -0
- package/dist/server/routes/model-providers.d.ts +27 -0
- package/dist/server/routes/model-providers.js +94 -0
- package/dist/server/routes/models.d.ts +3 -0
- package/dist/server/routes/models.js +36 -0
- package/dist/server/routes/notify.d.ts +2 -0
- package/dist/server/routes/notify.js +17 -0
- package/dist/server/routes/playground.d.ts +29 -0
- package/dist/server/routes/playground.js +81 -0
- package/dist/server/routes/release.d.ts +20 -0
- package/dist/server/routes/release.js +86 -0
- package/dist/server/routes/router.d.ts +12 -0
- package/dist/server/routes/router.js +32 -0
- package/dist/server/routes/skill-registry.d.ts +5 -0
- package/dist/server/routes/skill-registry.js +51 -0
- package/dist/server/routes/tasks.d.ts +58 -0
- package/dist/server/routes/tasks.js +1442 -0
- package/dist/server/routes/transcribe.d.ts +2 -0
- package/dist/server/routes/transcribe.js +124 -0
- package/dist/server/routes/types.d.ts +68 -0
- package/dist/server/routes/types.js +1 -0
- package/dist/server/routes/ui.d.ts +5 -0
- package/dist/server/routes/ui.js +81 -0
- package/dist/server/routes/utils.d.ts +3 -0
- package/dist/server/routes/utils.js +23 -0
- package/dist/server/serve-reaper.d.ts +135 -0
- package/dist/server/serve-reaper.js +491 -0
- package/dist/server/server.d.ts +49 -0
- package/dist/server/server.js +2171 -0
- package/dist/server/supervisor.d.ts +89 -0
- package/dist/server/supervisor.js +231 -0
- package/dist/server/system.d.ts +198 -0
- package/dist/server/system.js +480 -0
- package/dist/server/task-check.d.ts +45 -0
- package/dist/server/task-check.js +71 -0
- package/dist/server/task-transitions.d.ts +51 -0
- package/dist/server/task-transitions.js +78 -0
- package/dist/server/task-watchdog.d.ts +158 -0
- package/dist/server/task-watchdog.js +681 -0
- package/dist/server/test-run.d.ts +28 -0
- package/dist/server/test-run.js +81 -0
- package/dist/server/watcher.d.ts +21 -0
- package/dist/server/watcher.js +194 -0
- package/dist/server/write.d.ts +94 -0
- package/dist/server/write.js +345 -0
- package/dist/ui/assets/AgentsView-CF4wRvHh.css +1 -0
- package/dist/ui/assets/AgentsView-D-myL-kA.js +1 -0
- package/dist/ui/assets/ContextView-D3TC-RSp.css +1 -0
- package/dist/ui/assets/ContextView-DsOqAdyE.js +4 -0
- package/dist/ui/assets/DashboardView-BOhsIJqO.js +1 -0
- package/dist/ui/assets/DashboardView-Ck9o0RP2.css +1 -0
- package/dist/ui/assets/DeploymentsView--iIUVfEH.css +1 -0
- package/dist/ui/assets/DeploymentsView-DXu_FYR2.js +1 -0
- package/dist/ui/assets/InputsView-24HVbVPk.js +1 -0
- package/dist/ui/assets/InputsView-BLb7GHtx.css +1 -0
- package/dist/ui/assets/LoginView-6QlGg6cx.css +1 -0
- package/dist/ui/assets/LoginView-BqupI0Lt.js +1 -0
- package/dist/ui/assets/ReleasesView-BEG1A0YH.css +1 -0
- package/dist/ui/assets/ReleasesView-CAgMuxpe.js +3 -0
- package/dist/ui/assets/SettingsView-CA-NYSMK.css +1 -0
- package/dist/ui/assets/SettingsView-O3iZSnjg.js +3 -0
- package/dist/ui/assets/WorkView-D8Tl6qwy.js +1 -0
- package/dist/ui/assets/WorkView-DMYL2s3W.css +1 -0
- package/dist/ui/assets/abnfDiagram-VCTEODGH-J5lkSnEE.js +1 -0
- package/dist/ui/assets/arc-BHZnmhhM.js +1 -0
- package/dist/ui/assets/architecture-7GRP2DOG-pLdFt-xS.js +1 -0
- package/dist/ui/assets/architectureDiagram-5GKGNRK7-C3k89uKX.js +36 -0
- package/dist/ui/assets/array-BifhSqXX.js +1 -0
- package/dist/ui/assets/blockDiagram-I7D4REHJ-AWNoBzop.js +129 -0
- package/dist/ui/assets/button-lbikqSph.js +1 -0
- package/dist/ui/assets/c4Diagram-7LVT6UL2-DKG1D5gd.js +38 -0
- package/dist/ui/assets/channel-gA4qBv3A.js +1 -0
- package/dist/ui/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/dist/ui/assets/chunk-4HAMMTFA-D1YR8lF_.js +62 -0
- package/dist/ui/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/dist/ui/assets/chunk-75Z2AOVW-5GuBxfsL.js +2 -0
- package/dist/ui/assets/chunk-DU6HZSFF-DfdNNqwa.js +127 -0
- package/dist/ui/assets/chunk-F27PBJKO-BPZzinQ4.js +1 -0
- package/dist/ui/assets/chunk-FOHPRMQF-l2z4K2jD.js +161 -0
- package/dist/ui/assets/chunk-GMAD6QVW-DGWvSKj5.js +72 -0
- package/dist/ui/assets/chunk-GVQU2GXP-D6Aw75wJ.js +1 -0
- package/dist/ui/assets/chunk-IMKFNOWR-B8mMBKCW.js +231 -0
- package/dist/ui/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/dist/ui/assets/chunk-L3NEJ4N5-D5OJuXYE.js +1 -0
- package/dist/ui/assets/chunk-OSK3NFVY-B7r9w2UW.js +10 -0
- package/dist/ui/assets/chunk-P2QGCYS3-C74X4AQX.js +1 -0
- package/dist/ui/assets/chunk-POPQ4Y6H-C5_u1gUS.js +1 -0
- package/dist/ui/assets/chunk-PWAF6VOD-DfoTLtxn.js +1 -0
- package/dist/ui/assets/chunk-SHT3W25Y-D40m2eqz.js +168 -0
- package/dist/ui/assets/chunk-SVP7TREG-euxi0Kap.js +88 -0
- package/dist/ui/assets/chunk-TICWLB2K-fkdxngzN.js +206 -0
- package/dist/ui/assets/chunk-XXDRQBXY-DFBUG-OT.js +1 -0
- package/dist/ui/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/dist/ui/assets/classDiagram-ZZMXUADV-BzW8bq2P.js +1 -0
- package/dist/ui/assets/classDiagram-v2-VYDZK3BY-BzW8bq2P.js +1 -0
- package/dist/ui/assets/cose-bilkent-JH36ORCC-BTcgMon1.js +1 -0
- package/dist/ui/assets/cynefin-OW5HDTMX-B5m_COsS.js +1 -0
- package/dist/ui/assets/cynefinDiagram-5FMLGOSQ-C-AG6CGe.js +62 -0
- package/dist/ui/assets/cytoscape.esm-FBntH-9t.js +321 -0
- package/dist/ui/assets/dagre-GXQ25YYZ-DDfDnp8L.js +4 -0
- package/dist/ui/assets/dagre-PrKaheQc.js +1 -0
- package/dist/ui/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/dist/ui/assets/diagram-S7CK7UJ4-DjQZAlEY.js +30 -0
- package/dist/ui/assets/diagram-UQ7AKVKN-DAZ9yp1B.js +41 -0
- package/dist/ui/assets/diagram-VSXAHHWV-BWepUKJD.js +3 -0
- package/dist/ui/assets/diagram-VX7I27RA-EcV1J81b.js +24 -0
- package/dist/ui/assets/diagram-Z3DM3KII-Dbz7XU_K.js +24 -0
- package/dist/ui/assets/dist-DTg6UBE_.js +1 -0
- package/dist/ui/assets/ebnfDiagram-PWID7BFC-BUyQw8Mm.js +1 -0
- package/dist/ui/assets/erDiagram-RLTQ6QDP-WVf8ukKt.js +99 -0
- package/dist/ui/assets/eventmodeling-NTZA5JFV-DHqyLOAn.js +1 -0
- package/dist/ui/assets/flowDiagram-HODETNUW-CYk_ze5K.js +1 -0
- package/dist/ui/assets/ganttDiagram-EL5Y4UJY-rB4MZuB-.js +292 -0
- package/dist/ui/assets/gitGraph-4MIJSDKK-BOTWM8dY.js +1 -0
- package/dist/ui/assets/gitGraphDiagram-WWUBYQGX-eYscuhtP.js +106 -0
- package/dist/ui/assets/graphlib-DS17s2tU.js +1 -0
- package/dist/ui/assets/index-BACBfnga.css +1 -0
- package/dist/ui/assets/index-BOQ-ust6.js +26 -0
- package/dist/ui/assets/info-A6RAGUB7-_67wOORe.js +1 -0
- package/dist/ui/assets/infoDiagram-27XIBGKW-XVXbddTA.js +2 -0
- package/dist/ui/assets/init-C-OQMol4.js +1 -0
- package/dist/ui/assets/ishikawaDiagram-5VMMS53U-ClLNiEru.js +70 -0
- package/dist/ui/assets/journeyDiagram-3NMN7TZE-NRawjR6i.js +139 -0
- package/dist/ui/assets/kanban-definition-UXKFOSKX-BgSzBfIx.js +89 -0
- package/dist/ui/assets/katex-DolUETbr.js +257 -0
- package/dist/ui/assets/line-KAyfuul4.js +1 -0
- package/dist/ui/assets/linear-cCCPOpu7.js +1 -0
- package/dist/ui/assets/mermaid-parser.core-CgXZk1iN.js +7 -0
- package/dist/ui/assets/mermaid.core-BUUkChHW.js +44 -0
- package/dist/ui/assets/mindmap-definition-YA3MSWOX-Cpn7vW4r.js +96 -0
- package/dist/ui/assets/ordinal-BDEzSJ7C.js +1 -0
- package/dist/ui/assets/packet-AYTQ26CC-0lZwM-d6.js +1 -0
- package/dist/ui/assets/path-COt_16Va.js +1 -0
- package/dist/ui/assets/pegDiagram-XKGWAZYB-impy0inn.js +1 -0
- package/dist/ui/assets/pie-WAS4IAKB-CqfdGHYg.js +1 -0
- package/dist/ui/assets/pieDiagram-E7YTZNPT-CxjL4I1h.js +39 -0
- package/dist/ui/assets/quadrantDiagram-AXDQQJYC-Cjd1QMIe.js +7 -0
- package/dist/ui/assets/radar-RG4KPBEZ-CRkQIwiv.js +1 -0
- package/dist/ui/assets/railroad-74A4TZTK-i67XSWTB.js +1 -0
- package/dist/ui/assets/railroad-abnf-HS5TGJTU-Cv9m5aS3.js +1 -0
- package/dist/ui/assets/railroad-ebnf-LZEXJU2U-C5ZIn-Jh.js +1 -0
- package/dist/ui/assets/railroad-peg-WCYAUIDC-CGra9Oq-.js +1 -0
- package/dist/ui/assets/railroadDiagram-O6MQD6OU-_pK0qyaY.js +1 -0
- package/dist/ui/assets/releases-B0WNV-SY.js +1 -0
- package/dist/ui/assets/repoos-cto-square.webp +0 -0
- package/dist/ui/assets/repoos-helpful-pm-square.webp +0 -0
- package/dist/ui/assets/repoos-orchestrator-square.webp +0 -0
- package/dist/ui/assets/repoos-performance-agent-square.webp +0 -0
- package/dist/ui/assets/repoos-ross-from-friends-square.webp +0 -0
- package/dist/ui/assets/repoos-tech-debt-agent-square.webp +0 -0
- package/dist/ui/assets/requirementDiagram-BXWQKSXE-CRrFX-yw.js +84 -0
- package/dist/ui/assets/rough.esm-By172zw-.js +1 -0
- package/dist/ui/assets/sankeyDiagram-P5KCCOFB-X_xFRj9E.js +40 -0
- package/dist/ui/assets/sequenceDiagram-WJ2MYXX4-Byb49lKf.js +162 -0
- package/dist/ui/assets/sizeCapture-INFHLROL-B0uUizjq.js +1 -0
- package/dist/ui/assets/src-BH-TyZbA.js +1 -0
- package/dist/ui/assets/stateDiagram-D77RDMKH-CzaPrqYF.js +1 -0
- package/dist/ui/assets/stateDiagram-v2-MP3YSRHH-BCXckz7K.js +1 -0
- package/dist/ui/assets/swimlanes-42K2YHIH-Cx_1lnqr.js +1 -0
- package/dist/ui/assets/swimlanesDiagram-VR7AAH4N-54qsKi6A.js +8 -0
- package/dist/ui/assets/timeline-definition-24CTP7MA-B1IbZPQ-.js +120 -0
- package/dist/ui/assets/treeView-Q6P3EWNA-DFWhVE3d.js +1 -0
- package/dist/ui/assets/treemap-WGGIJYW6-BmbYPThn.js +1 -0
- package/dist/ui/assets/vendor-ui-DPm7zLxi.js +3 -0
- package/dist/ui/assets/vendor-vue-CobgyEJp.js +1 -0
- package/dist/ui/assets/vennDiagram-4TSXK5OY-Ae2AtRpt.js +34 -0
- package/dist/ui/assets/wardley-WFR3VGLG-BtD4X4tL.js +1 -0
- package/dist/ui/assets/wardleyDiagram-VM6X3IG4-B3YN2fTd.js +78 -0
- package/dist/ui/assets/xychartDiagram-S5SC5T6Z-CIzJfMKA.js +7 -0
- package/dist/ui/favicon.svg +11 -0
- package/dist/ui/index.html +49 -0
- package/dist/ui/sw.js +48 -0
- package/package.json +91 -0
|
@@ -0,0 +1,1227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `repoos check` — the definition-of-done gate.
|
|
3
|
+
*
|
|
4
|
+
* Runs, in sequence: build staleness check, formatting & lint guard
|
|
5
|
+
* (fmt:check + lint, since nothing else enforces them — deliberately BEFORE
|
|
6
|
+
* the build: both are pure source-level checks with no build dependency, and
|
|
7
|
+
* running them first means a formatting fix never has to pay for a second
|
|
8
|
+
* full build in the same `repoos check` invocation — see the "Formatting &
|
|
9
|
+
* lint guard" comment below), full build (tsc + asset copy), CSS layering
|
|
10
|
+
* guard, theme contrast guard (button-gradient validity + WCAG contrast on
|
|
11
|
+
* every theme's fg/bg token pairs), test suite (if present), and a headless
|
|
12
|
+
* browser smoke test. The UI smoke step is per-project and opt-in (#0348): a
|
|
13
|
+
* project declares a command via a `smoke` package.json script or `[check]
|
|
14
|
+
* uiSmoke` in repoos.toml, and skips cleanly when it declares neither. RepoOS
|
|
15
|
+
* itself opts in the same way (its `smoke` script runs src/commands/ui-smoke.ts).
|
|
16
|
+
* The stylesheet guards are likewise opt-in (#0351): `[check] uiStylesheet`
|
|
17
|
+
* plus a `themeScopes`/`contrastPairs`/`gradientTokens` vocabulary, so neither
|
|
18
|
+
* guard carries a RepoOS-only path or token name.
|
|
19
|
+
*
|
|
20
|
+
* Exits non-zero on any failure. Designed for CI gates and agent pre-review.
|
|
21
|
+
*/
|
|
22
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
23
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
24
|
+
import { cpus, totalmem } from "node:os";
|
|
25
|
+
import { isAbsolute, join, relative, sep } from "node:path";
|
|
26
|
+
import { c } from "../cli/colors.js";
|
|
27
|
+
import { checkBuildForRoot } from "../core/build.js";
|
|
28
|
+
import { findRepoRoot, loadConfig } from "../core/config.js";
|
|
29
|
+
import { availableMemBytes } from "../core/sysmem.js";
|
|
30
|
+
import { preferBunForDevTasks } from "../core/runtime.js";
|
|
31
|
+
function pass(name, detail) {
|
|
32
|
+
return { name, ok: true, detail };
|
|
33
|
+
}
|
|
34
|
+
function fail(name, detail) {
|
|
35
|
+
return { name, ok: false, detail };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Split a CSS selector list on top-level commas (respecting parentheses,
|
|
39
|
+
* so `:not(.a,.b)` stays one group).
|
|
40
|
+
*/
|
|
41
|
+
function selectorGroups(sel) {
|
|
42
|
+
const parts = [];
|
|
43
|
+
let depth = 0;
|
|
44
|
+
let cur = "";
|
|
45
|
+
for (const ch of sel) {
|
|
46
|
+
if (ch === "(") {
|
|
47
|
+
depth++;
|
|
48
|
+
cur += ch;
|
|
49
|
+
}
|
|
50
|
+
else if (ch === ")") {
|
|
51
|
+
depth--;
|
|
52
|
+
cur += ch;
|
|
53
|
+
}
|
|
54
|
+
else if (ch === "," && depth === 0) {
|
|
55
|
+
parts.push(cur);
|
|
56
|
+
cur = "";
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
cur += ch;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
parts.push(cur);
|
|
63
|
+
return parts.map((p) => p.trim()).filter(Boolean);
|
|
64
|
+
}
|
|
65
|
+
/** True for a selector that targets every element of a kind: `*` or a bare tag. */
|
|
66
|
+
function isBroadSelector(g) {
|
|
67
|
+
if (g === "*")
|
|
68
|
+
return true;
|
|
69
|
+
return /^[a-z][a-z0-9-]*([\s.#:\[>~+]|$)/.test(g);
|
|
70
|
+
}
|
|
71
|
+
/** Recursively list `.ts` files under `dir`, skipping node_modules/dist/dotdirs. */
|
|
72
|
+
function walkTsFiles(dir, acc = []) {
|
|
73
|
+
let entries;
|
|
74
|
+
try {
|
|
75
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return acc;
|
|
79
|
+
}
|
|
80
|
+
for (const e of entries) {
|
|
81
|
+
if (e.name.startsWith(".") || e.name === "node_modules" || e.name === "dist")
|
|
82
|
+
continue;
|
|
83
|
+
const full = join(dir, e.name);
|
|
84
|
+
if (e.isDirectory())
|
|
85
|
+
walkTsFiles(full, acc);
|
|
86
|
+
else if (e.name.endsWith(".ts"))
|
|
87
|
+
acc.push(full);
|
|
88
|
+
}
|
|
89
|
+
return acc;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Bare `require(...)` calls guard (#0271 follow-up): this package is
|
|
93
|
+
* `"type": "module"` (ESM) — a bare, un-shadowed `require` is undefined at
|
|
94
|
+
* runtime in the compiled `dist/` output, throwing `ReferenceError`.
|
|
95
|
+
* Confirmed live: `integration-job.ts`'s `removeJob()` used bare
|
|
96
|
+
* `require("fs")` inside a try/catch that silently swallowed the
|
|
97
|
+
* `ReferenceError`, so a moot close-out job was never actually deleted from
|
|
98
|
+
* disk — it kept getting re-picked-up and re-processed in a tight infinite
|
|
99
|
+
* loop, live, in production (task #0258), until someone noticed and asked
|
|
100
|
+
* why it was stuck.
|
|
101
|
+
*
|
|
102
|
+
* vitest transpiles TypeScript on the fly with its own module loader, which
|
|
103
|
+
* DOES provide a working `require()` even in these ESM-flagged files — so
|
|
104
|
+
* this bug is invisible to every unit test that imports the affected module,
|
|
105
|
+
* no matter how thorough. Only the actual compiled `dist/` output, run under
|
|
106
|
+
* Node's real ESM semantics, exposes it. This static, textual guard is the
|
|
107
|
+
* only check in the whole gate that runs against the real failure mode.
|
|
108
|
+
*
|
|
109
|
+
* `createRequire(...)` (a real, valid way to get a working `require` in ESM
|
|
110
|
+
* — see `ui-harness.ts`) shadows the global; a file that uses it anywhere is
|
|
111
|
+
* exempted file-wide rather than precisely scoping which calls are shadowed
|
|
112
|
+
* and which aren't — simpler, and a false negative here just means a
|
|
113
|
+
* legitimately-shadowed file's OTHER bare-require bugs (if any) go
|
|
114
|
+
* uncaught, never that a broken bare require ships silently.
|
|
115
|
+
*/
|
|
116
|
+
export function bareRequireOffenders(roots, opts = {}) {
|
|
117
|
+
const offenders = [];
|
|
118
|
+
const repoRoot = opts.repoRoot ?? ".";
|
|
119
|
+
const excludes = (opts.excludes ?? []).map(normalizeGuardDir).filter(Boolean);
|
|
120
|
+
for (const root of roots) {
|
|
121
|
+
const rootRel = root === "." ? "." : normalizeGuardDir(root);
|
|
122
|
+
if (!rootRel)
|
|
123
|
+
continue;
|
|
124
|
+
const absRoot = rootRel === "." ? repoRoot : join(repoRoot, rootRel);
|
|
125
|
+
for (const absPath of walkTsFiles(absRoot)) {
|
|
126
|
+
const relPath = relative(repoRoot, absPath).split(sep).join("/");
|
|
127
|
+
if (relPath.endsWith(".test.ts"))
|
|
128
|
+
continue; // vitest supplies its own require shim
|
|
129
|
+
if (isExcludedPath(relPath, excludes))
|
|
130
|
+
continue;
|
|
131
|
+
let content;
|
|
132
|
+
try {
|
|
133
|
+
content = readFileSync(absPath, "utf8");
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (content.includes("createRequire"))
|
|
139
|
+
continue; // legitimately shadowed file-wide
|
|
140
|
+
content.split("\n").forEach((line, i) => {
|
|
141
|
+
const trimmed = line.trim();
|
|
142
|
+
if (trimmed.startsWith("*") || trimmed.startsWith("//"))
|
|
143
|
+
return;
|
|
144
|
+
if (/\brequire\(/.test(line))
|
|
145
|
+
offenders.push(`${relPath}:${i + 1}`);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return offenders;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Strip `//` and `/* */` comments and trailing commas from a tsconfig so it
|
|
153
|
+
* parses as JSON (tsconfig.json is JSONC, which `JSON.parse` rejects). Both
|
|
154
|
+
* transformations are string-aware: a `//` or `,]` inside a double-quoted
|
|
155
|
+
* string value is left untouched, and only a comma whose next significant
|
|
156
|
+
* token is `}` or `]` is dropped.
|
|
157
|
+
*/
|
|
158
|
+
function stripJsonComments(text) {
|
|
159
|
+
let out = "";
|
|
160
|
+
let i = 0;
|
|
161
|
+
let inStr = false;
|
|
162
|
+
while (i < text.length) {
|
|
163
|
+
const ch = text[i];
|
|
164
|
+
if (inStr) {
|
|
165
|
+
out += ch;
|
|
166
|
+
if (ch === "\\") {
|
|
167
|
+
out += text[i + 1] ?? "";
|
|
168
|
+
i += 2;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (ch === '"')
|
|
172
|
+
inStr = false;
|
|
173
|
+
i++;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (ch === '"') {
|
|
177
|
+
inStr = true;
|
|
178
|
+
out += ch;
|
|
179
|
+
i++;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (ch === "/" && text[i + 1] === "/") {
|
|
183
|
+
while (i < text.length && text[i] !== "\n")
|
|
184
|
+
i++;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (ch === "/" && text[i + 1] === "*") {
|
|
188
|
+
i += 2;
|
|
189
|
+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/"))
|
|
190
|
+
i++;
|
|
191
|
+
i += 2;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (ch === ",") {
|
|
195
|
+
const next = nextSignificantChar(text, i + 1);
|
|
196
|
+
if (next === "}" || next === "]") {
|
|
197
|
+
i++; // trailing comma
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
out += ch;
|
|
202
|
+
i++;
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
/** The next non-whitespace, non-comment character at or after `from`, or null. */
|
|
207
|
+
function nextSignificantChar(text, from) {
|
|
208
|
+
let i = from;
|
|
209
|
+
while (i < text.length) {
|
|
210
|
+
const ch = text[i];
|
|
211
|
+
if (/\s/.test(ch)) {
|
|
212
|
+
i++;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (ch === "/" && text[i + 1] === "/") {
|
|
216
|
+
while (i < text.length && text[i] !== "\n")
|
|
217
|
+
i++;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (ch === "/" && text[i + 1] === "*") {
|
|
221
|
+
i += 2;
|
|
222
|
+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/"))
|
|
223
|
+
i++;
|
|
224
|
+
i += 2;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
return ch;
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
/** Parse a `tsconfig.json` (JSONC) and read the fields the guard needs, or null. */
|
|
232
|
+
export function readTsconfig(repoRoot) {
|
|
233
|
+
const path = join(repoRoot, "tsconfig.json");
|
|
234
|
+
if (!existsSync(path))
|
|
235
|
+
return null;
|
|
236
|
+
try {
|
|
237
|
+
return JSON.parse(stripJsonComments(readFileSync(path, "utf8")));
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Turn one tsconfig glob (`src/**/*.ts`, `packages/app/src`, `lib/index.ts`)
|
|
245
|
+
* into a repo-relative directory to walk. Returns `"."` for a whole-repo glob
|
|
246
|
+
* (`**/*.ts`), "" for anything absolute, home-relative, or negated.
|
|
247
|
+
*/
|
|
248
|
+
function globToScanRoot(glob) {
|
|
249
|
+
let g = glob.replace(/\\/g, "/");
|
|
250
|
+
while (g.startsWith("./"))
|
|
251
|
+
g = g.slice(2);
|
|
252
|
+
if (g.startsWith("!") || g.startsWith("/") || g.startsWith("~"))
|
|
253
|
+
return "";
|
|
254
|
+
const globAt = g.search(/[*?[{]/);
|
|
255
|
+
let base = globAt === -1 ? g : g.slice(0, globAt);
|
|
256
|
+
if (/\.(ts|tsx|mts|cts)$/.test(base))
|
|
257
|
+
base = base.replace(/\/[^/]*$/, "");
|
|
258
|
+
if (base === "" || base === ".")
|
|
259
|
+
return ".";
|
|
260
|
+
return normalizeGuardDir(base);
|
|
261
|
+
}
|
|
262
|
+
function escapeRegExp(s) {
|
|
263
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
264
|
+
}
|
|
265
|
+
/** Compile a tsconfig-style glob (`src/**/*.ts`, `**/*.spec.ts`) to a regex. */
|
|
266
|
+
function globToRegExp(pattern) {
|
|
267
|
+
let re = "";
|
|
268
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
269
|
+
const ch = pattern[i];
|
|
270
|
+
if (ch === "*") {
|
|
271
|
+
if (pattern[i + 1] === "*") {
|
|
272
|
+
if (pattern[i + 2] === "/") {
|
|
273
|
+
re += "(?:.*/)?"; // `**/` — any number of leading directories
|
|
274
|
+
i += 2;
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
re += ".*";
|
|
278
|
+
i += 1;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
re += "[^/]*";
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else if (ch === "?") {
|
|
286
|
+
re += "[^/]";
|
|
287
|
+
}
|
|
288
|
+
else if (ch === "{") {
|
|
289
|
+
const end = pattern.indexOf("}", i);
|
|
290
|
+
if (end === -1) {
|
|
291
|
+
re += "\\{";
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
const alts = pattern
|
|
295
|
+
.slice(i + 1, end)
|
|
296
|
+
.split(",")
|
|
297
|
+
.map(escapeRegExp);
|
|
298
|
+
re += `(?:${alts.join("|")})`;
|
|
299
|
+
i = end;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
re += escapeRegExp(ch);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return new RegExp(`^${re}$`);
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Whether a repo-relative path is excluded. A literal path excludes that file
|
|
310
|
+
* exactly, or — for a directory — everything beneath it. A pattern containing
|
|
311
|
+
* glob metacharacters is compiled and matched against the whole path. Keeping
|
|
312
|
+
* these distinct matters: `globToScanRoot("src/legacy.ts")` would collapse a
|
|
313
|
+
* single-file exclude to `src` and wipe out the whole tree (#0352 review).
|
|
314
|
+
*/
|
|
315
|
+
function isExcludedPath(relPath, patterns) {
|
|
316
|
+
for (const p of patterns) {
|
|
317
|
+
if (!/[*?[{]/.test(p)) {
|
|
318
|
+
if (relPath === p || relPath.startsWith(`${p}/`))
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
else if (globToRegExp(p).test(relPath)) {
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Decide which source roots the bare-`require()` guard scans (#0352). Explicit
|
|
329
|
+
* `[check] bareRequireDirs` wins; otherwise the repo's tsconfig `include` (plus
|
|
330
|
+
* `files`) is mapped to directories, minus its `exclude` list. Pure so the
|
|
331
|
+
* resolution is unit-testable; if neither yields a root the guard skips rather
|
|
332
|
+
* than passing vacuously.
|
|
333
|
+
*/
|
|
334
|
+
export function resolveBareRequireRoots(configured, tsconfig, configuredExcludes) {
|
|
335
|
+
const normalizePatterns = (v) => [
|
|
336
|
+
...new Set((v ?? []).map(normalizeGuardDir).filter(Boolean)),
|
|
337
|
+
];
|
|
338
|
+
const configRoots = dedupeRoots((configured ?? []).map((d) => (d === "." ? "." : normalizeGuardDir(d))).filter(Boolean));
|
|
339
|
+
if (configRoots.length) {
|
|
340
|
+
return {
|
|
341
|
+
roots: configRoots,
|
|
342
|
+
excludes: normalizePatterns(configuredExcludes),
|
|
343
|
+
source: "config",
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
const patterns = [
|
|
347
|
+
...(Array.isArray(tsconfig?.include) ? tsconfig.include : []),
|
|
348
|
+
...(Array.isArray(tsconfig?.files) ? tsconfig.files : []),
|
|
349
|
+
].filter((v) => typeof v === "string" && v.trim() !== "");
|
|
350
|
+
const roots = dedupeRoots(patterns.map(globToScanRoot).filter(Boolean));
|
|
351
|
+
const excludes = normalizePatterns((Array.isArray(tsconfig?.exclude) ? tsconfig.exclude : []).filter((v) => typeof v === "string" && v.trim() !== ""));
|
|
352
|
+
return { roots, excludes, source: roots.length ? "tsconfig" : "none" };
|
|
353
|
+
}
|
|
354
|
+
/** Drop any root that is a descendant of another, and collapse to `["."]` if present. */
|
|
355
|
+
function dedupeRoots(roots) {
|
|
356
|
+
const set = [...new Set(roots)];
|
|
357
|
+
if (set.includes("."))
|
|
358
|
+
return ["."];
|
|
359
|
+
return set.filter((r) => !set.some((o) => o !== r && r.startsWith(`${o}/`)));
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Binary attachments (screenshots, PDFs) under the task/input folders must
|
|
363
|
+
* never enter git history: they are uploaded through the UI, written to
|
|
364
|
+
* `<dir>/.attachments/`, and served back by the running server from disk — the
|
|
365
|
+
* committed record is the task/input `.md`, not the pixels. Binaries in
|
|
366
|
+
* history bloat the repo irreversibly (this repo once carried ~250 MiB of
|
|
367
|
+
* stale screenshot churn). Product image assets — UI, icons, logos, docs —
|
|
368
|
+
* live outside those folders and stay tracked.
|
|
369
|
+
*
|
|
370
|
+
* The folder names come from `repoos.toml` (`workDir`/`inputsDir`) so a managed
|
|
371
|
+
* repo that renames them still gets the guard; they default to `work`/`inputs`.
|
|
372
|
+
*
|
|
373
|
+
* Pure (takes the tracked-file list) so it is unit-testable; the check block
|
|
374
|
+
* feeds it `git ls-files`.
|
|
375
|
+
*/
|
|
376
|
+
/**
|
|
377
|
+
* Normalize a configured guard directory (`workDir`/`inputsDir`) for prefix
|
|
378
|
+
* matching against `git ls-files` output, which is always repo-root-relative:
|
|
379
|
+
* strip any leading `./` and trailing slashes. Returns empty for anything that
|
|
380
|
+
* can't be a repo-relative directory — an explicit `""`, `"."`/`"./"`, an
|
|
381
|
+
* absolute path, a home-relative `~/…`, or a path containing a `..` segment.
|
|
382
|
+
* Those would all silently match nothing (or, for `""`, fatal git's pathspec),
|
|
383
|
+
* so callers must treat empty as unusable rather than as "match everything".
|
|
384
|
+
*/
|
|
385
|
+
export function normalizeGuardDir(d) {
|
|
386
|
+
let trimmed = d;
|
|
387
|
+
while (trimmed.startsWith("./"))
|
|
388
|
+
trimmed = trimmed.slice(2);
|
|
389
|
+
trimmed = trimmed.replace(/\/+$/, "");
|
|
390
|
+
if (trimmed === "" || trimmed === ".")
|
|
391
|
+
return "";
|
|
392
|
+
if (isAbsolute(trimmed) || trimmed.startsWith("~") || trimmed.split("/").includes("..")) {
|
|
393
|
+
return "";
|
|
394
|
+
}
|
|
395
|
+
return trimmed;
|
|
396
|
+
}
|
|
397
|
+
export function taskAssetOffenders(trackedPaths, dirs = {}) {
|
|
398
|
+
const IMG = /\.(png|jpe?g|gif|webp|avif|bmp|svg|ico|pdf)$/i;
|
|
399
|
+
const prefixes = [dirs.workDir ?? "work", dirs.inputsDir ?? "inputs"]
|
|
400
|
+
.map(normalizeGuardDir)
|
|
401
|
+
.filter(Boolean)
|
|
402
|
+
.map((d) => `${d}/`);
|
|
403
|
+
return trackedPaths.filter((p) => prefixes.some((pre) => p.startsWith(pre)) && IMG.test(p));
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Scan a stylesheet for UNLAYERED universal/bare-element selectors.
|
|
407
|
+
* With Tailwind v4 everything lives in cascade layers; an unlayered `*`
|
|
408
|
+
* or tag rule silently beats every utility (unlayered > @layer), which
|
|
409
|
+
* already collapsed all shadcn padding once. Scoped class/ID/attribute
|
|
410
|
+
* rules are intentional legacy overrides and stay allowed.
|
|
411
|
+
*/
|
|
412
|
+
export function cssLayeringOffenders(css) {
|
|
413
|
+
const out = [];
|
|
414
|
+
const src = css.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
415
|
+
const scopes = [];
|
|
416
|
+
const inLayer = () => scopes.includes("layer");
|
|
417
|
+
const inKeyframes = () => scopes.includes("keyframes");
|
|
418
|
+
src.split("\n").forEach((line, li) => {
|
|
419
|
+
let seg = "";
|
|
420
|
+
for (let i = 0; i < line.length; i++) {
|
|
421
|
+
const ch = line[i];
|
|
422
|
+
if (ch === "{") {
|
|
423
|
+
const sel = seg.trim();
|
|
424
|
+
if (sel.startsWith("@")) {
|
|
425
|
+
if (/^@keyframes\b/.test(sel))
|
|
426
|
+
scopes.push("keyframes");
|
|
427
|
+
else if (/^@layer\b/.test(sel))
|
|
428
|
+
scopes.push("layer");
|
|
429
|
+
else
|
|
430
|
+
scopes.push("");
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
if (!inLayer() && !inKeyframes()) {
|
|
434
|
+
for (const g of selectorGroups(sel)) {
|
|
435
|
+
if (isBroadSelector(g))
|
|
436
|
+
out.push(`${li + 1}: ${g}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
scopes.push("");
|
|
440
|
+
}
|
|
441
|
+
seg = "";
|
|
442
|
+
}
|
|
443
|
+
else if (ch === "}") {
|
|
444
|
+
scopes.pop();
|
|
445
|
+
seg = "";
|
|
446
|
+
}
|
|
447
|
+
else if (ch === ";") {
|
|
448
|
+
seg = "";
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
seg += ch;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
return out;
|
|
456
|
+
}
|
|
457
|
+
function heading(label) {
|
|
458
|
+
console.log(c.bold(c.cyan(`\n ◆ ${label}`)));
|
|
459
|
+
}
|
|
460
|
+
const MIN_CONTRAST = 3.0;
|
|
461
|
+
function parseThemeBlocks(css, scopeNames) {
|
|
462
|
+
const blocks = [];
|
|
463
|
+
let cur = null;
|
|
464
|
+
let buf = "";
|
|
465
|
+
for (const raw of css.split("\n")) {
|
|
466
|
+
const t = raw.trim();
|
|
467
|
+
if (cur === null) {
|
|
468
|
+
if (t.endsWith("{") && !t.startsWith("@")) {
|
|
469
|
+
const sel = t.slice(0, -1).trim();
|
|
470
|
+
cur = scopeNames.get(sel) ?? "";
|
|
471
|
+
buf = "";
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
else if (t === "}") {
|
|
475
|
+
if (cur) {
|
|
476
|
+
const decls = {};
|
|
477
|
+
for (const pair of buf.split(";")) {
|
|
478
|
+
const idx = pair.indexOf(":");
|
|
479
|
+
if (idx > 0) {
|
|
480
|
+
const k = pair.slice(0, idx).trim();
|
|
481
|
+
const v = pair.slice(idx + 1).trim();
|
|
482
|
+
if (k.startsWith("--"))
|
|
483
|
+
decls[k] = v;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
blocks.push({ variant: cur, decls });
|
|
487
|
+
}
|
|
488
|
+
cur = null;
|
|
489
|
+
}
|
|
490
|
+
else if (cur) {
|
|
491
|
+
buf += t;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return blocks;
|
|
495
|
+
}
|
|
496
|
+
function resolveVar(value, map, depth = 0) {
|
|
497
|
+
if (depth > 6)
|
|
498
|
+
return value;
|
|
499
|
+
const m = value.trim().match(/^var\(--([a-z0-9-]+)\)$/i);
|
|
500
|
+
if (!m)
|
|
501
|
+
return value;
|
|
502
|
+
const next = map[`--${m[1]}`];
|
|
503
|
+
return next !== undefined ? resolveVar(next, map, depth + 1) : value;
|
|
504
|
+
}
|
|
505
|
+
function parseColor(v) {
|
|
506
|
+
const s = v.trim().toLowerCase();
|
|
507
|
+
if (!s || s === "none")
|
|
508
|
+
return null;
|
|
509
|
+
if (s === "transparent")
|
|
510
|
+
return { r: 0, g: 0, b: 0, a: 0 };
|
|
511
|
+
if (s.startsWith("#")) {
|
|
512
|
+
const h = s.slice(1);
|
|
513
|
+
if (h.length === 3 || h.length === 4) {
|
|
514
|
+
const full = h
|
|
515
|
+
.split("")
|
|
516
|
+
.map((ch) => ch + ch)
|
|
517
|
+
.join("");
|
|
518
|
+
return parseColor("#" + full);
|
|
519
|
+
}
|
|
520
|
+
if (h.length === 6 || h.length === 8) {
|
|
521
|
+
const n = parseInt(h.slice(0, 6), 16);
|
|
522
|
+
const a = h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1;
|
|
523
|
+
return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255, a };
|
|
524
|
+
}
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
const m = s.match(/^rgba?\(([\d.]+),([\d.]+),([\d.]+)(?:,([\d.]+))?\)$/);
|
|
528
|
+
if (m) {
|
|
529
|
+
return {
|
|
530
|
+
r: parseInt(m[1], 10),
|
|
531
|
+
g: parseInt(m[2], 10),
|
|
532
|
+
b: parseInt(m[3], 10),
|
|
533
|
+
a: m[4] !== undefined ? parseFloat(m[4]) : 1,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
function composite(fg, bg) {
|
|
539
|
+
const a = fg.a;
|
|
540
|
+
return {
|
|
541
|
+
r: fg.r * a + bg.r * (1 - a),
|
|
542
|
+
g: fg.g * a + bg.g * (1 - a),
|
|
543
|
+
b: fg.b * a + bg.b * (1 - a),
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
function channelLum(ch) {
|
|
547
|
+
const c = ch / 255;
|
|
548
|
+
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
549
|
+
}
|
|
550
|
+
function luminance(c) {
|
|
551
|
+
return 0.2126 * channelLum(c.r) + 0.7152 * channelLum(c.g) + 0.0722 * channelLum(c.b);
|
|
552
|
+
}
|
|
553
|
+
function contrastRatio(a, b) {
|
|
554
|
+
const [hi, lo] = a >= b ? [a, b] : [b, a];
|
|
555
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
556
|
+
}
|
|
557
|
+
/** Resolve a token value to concrete colors (composited over `bg`). */
|
|
558
|
+
function colorCandidates(raw, map, bg) {
|
|
559
|
+
const resolved = resolveVar(raw, map);
|
|
560
|
+
const out = [];
|
|
561
|
+
if (resolved.includes("gradient(")) {
|
|
562
|
+
const re = /#[0-9a-fA-F]{3,8}|rgba?\([^)]*\)|transparent/g;
|
|
563
|
+
let m;
|
|
564
|
+
while ((m = re.exec(resolved))) {
|
|
565
|
+
const c = parseColor(m[0]);
|
|
566
|
+
if (c)
|
|
567
|
+
out.push(composite(c, bg));
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
const c = parseColor(resolved);
|
|
572
|
+
if (c)
|
|
573
|
+
out.push(composite(c, bg));
|
|
574
|
+
}
|
|
575
|
+
return out;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* An opaque backdrop to composite semi-transparent colors over before their
|
|
579
|
+
* luminance is meaningful. Prefer the configured `backdropToken` (the page
|
|
580
|
+
* background); otherwise fall back to the pair's own background token when that
|
|
581
|
+
* resolves to a solid color; only then to white. A missing backdrop never skips
|
|
582
|
+
* a pair — at worst it approximates an alpha channel.
|
|
583
|
+
*/
|
|
584
|
+
function resolveBackdrop(map, bgToken, backdropToken) {
|
|
585
|
+
const configured = backdropToken ? map[backdropToken] : undefined;
|
|
586
|
+
for (const raw of [configured, map[bgToken]]) {
|
|
587
|
+
if (raw === undefined)
|
|
588
|
+
continue;
|
|
589
|
+
const solid = parseColor(resolveVar(raw, map));
|
|
590
|
+
if (solid)
|
|
591
|
+
return composite(solid, solid);
|
|
592
|
+
}
|
|
593
|
+
return { r: 255, g: 255, b: 255 };
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* True when at least one configured scope selector opens a block in `css`.
|
|
597
|
+
* The theme-contrast step uses this to skip a stylesheet with no theme blocks
|
|
598
|
+
* rather than keying off a hardcoded RepoOS selector like `:root`.
|
|
599
|
+
*/
|
|
600
|
+
export function hasThemeBlocks(css, scopes) {
|
|
601
|
+
return parseThemeBlocks(css, new Map(scopes.map((s) => [s.selector, s.name]))).length > 0;
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Advisory config-shape warnings for declared theme scopes: a typo'd
|
|
605
|
+
* `inherits` target or a duplicated selector/name otherwise collapses
|
|
606
|
+
* silently (empty declarations / last-wins). Returned rather than thrown so
|
|
607
|
+
* `repoos check` can surface them without failing the gate on a config typo.
|
|
608
|
+
*/
|
|
609
|
+
export function themeScopeConfigWarnings(scopes) {
|
|
610
|
+
const warnings = [];
|
|
611
|
+
const names = new Set(scopes.map((s) => s.name));
|
|
612
|
+
const seenNames = new Set();
|
|
613
|
+
const seenSelectors = new Set();
|
|
614
|
+
for (const s of scopes) {
|
|
615
|
+
if (seenNames.has(s.name))
|
|
616
|
+
warnings.push(`[check] themeScopes: duplicate name "${s.name}"`);
|
|
617
|
+
seenNames.add(s.name);
|
|
618
|
+
if (seenSelectors.has(s.selector)) {
|
|
619
|
+
warnings.push(`[check] themeScopes: duplicate selector "${s.selector}"`);
|
|
620
|
+
}
|
|
621
|
+
seenSelectors.add(s.selector);
|
|
622
|
+
for (const base of s.inherits ?? []) {
|
|
623
|
+
if (!names.has(base)) {
|
|
624
|
+
warnings.push(`[check] themeScopes: "${s.name}" inherits unknown scope "${base}"`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return warnings;
|
|
629
|
+
}
|
|
630
|
+
export function themeContrastOffenders(css, config) {
|
|
631
|
+
const { scopes, pairs, gradientTokens, backdropToken } = config;
|
|
632
|
+
const blocks = parseThemeBlocks(css, new Map(scopes.map((s) => [s.selector, s.name])));
|
|
633
|
+
if (!blocks.length)
|
|
634
|
+
return [];
|
|
635
|
+
const byVariant = {};
|
|
636
|
+
for (const b of blocks)
|
|
637
|
+
byVariant[b.variant] = b;
|
|
638
|
+
const out = [];
|
|
639
|
+
for (const scope of scopes) {
|
|
640
|
+
const map = {};
|
|
641
|
+
for (const base of scope.inherits?.length ? scope.inherits : [scope.name]) {
|
|
642
|
+
Object.assign(map, byVariant[base]?.decls ?? {});
|
|
643
|
+
}
|
|
644
|
+
for (const tk of gradientTokens) {
|
|
645
|
+
const raw = map[tk];
|
|
646
|
+
if (raw !== undefined && !resolveVar(raw, map).includes("gradient(")) {
|
|
647
|
+
out.push(`${scope.name} · ${tk} must be a gradient — it is consumed via background-image`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
for (const { fg, bg: bgK } of pairs) {
|
|
651
|
+
if (map[fg] === undefined || map[bgK] === undefined)
|
|
652
|
+
continue;
|
|
653
|
+
const backdrop = resolveBackdrop(map, bgK, backdropToken);
|
|
654
|
+
const fgs = colorCandidates(map[fg], map, backdrop);
|
|
655
|
+
const bgs = colorCandidates(map[bgK], map, backdrop);
|
|
656
|
+
if (!fgs.length || !bgs.length)
|
|
657
|
+
continue;
|
|
658
|
+
let worst = Infinity;
|
|
659
|
+
for (const f of fgs)
|
|
660
|
+
for (const b of bgs) {
|
|
661
|
+
const c = contrastRatio(luminance(f), luminance(b));
|
|
662
|
+
if (c < worst)
|
|
663
|
+
worst = c;
|
|
664
|
+
}
|
|
665
|
+
if (worst < MIN_CONTRAST) {
|
|
666
|
+
out.push(`${scope.name} · ${fg} on ${bgK} → ${worst.toFixed(2)} (need ≥${MIN_CONTRAST})`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return out;
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Which git ref (if any) to scope the Tests step to, via Vitest's own
|
|
674
|
+
* `--changed <ref>` (git-diff + module graph, only runs tests that could
|
|
675
|
+
* actually be affected). Unset for a standalone `repoos check` — that path
|
|
676
|
+
* is the full definition-of-done gate and must never narrow coverage.
|
|
677
|
+
*
|
|
678
|
+
* Only the two per-branch pre-merge checks set this: the engineer's own
|
|
679
|
+
* self-check before requesting handoff, and the server's handoff-finalize
|
|
680
|
+
* re-verification (both re-checking the SAME isolated branch, just diffed
|
|
681
|
+
* against its own base). The close-out gate that validates the actual merge
|
|
682
|
+
* onto main (integration-orchestrator.ts's validateCandidate) deliberately
|
|
683
|
+
* never sets it — that check is about interaction with whatever else has
|
|
684
|
+
* landed on main since, which a per-branch diff can't see.
|
|
685
|
+
*/
|
|
686
|
+
export function changedTestRef(env) {
|
|
687
|
+
const ref = env.REPOOS_CHECK_CHANGED;
|
|
688
|
+
return ref && ref.trim() ? ref.trim() : undefined;
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* The `package.json` script name a project uses to opt into `repoos check`'s
|
|
692
|
+
* UI smoke step with no configuration — the same zero-config convention
|
|
693
|
+
* `build`/`test`/`lint` already follow elsewhere in this gate.
|
|
694
|
+
*/
|
|
695
|
+
export const UI_SMOKE_SCRIPT = "smoke";
|
|
696
|
+
/**
|
|
697
|
+
* Resolve which UI smoke command (if any) `repoos check` should run for the
|
|
698
|
+
* current project (#0348). Precedence: `repoos.toml` `[check] uiSmoke` wins,
|
|
699
|
+
* then a `smoke` package.json script, else null — the caller then skips the
|
|
700
|
+
* step cleanly. RepoOS's own repo resolves through this same path (its `smoke`
|
|
701
|
+
* script); there is no RepoOS-only fallback.
|
|
702
|
+
*/
|
|
703
|
+
export function resolveSmokeCommand(configured, pkgScripts) {
|
|
704
|
+
const fromConfig = configured?.trim();
|
|
705
|
+
if (fromConfig)
|
|
706
|
+
return { source: "config", command: fromConfig };
|
|
707
|
+
if (pkgScripts?.[UI_SMOKE_SCRIPT])
|
|
708
|
+
return { source: "script", script: UI_SMOKE_SCRIPT };
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Vitest worker-pool size for the Tests step. `vite.config.ts` pins a
|
|
713
|
+
* conservative floor (`maxWorkers: 2`) because several agent-driven check runs
|
|
714
|
+
* routinely overlap across worktrees and each pool otherwise multiplies against
|
|
715
|
+
* the others. But a solo run on an idle machine with memory to spare leaves
|
|
716
|
+
* most of the box idle — the subprocess-heavy suites (agent-review,
|
|
717
|
+
* done-reliability, task-watchdog) are I/O-bound on `waitFor` polls, so more
|
|
718
|
+
* workers is close to linear there.
|
|
719
|
+
*
|
|
720
|
+
* Scale up only with real headroom, gated on BOTH cpu count and reclaimable
|
|
721
|
+
* memory (budget ~900MB per worker + its fixture subprocesses). An explicit
|
|
722
|
+
* `REPOOS_TEST_WORKERS` always wins; anything else falls back to the config
|
|
723
|
+
* floor via `undefined`.
|
|
724
|
+
*/
|
|
725
|
+
export function testPoolSize(env) {
|
|
726
|
+
const pinned = Number(env.REPOOS_TEST_WORKERS);
|
|
727
|
+
if (Number.isFinite(pinned) && pinned >= 1)
|
|
728
|
+
return Math.floor(pinned);
|
|
729
|
+
const avail = availableMemBytes();
|
|
730
|
+
// Scale up only with genuine headroom. `availableMem` counts reclaimable
|
|
731
|
+
// inactive/cache, but on a box that has been thrashing that memory is slow
|
|
732
|
+
// and expensive to actually reclaim — so also require it to be a solid
|
|
733
|
+
// fraction of total before trusting it. A starved machine stays at the
|
|
734
|
+
// config floor (2).
|
|
735
|
+
if (avail < totalmem() * 0.45)
|
|
736
|
+
return undefined;
|
|
737
|
+
const byCpu = Math.floor(cpus().length * 0.75);
|
|
738
|
+
const byMem = Math.floor(avail / (900 * 1024 * 1024));
|
|
739
|
+
const n = Math.min(byCpu, byMem, 8);
|
|
740
|
+
return n >= 3 ? n : undefined;
|
|
741
|
+
}
|
|
742
|
+
export async function cmdCheck() {
|
|
743
|
+
let exitCode = 0;
|
|
744
|
+
const results = [];
|
|
745
|
+
// ── 1. Build staleness ──────────────────────────────────────────────
|
|
746
|
+
heading("Build staleness check");
|
|
747
|
+
// `repoos check` validates the checkout it was invoked in. In particular,
|
|
748
|
+
// a global/dev-linked CLI may be running from the main checkout while cwd
|
|
749
|
+
// is a task worktree with its own source and build marker.
|
|
750
|
+
const stale = checkBuildForRoot(findRepoRoot());
|
|
751
|
+
if (stale.stale && stale.applicable) {
|
|
752
|
+
console.log(c.yellow(` ⚠ ${stale.message}`));
|
|
753
|
+
results.push(fail("staleness", stale.message ?? "build is stale"));
|
|
754
|
+
exitCode = 1;
|
|
755
|
+
}
|
|
756
|
+
else if (stale.code === "fresh") {
|
|
757
|
+
console.log(c.green(" ✔ Build is fresh"));
|
|
758
|
+
results.push(pass("staleness"));
|
|
759
|
+
}
|
|
760
|
+
else {
|
|
761
|
+
// Not a RepoOS-style build (no dist/ or no marker) — degrade to a skip
|
|
762
|
+
// rather than failing a project whose pipeline simply isn't ours. See
|
|
763
|
+
// checkBuildForRoot: `applicable` is false exactly when no marker exists.
|
|
764
|
+
console.log(c.dim(` · ${stale.message ?? stale.code}`));
|
|
765
|
+
results.push(pass("staleness", stale.message ?? stale.code));
|
|
766
|
+
}
|
|
767
|
+
// ── 1b. Lockfile sync check ─────────────────────────────────────────
|
|
768
|
+
// A dependency bump in package.json without a regenerated bun.lock passes
|
|
769
|
+
// every other check here (node_modules is already installed) but breaks
|
|
770
|
+
// `bun install --frozen-lockfile` for every fresh worktree bootstrap
|
|
771
|
+
// (bootstrap.ts installDeps) — silently, since bootstrap runs on a
|
|
772
|
+
// different checkout than the one that merged the drift. Dry-run makes
|
|
773
|
+
// the frozen install fail loudly instead, before the merge lands.
|
|
774
|
+
heading("Lockfile sync check");
|
|
775
|
+
if (!existsSync("bun.lock")) {
|
|
776
|
+
console.log(c.dim(" · No bun.lock — skipping"));
|
|
777
|
+
results.push(pass("lockfile-sync", "skipped — no bun.lock"));
|
|
778
|
+
}
|
|
779
|
+
else {
|
|
780
|
+
try {
|
|
781
|
+
execSync("bun install --frozen-lockfile --dry-run", { stdio: "pipe", timeout: 60_000 });
|
|
782
|
+
console.log(c.green(" ✔ bun.lock matches package.json"));
|
|
783
|
+
results.push(pass("lockfile-sync"));
|
|
784
|
+
}
|
|
785
|
+
catch {
|
|
786
|
+
const msg = "bun.lock is out of sync with package.json — run `bun install` and commit the updated lockfile";
|
|
787
|
+
console.log(c.red(" ✗ " + msg));
|
|
788
|
+
results.push(fail("lockfile-sync", msg));
|
|
789
|
+
exitCode = 1;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
const pkg = JSON.parse(existsSync("package.json") ? readFileSync("package.json", "utf8") : "{}");
|
|
793
|
+
// Per-project step config (#0348) — `[check] uiSmoke` from repoos.toml. Read
|
|
794
|
+
// once here so the UI smoke step below can resolve its command without a
|
|
795
|
+
// second config load; loadConfig is cheap and repoos.toml is git-tracked.
|
|
796
|
+
const cfg = loadConfig(findRepoRoot());
|
|
797
|
+
// ── 1c. Zero-runtime-dependencies guard ─────────────────────────────
|
|
798
|
+
// "Zero runtime dependencies" is a hard design constraint (AGENTS.md),
|
|
799
|
+
// publicly claimed on the landing page (landing/src/App.vue), and it has
|
|
800
|
+
// already silently drifted from `package.json` once (#0343: mermaid ended
|
|
801
|
+
// up in `dependencies` instead of `devDependencies`, discovered only by a
|
|
802
|
+
// manual audit). This makes the claim self-enforcing instead of
|
|
803
|
+
// convention-only.
|
|
804
|
+
//
|
|
805
|
+
// Scoped to RepoOS's own package.json rather than running for every
|
|
806
|
+
// managed project: this constraint is specific to this repo, not a
|
|
807
|
+
// general rule `repoos check` should impose on projects it manages (a
|
|
808
|
+
// typical managed project has legitimate runtime deps). #0348 tracks
|
|
809
|
+
// making check steps like this declarable per-project instead of
|
|
810
|
+
// hardcoded here; do not widen this scope without that mechanism.
|
|
811
|
+
//
|
|
812
|
+
// Matches the unscoped OR scoped package name (`repoos` / `@.../repoos`):
|
|
813
|
+
// the package was renamed to `@repo-os/repoos` after this guard was
|
|
814
|
+
// written with a literal `=== "repoos"` check, which silently disabled
|
|
815
|
+
// it — the exact "constraint silently stops being enforced" failure mode
|
|
816
|
+
// #0343 exists to catch, now recurring in the guard meant to catch it.
|
|
817
|
+
heading("Zero runtime dependencies guard");
|
|
818
|
+
const pkgBaseName = typeof pkg.name === "string" ? pkg.name.split("/").pop() : undefined;
|
|
819
|
+
if (pkgBaseName !== "repoos") {
|
|
820
|
+
results.push(pass("zero-runtime-deps", "skipped — not RepoOS's own package.json"));
|
|
821
|
+
}
|
|
822
|
+
else {
|
|
823
|
+
const deps = Object.keys(pkg.dependencies ?? {});
|
|
824
|
+
if (deps.length > 0) {
|
|
825
|
+
const msg = `package.json "dependencies" must be empty (zero runtime dependencies is a hard ` +
|
|
826
|
+
`design constraint — AGENTS.md). Found: ${deps.join(", ")}. Move build-time-only ` +
|
|
827
|
+
`packages to devDependencies, or if the constraint no longer holds, update AGENTS.md ` +
|
|
828
|
+
`and landing/src/App.vue's "Zero runtime dependencies" claim instead (see #0343).`;
|
|
829
|
+
console.log(c.red(" ✗ " + msg));
|
|
830
|
+
results.push(fail("zero-runtime-deps", msg));
|
|
831
|
+
exitCode = 1;
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
console.log(c.green(" ✔ package.json has no runtime dependencies"));
|
|
835
|
+
results.push(pass("zero-runtime-deps"));
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
// ── 1d. Formatting & lint guard ──────────────────────────────────────
|
|
839
|
+
// Nothing else runs the formatter/linter — no git hook, no CI job — so
|
|
840
|
+
// without this step `dist/` builds fine while the source silently drifts out
|
|
841
|
+
// of the house style (that's how it accumulated ~10 unformatted files). Both
|
|
842
|
+
// are sub-second with oxfmt/oxlint. Gated on the scripts existing so a plain
|
|
843
|
+
// `repoos init` repo without them just skips.
|
|
844
|
+
// Runs BEFORE the build (not after, as it originally did): fmt/lint are
|
|
845
|
+
// pure source-level checks, no build dependency either way, but a fix
|
|
846
|
+
// (`bun run fmt`) rewrites files — if that happened after a build already
|
|
847
|
+
// ran, the next `repoos check` would see source newer than the build
|
|
848
|
+
// marker and pay for a second full rebuild of nothing but whitespace
|
|
849
|
+
// changes. Checking first means the build only ever runs once source is
|
|
850
|
+
// already clean.
|
|
851
|
+
heading("Formatting & lint guard");
|
|
852
|
+
// Gates the (expensive) Full build and UI smoke test below: a fix here
|
|
853
|
+
// (`bun run fmt`) rewrites source, so a build against the still-unformatted
|
|
854
|
+
// tree is immediately invalidated the moment that fix lands — building it
|
|
855
|
+
// anyway just burns a full tsc pass for nothing. This doesn't weaken the
|
|
856
|
+
// gate: it already fails this run (exitCode is set below) either way, so
|
|
857
|
+
// there is no scenario where skipping the build here lets a broken one
|
|
858
|
+
// slip through unnoticed — it only defers verifying the build to the
|
|
859
|
+
// rerun once source is actually clean.
|
|
860
|
+
let fmtLintFailed = false;
|
|
861
|
+
for (const [label, script, hint] of [
|
|
862
|
+
["Formatting", "fmt:check", "run `bun run fmt` to fix"],
|
|
863
|
+
["Lint", "lint", "fix the reported errors"],
|
|
864
|
+
]) {
|
|
865
|
+
if (!pkg.scripts?.[script]) {
|
|
866
|
+
console.log(c.dim(` · No \`${script}\` script — skipping ${label.toLowerCase()}`));
|
|
867
|
+
results.push(pass(`check-${script}`, `skipped — no ${script} script`));
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
try {
|
|
871
|
+
execSync(`${preferBunForDevTasks() ? "bun run" : "npm run"} ${script}`, {
|
|
872
|
+
stdio: "pipe",
|
|
873
|
+
timeout: 120_000,
|
|
874
|
+
});
|
|
875
|
+
console.log(c.green(` ✔ ${label} clean`));
|
|
876
|
+
results.push(pass(`check-${script}`));
|
|
877
|
+
}
|
|
878
|
+
catch (e) {
|
|
879
|
+
const out = [e.stdout, e.stderr]
|
|
880
|
+
.map((b) => b?.toString().trim())
|
|
881
|
+
.filter(Boolean)
|
|
882
|
+
.join("\n");
|
|
883
|
+
const msg = `${label} check failed — ${hint}:\n${out || e.message}`;
|
|
884
|
+
console.log(c.red(` ✗ ${label} check failed — ${hint}`));
|
|
885
|
+
results.push(fail(`check-${script}`, msg));
|
|
886
|
+
exitCode = 1;
|
|
887
|
+
fmtLintFailed = true;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
// ── 2. Full build ───────────────────────────────────────────────────
|
|
891
|
+
// Always run `bun run build`; it is staleness-aware now (scripts/build.mjs,
|
|
892
|
+
// #0377) and skips in ~0.1s when src/ is unchanged since the marker was
|
|
893
|
+
// written. That replaces the old REPOOS_SKIP_BUILD opt-in: the close-out
|
|
894
|
+
// pipeline (src/server/done.ts, integration-orchestrator.ts, release.ts) runs
|
|
895
|
+
// `bun run build` just before invoking `repoos check`, and this step now
|
|
896
|
+
// detects the fresh marker on its own instead of via a private env flag.
|
|
897
|
+
//
|
|
898
|
+
// The staleness step above still runs and reports FIRST (#0276): a genuinely
|
|
899
|
+
// stale build is surfaced there, then this step repairs it within the same
|
|
900
|
+
// invocation. The build's skip is safe here precisely because step 1 already
|
|
901
|
+
// verified the marker matches src/ — a stale or missing marker never skips.
|
|
902
|
+
heading("Full build");
|
|
903
|
+
if (fmtLintFailed) {
|
|
904
|
+
console.log(c.dim(" · Skipped — formatting/lint failed above; fix that first, dist would be stale " +
|
|
905
|
+
"the moment you do, and this run already fails regardless"));
|
|
906
|
+
results.push(pass("build", "skipped — formatting/lint failed, fix and rerun"));
|
|
907
|
+
}
|
|
908
|
+
else {
|
|
909
|
+
try {
|
|
910
|
+
execSync("bun run build", { stdio: "inherit", timeout: 120_000 });
|
|
911
|
+
console.log(c.green(" ✔ Build succeeded"));
|
|
912
|
+
results.push(pass("build"));
|
|
913
|
+
}
|
|
914
|
+
catch (e) {
|
|
915
|
+
const msg = e.message;
|
|
916
|
+
console.log(c.red(" ✗ Build failed"));
|
|
917
|
+
results.push(fail("build", msg));
|
|
918
|
+
exitCode = 1;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
// ── 2b. CSS layering guard ──────────────────────────────────────────
|
|
922
|
+
// Both stylesheet guards below read `[check] uiStylesheet` (#0351): there is
|
|
923
|
+
// no RepoOS-shaped default path, so a project that declares neither a
|
|
924
|
+
// stylesheet nor a token vocabulary skips both cleanly. The path is resolved
|
|
925
|
+
// against the repo root (not cwd) and read once here; a configured path that
|
|
926
|
+
// doesn't exist is a misconfiguration worth a warning, not a silent skip.
|
|
927
|
+
heading("CSS layering guard");
|
|
928
|
+
const cssRelPath = cfg.check?.uiStylesheet;
|
|
929
|
+
const repoRoot = findRepoRoot();
|
|
930
|
+
const cssPath = cssRelPath && !isAbsolute(cssRelPath) ? join(repoRoot, cssRelPath) : cssRelPath;
|
|
931
|
+
const cssExists = Boolean(cssPath && existsSync(cssPath));
|
|
932
|
+
if (cssRelPath && !cssExists) {
|
|
933
|
+
console.log(c.yellow(` ⚠ [check] uiStylesheet "${cssRelPath}" does not exist in this repo — stylesheet guards will skip`));
|
|
934
|
+
}
|
|
935
|
+
const cssSrc = cssPath && cssExists ? readFileSync(cssPath, "utf8") : "";
|
|
936
|
+
if (!cssRelPath) {
|
|
937
|
+
console.log(c.dim(" · No [check] uiStylesheet configured — skipping"));
|
|
938
|
+
results.push(pass("css-layers", "skipped — no [check] uiStylesheet configured"));
|
|
939
|
+
}
|
|
940
|
+
else if (!cssExists) {
|
|
941
|
+
console.log(c.dim(` · ${cssRelPath} does not exist — skipping`));
|
|
942
|
+
results.push(pass("css-layers", `skipped — ${cssRelPath} not found`));
|
|
943
|
+
}
|
|
944
|
+
else if (!cssSrc.includes('@import "tailwindcss"')) {
|
|
945
|
+
console.log(c.dim(` · ${cssPath} is not a Tailwind v4 stylesheet — skipping`));
|
|
946
|
+
results.push(pass("css-layers", `skipped — ${cssPath} has no Tailwind v4 import`));
|
|
947
|
+
}
|
|
948
|
+
else {
|
|
949
|
+
const offenders = cssLayeringOffenders(cssSrc);
|
|
950
|
+
if (offenders.length > 0) {
|
|
951
|
+
const msg = "Unlayered universal/bare-element selectors (they silently beat all Tailwind utilities):\n " +
|
|
952
|
+
offenders.slice(0, 8).join("\n ") +
|
|
953
|
+
"\n Wrap them in @layer base or scope them to a class/id.";
|
|
954
|
+
console.log(c.red(" ✗ " + msg.split("\n")[0]));
|
|
955
|
+
results.push(fail("css-layers", msg));
|
|
956
|
+
exitCode = 1;
|
|
957
|
+
}
|
|
958
|
+
else {
|
|
959
|
+
console.log(c.green(" ✔ No unlayered universal/bare-element selectors"));
|
|
960
|
+
results.push(pass("css-layers"));
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
// ── 2c. Theme contrast guard ────────────────────────────────────────
|
|
964
|
+
heading("Theme contrast guard");
|
|
965
|
+
const themeScopes = cfg.check?.themeScopes ?? [];
|
|
966
|
+
for (const w of themeScopeConfigWarnings(themeScopes))
|
|
967
|
+
console.log(c.yellow(` ⚠ ${w}`));
|
|
968
|
+
if (!cssRelPath) {
|
|
969
|
+
console.log(c.dim(" · No [check] uiStylesheet configured — skipping"));
|
|
970
|
+
results.push(pass("theme-contrast", "skipped — no [check] uiStylesheet configured"));
|
|
971
|
+
}
|
|
972
|
+
else if (!cssExists) {
|
|
973
|
+
console.log(c.dim(` · ${cssRelPath} does not exist — skipping`));
|
|
974
|
+
results.push(pass("theme-contrast", `skipped — ${cssRelPath} not found`));
|
|
975
|
+
}
|
|
976
|
+
else if (!themeScopes.length) {
|
|
977
|
+
console.log(c.dim(" · No [check] themeScopes configured — skipping"));
|
|
978
|
+
results.push(pass("theme-contrast", "skipped — no [check] themeScopes configured"));
|
|
979
|
+
}
|
|
980
|
+
else if (!hasThemeBlocks(cssSrc, themeScopes)) {
|
|
981
|
+
console.log(c.dim(` · No configured theme scope matched a block in ${cssRelPath} — skipping`));
|
|
982
|
+
results.push(pass("theme-contrast", "skipped — no configured theme block found"));
|
|
983
|
+
}
|
|
984
|
+
else {
|
|
985
|
+
const offenders = themeContrastOffenders(cssSrc, {
|
|
986
|
+
scopes: themeScopes,
|
|
987
|
+
pairs: cfg.check?.contrastPairs ?? [],
|
|
988
|
+
gradientTokens: cfg.check?.gradientTokens ?? [],
|
|
989
|
+
backdropToken: cfg.check?.backdropToken,
|
|
990
|
+
});
|
|
991
|
+
if (offenders.length > 0) {
|
|
992
|
+
const msg = "Low-contrast or invalid theme tokens (invisible text risk):\n " +
|
|
993
|
+
offenders.slice(0, 10).join("\n ");
|
|
994
|
+
console.log(c.red(" ✗ " + msg.split("\n")[0]));
|
|
995
|
+
results.push(fail("theme-contrast", msg));
|
|
996
|
+
exitCode = 1;
|
|
997
|
+
}
|
|
998
|
+
else {
|
|
999
|
+
console.log(c.green(" ✔ Theme tokens have valid button gradients and ≥3:1 contrast"));
|
|
1000
|
+
results.push(pass("theme-contrast"));
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
// ── 2d. Bare require() guard ─────────────────────────────────────────
|
|
1004
|
+
// The bug this guards is specific to `"type": "module"` packages (a bare
|
|
1005
|
+
// `require` is valid in CJS), and the directories to scan are per-project
|
|
1006
|
+
// (#0352): `[check] bareRequireDirs`, else the tsconfig include list.
|
|
1007
|
+
heading("Bare require() guard");
|
|
1008
|
+
{
|
|
1009
|
+
const resolved = resolveBareRequireRoots(cfg.check?.bareRequireDirs, readTsconfig(repoRoot), cfg.check?.bareRequireExcludes);
|
|
1010
|
+
if (pkg.type !== "module") {
|
|
1011
|
+
console.log(c.dim(' · package.json is not "type": "module" — skipping (bare require() is valid in CJS)'));
|
|
1012
|
+
results.push(pass("bare-require", 'skipped — package.json is not "type": "module"'));
|
|
1013
|
+
}
|
|
1014
|
+
else if (resolved.roots.length === 0) {
|
|
1015
|
+
console.log(c.dim(" · No source roots to scan — set [check] bareRequireDirs or a tsconfig include — skipping"));
|
|
1016
|
+
results.push(pass("bare-require", "skipped — no source roots configured"));
|
|
1017
|
+
}
|
|
1018
|
+
else {
|
|
1019
|
+
const offenders = bareRequireOffenders(resolved.roots, {
|
|
1020
|
+
repoRoot,
|
|
1021
|
+
excludes: resolved.excludes,
|
|
1022
|
+
});
|
|
1023
|
+
if (offenders.length > 0) {
|
|
1024
|
+
const msg = 'Bare require() in ESM source (this package is "type": "module" — a bare require throws ' +
|
|
1025
|
+
"ReferenceError at runtime in dist/, silently if caught):\n " +
|
|
1026
|
+
offenders.slice(0, 10).join("\n ") +
|
|
1027
|
+
'\n Import from "node:..." normally, or use createRequire(import.meta.url) if you ' +
|
|
1028
|
+
"genuinely need CJS interop (see ui-harness.ts).";
|
|
1029
|
+
console.log(c.red(" ✗ " + msg.split("\n")[0]));
|
|
1030
|
+
results.push(fail("bare-require", msg));
|
|
1031
|
+
exitCode = 1;
|
|
1032
|
+
}
|
|
1033
|
+
else {
|
|
1034
|
+
const from = resolved.source === "config" ? "[check] bareRequireDirs" : "tsconfig include";
|
|
1035
|
+
console.log(c.green(` ✔ No bare require() calls in ESM source (${from})`));
|
|
1036
|
+
results.push(pass("bare-require"));
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
// ── 2d′. Task / input asset guard ───────────────────────────────────
|
|
1041
|
+
heading("Task asset guard");
|
|
1042
|
+
{
|
|
1043
|
+
// Folder names are configurable (`workDir`/`inputsDir` in repoos.toml); a
|
|
1044
|
+
// managed repo that renames them must still be guarded, so read them rather
|
|
1045
|
+
// than assuming the default `work`/`inputs`.
|
|
1046
|
+
const config = loadConfig();
|
|
1047
|
+
const inputsDirRaw = config.inputsDir ?? "inputs";
|
|
1048
|
+
const guardDirs = [
|
|
1049
|
+
{ label: "workDir", raw: config.workDir, norm: normalizeGuardDir(config.workDir) },
|
|
1050
|
+
{ label: "inputsDir", raw: inputsDirRaw, norm: normalizeGuardDir(inputsDirRaw) },
|
|
1051
|
+
];
|
|
1052
|
+
for (const { label, raw, norm } of guardDirs) {
|
|
1053
|
+
if (!norm) {
|
|
1054
|
+
console.log(c.yellow(` ⚠ repoos.toml's ${label} ("${raw}") doesn't resolve to a repo-relative ` +
|
|
1055
|
+
"directory — the task-asset guard can't be scoped to it. Fix the config."));
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
// Only usable, repo-relative dirs go into the pathspec. An empty one is
|
|
1059
|
+
// dropped, not passed to git (`git ls-files -- ""` exits fatally, and the
|
|
1060
|
+
// catch below would turn that into a silent green). If BOTH are unusable
|
|
1061
|
+
// the guard can't run at all — fail loudly rather than pass with nothing
|
|
1062
|
+
// to check.
|
|
1063
|
+
const usableDirs = guardDirs.map((d) => d.norm).filter(Boolean);
|
|
1064
|
+
if (usableDirs.length === 0) {
|
|
1065
|
+
const msg = "task-asset guard is disabled — neither workDir nor inputsDir in repoos.toml resolves " +
|
|
1066
|
+
"to a repo-relative directory, so no task/input folder can be checked. Fix the config.";
|
|
1067
|
+
console.log(c.red(" ✗ " + msg));
|
|
1068
|
+
results.push(fail("task-assets", msg));
|
|
1069
|
+
exitCode = 1;
|
|
1070
|
+
}
|
|
1071
|
+
else {
|
|
1072
|
+
let tracked = [];
|
|
1073
|
+
try {
|
|
1074
|
+
// Pathspecs are pre-validated (non-empty, repo-relative) above, so a
|
|
1075
|
+
// failure here means no git repo / git unavailable — not bad config.
|
|
1076
|
+
tracked = execFileSync("git", ["ls-files", "--", ...usableDirs], {
|
|
1077
|
+
encoding: "utf8",
|
|
1078
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
1079
|
+
})
|
|
1080
|
+
.split("\n")
|
|
1081
|
+
.filter(Boolean);
|
|
1082
|
+
}
|
|
1083
|
+
catch {
|
|
1084
|
+
/* not a git repo / git unavailable — nothing to guard */
|
|
1085
|
+
}
|
|
1086
|
+
const offenders = taskAssetOffenders(tracked, {
|
|
1087
|
+
workDir: config.workDir,
|
|
1088
|
+
inputsDir: inputsDirRaw,
|
|
1089
|
+
});
|
|
1090
|
+
const labels = usableDirs.map((d) => `${d}/`).join(" or ");
|
|
1091
|
+
if (offenders.length > 0) {
|
|
1092
|
+
const msg = `Binary attachments committed under ${labels} — these are served by ` +
|
|
1093
|
+
"the running server from disk and must never enter git history (it bloats the repo " +
|
|
1094
|
+
"irreversibly). Run `git rm --cached` on them (they stay on disk) and let " +
|
|
1095
|
+
"`.gitignore` keep them out:\n " +
|
|
1096
|
+
offenders.slice(0, 15).join("\n ") +
|
|
1097
|
+
(offenders.length > 15 ? `\n …and ${offenders.length - 15} more` : "");
|
|
1098
|
+
console.log(c.red(" ✗ " + msg.split("\n")[0]));
|
|
1099
|
+
results.push(fail("task-assets", msg));
|
|
1100
|
+
exitCode = 1;
|
|
1101
|
+
}
|
|
1102
|
+
else {
|
|
1103
|
+
console.log(c.green(` ✔ No committed binaries under ${labels}`));
|
|
1104
|
+
results.push(pass("task-assets"));
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
// ── 3. Tests (if any) ───────────────────────────────────────────────
|
|
1109
|
+
heading("Tests");
|
|
1110
|
+
const hasTestScript = Boolean(pkg.scripts && pkg.scripts.test);
|
|
1111
|
+
const hasTestFiles = existsSync("test") || existsSync("__tests__") || existsSync("tests");
|
|
1112
|
+
// The close-out pipeline can hand the test suite to the Remote Validation
|
|
1113
|
+
// Runner (docs/remote-validation.md) — a Hetzner VM runs `bun run build` +
|
|
1114
|
+
// `bun run test` off this machine — and then invoke the LOCAL `repoos check`
|
|
1115
|
+
// with REPOOS_SKIP_TESTS=1 for only the cheap static guards + UI smoke.
|
|
1116
|
+
// Standalone `repoos check` never sets it, so the CLI gate is unchanged.
|
|
1117
|
+
if (process.env.REPOOS_SKIP_TESTS === "1") {
|
|
1118
|
+
console.log(c.dim(" · Skipped — test suite ran on the remote validation runner (REPOOS_SKIP_TESTS=1)"));
|
|
1119
|
+
results.push(pass("tests", "skipped — ran on the remote validation runner"));
|
|
1120
|
+
}
|
|
1121
|
+
else if (hasTestScript || hasTestFiles) {
|
|
1122
|
+
// Only the vitest-backed `bun run test` script understands `--changed`;
|
|
1123
|
+
// the bare `bun test` fallback (no package.json test script) is left
|
|
1124
|
+
// unscoped. `--bun` forces vitest's `#!/usr/bin/env node` shebang onto
|
|
1125
|
+
// Bun for a Bun-native repo (has bun.lock) unless pinned to Node — ~5x
|
|
1126
|
+
// faster and it stops the swap-thrash flake on a loaded machine.
|
|
1127
|
+
// preferBunForDevTasks() checks the lockfile so a managed repo whose
|
|
1128
|
+
// tests want Node is never switched.
|
|
1129
|
+
const changedRef = hasTestScript ? changedTestRef(process.env) : undefined;
|
|
1130
|
+
const runScript = preferBunForDevTasks() ? "bun run --bun test" : "bun run test";
|
|
1131
|
+
const cmd = hasTestScript
|
|
1132
|
+
? changedRef
|
|
1133
|
+
? `${runScript} -- --changed ${changedRef}`
|
|
1134
|
+
: runScript
|
|
1135
|
+
: "bun test";
|
|
1136
|
+
if (changedRef) {
|
|
1137
|
+
console.log(c.dim(` · Scoped to files changed vs ${changedRef} (--changed)`));
|
|
1138
|
+
}
|
|
1139
|
+
// Worker pool: `undefined` keeps the config floor (2). A solo run with
|
|
1140
|
+
// headroom scales up — passed through env, read back in vite.config.ts.
|
|
1141
|
+
const workers = hasTestScript ? testPoolSize(process.env) : undefined;
|
|
1142
|
+
const testEnv = workers
|
|
1143
|
+
? { ...process.env, REPOOS_TEST_WORKERS: String(workers) }
|
|
1144
|
+
: process.env;
|
|
1145
|
+
if (workers && !changedRef) {
|
|
1146
|
+
const availGiB = (availableMemBytes() / 1024 ** 3).toFixed(1);
|
|
1147
|
+
console.log(c.dim(` · Full suite · ${workers} workers (${availGiB} GiB reclaimable)`));
|
|
1148
|
+
}
|
|
1149
|
+
// A full unscoped run is ~10min even healthy and can legitimately reach
|
|
1150
|
+
// ~25min on a slow box; a too-tight cap SIGTERMs `bun run test` mid-run
|
|
1151
|
+
// (exit 143) even when every test is green. Vitest's own per-test
|
|
1152
|
+
// `testTimeout` fails a genuine hang fast; this is only the outer backstop.
|
|
1153
|
+
const timeoutMs = changedRef ? 300_000 : 1_500_000;
|
|
1154
|
+
try {
|
|
1155
|
+
execSync(cmd, { stdio: "inherit", timeout: timeoutMs, env: testEnv });
|
|
1156
|
+
console.log(c.green(" ✔ Tests passed"));
|
|
1157
|
+
results.push(pass("tests", changedRef ? `scoped to changed vs ${changedRef}` : undefined));
|
|
1158
|
+
}
|
|
1159
|
+
catch (e) {
|
|
1160
|
+
console.log(c.red(" ✗ Tests failed"));
|
|
1161
|
+
results.push(fail("tests", e.message));
|
|
1162
|
+
exitCode = 1;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
else {
|
|
1166
|
+
console.log(c.dim(" · No test suite found — skipping"));
|
|
1167
|
+
results.push(pass("tests", "skipped — no test suite"));
|
|
1168
|
+
}
|
|
1169
|
+
// ── 4. UI smoke test ────────────────────────────────────────────────
|
|
1170
|
+
// Per-project and opt-in (#0348). `repoos check` is the generic gate every
|
|
1171
|
+
// managed project runs, so it must not boot RepoOS's own dashboard for a
|
|
1172
|
+
// project that never declared a smoke command — that either silently tested
|
|
1173
|
+
// RepoOS's UI or failed on RepoOS-only infrastructure. A project opts in
|
|
1174
|
+
// with a `smoke` package.json script (zero-config default) or `[check]
|
|
1175
|
+
// uiSmoke` in repoos.toml (overrides the script). With neither, the step
|
|
1176
|
+
// skips cleanly. RepoOS dogfoods this: its own dashboard assertions live in
|
|
1177
|
+
// src/commands/ui-smoke.ts behind a `smoke` script (scripts/ui-smoke.mjs),
|
|
1178
|
+
// so the declaration path is exercised on every RepoOS check.
|
|
1179
|
+
heading("UI smoke test");
|
|
1180
|
+
const smokeCommand = resolveSmokeCommand(cfg.check?.uiSmoke, pkg.scripts);
|
|
1181
|
+
if (fmtLintFailed) {
|
|
1182
|
+
// Build was skipped above, so dist reflects whatever the last successful
|
|
1183
|
+
// build was (possibly stale, possibly absent) — never a build of the
|
|
1184
|
+
// current, still-unformatted source. Probing it here would be testing the
|
|
1185
|
+
// wrong thing — the same reason the build step above is gated on it.
|
|
1186
|
+
console.log(c.dim(" · Skipped — formatting/lint failed above, so build was skipped too"));
|
|
1187
|
+
results.push(pass("ui-smoke", "skipped — formatting/lint failed, fix and rerun"));
|
|
1188
|
+
}
|
|
1189
|
+
else if (smokeCommand) {
|
|
1190
|
+
const runner = preferBunForDevTasks() ? "bun run" : "npm run";
|
|
1191
|
+
const cmd = smokeCommand.source === "config" ? smokeCommand.command : `${runner} ${smokeCommand.script}`;
|
|
1192
|
+
const origin = smokeCommand.source === "config"
|
|
1193
|
+
? "repoos.toml [check] uiSmoke"
|
|
1194
|
+
: "package.json smoke script";
|
|
1195
|
+
console.log(c.dim(` · Running ${origin}: ${cmd}`));
|
|
1196
|
+
try {
|
|
1197
|
+
execSync(cmd, { stdio: "inherit", timeout: 300_000 });
|
|
1198
|
+
console.log(c.green(" ✔ Smoke command passed"));
|
|
1199
|
+
results.push(pass("ui-smoke", `ran ${origin}`));
|
|
1200
|
+
}
|
|
1201
|
+
catch (e) {
|
|
1202
|
+
console.log(c.red(" ✗ Smoke command failed"));
|
|
1203
|
+
results.push(fail("ui-smoke", e.message));
|
|
1204
|
+
exitCode = 1;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
else {
|
|
1208
|
+
console.log(c.dim(" · No smoke command configured — skipping"));
|
|
1209
|
+
console.log(c.dim(' Declare one with a `smoke` package.json script or [check] uiSmoke = "bun run smoke" in repoos.toml'));
|
|
1210
|
+
results.push(pass("ui-smoke", "skipped — no smoke command configured"));
|
|
1211
|
+
}
|
|
1212
|
+
// ── Summary ─────────────────────────────────────────────────────────
|
|
1213
|
+
const failed = results.filter((r) => !r.ok);
|
|
1214
|
+
console.log(c.bold(c.cyan("\n ── Results ──")));
|
|
1215
|
+
for (const r of results) {
|
|
1216
|
+
const icon = r.ok ? c.green("✔") : c.red("✗");
|
|
1217
|
+
const detail = r.detail ? c.dim(` — ${r.detail}`) : "";
|
|
1218
|
+
console.log(` ${icon} ${r.name}${detail}`);
|
|
1219
|
+
}
|
|
1220
|
+
if (failed.length === 0) {
|
|
1221
|
+
console.log(c.bold(c.green("\n All checks passed.\n")));
|
|
1222
|
+
}
|
|
1223
|
+
else {
|
|
1224
|
+
console.log(c.bold(c.red(`\n ${failed.length} check(s) failed.\n`)));
|
|
1225
|
+
}
|
|
1226
|
+
process.exit(exitCode);
|
|
1227
|
+
}
|