codeply-cli 0.5.5 → 0.6.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/LICENSE +21 -21
- package/THIRD_PARTY_NOTICES.md +65 -65
- package/bin/codeply.js +8 -3
- package/lib/agent.mjs +2596 -2268
- package/lib/ai.js +135 -27
- package/lib/ai.mjs +6 -6
- package/lib/apply-patch.js +217 -217
- package/lib/arity-table.json +138 -138
- package/lib/arity.js +70 -70
- package/lib/auth.mjs +6 -6
- package/lib/autopilot.js +84 -0
- package/lib/bot-mail.js +46 -0
- package/lib/bots.js +142 -15
- package/lib/brain.js +914 -0
- package/lib/call-assist.js +120 -0
- package/lib/chatgpt.js +282 -282
- package/lib/cloud-browser.mjs +143 -143
- package/lib/cloud.mjs +1396 -1312
- package/lib/commands.js +103 -103
- package/lib/computer.js +136 -0
- package/lib/design-library/assemble-artifact.mjs +21 -21
- package/lib/design-library/cleanup.mjs +45 -45
- package/lib/design-library/index.json +22379 -22379
- package/lib/design-library/make-table-data.mjs +39 -39
- package/lib/design-tools.mjs +173 -0
- package/lib/diagnostics.js +192 -192
- package/lib/dsh-acp.mjs +444 -0
- package/lib/edit-engine.js +364 -364
- package/lib/github-agent.mjs +326 -326
- package/lib/handoff.mjs +245 -0
- package/lib/history.js +78 -75
- package/lib/life-sources.js +319 -0
- package/lib/life-store.js +356 -0
- package/lib/life-triggers.js +286 -0
- package/lib/life.js +487 -0
- package/lib/mail-watch.js +3 -3
- package/lib/mcp.js +258 -258
- package/lib/native-tools.mjs +17 -5
- package/lib/oauth-connectors.js +76 -21
- package/lib/permissions.js +109 -109
- package/lib/plugins.js +356 -353
- package/lib/reach.js +67 -67
- package/lib/render-md.mjs +647 -617
- package/lib/screen.js +77 -0
- package/lib/server.mjs +458 -457
- package/lib/session-db.js +220 -220
- package/lib/share.js +208 -168
- package/lib/skills.js +4 -1
- package/lib/snapshot.js +146 -146
- package/lib/subagents.js +4 -2
- package/lib/telemetry.js +105 -105
- package/lib/terminal-hints.js +154 -154
- package/lib/textarea.mjs +264 -258
- package/lib/tools.mjs +326 -40
- package/lib/tui.mjs +1459 -1086
- package/lib/typecheck-worker.js +147 -147
- package/lib/web-tools.js +209 -209
- package/package.json +1 -1
- package/skills/SOURCE.md +58 -58
- package/skills/accessibility/SKILL.md +147 -147
- package/skills/agent-architecture-audit/SKILL.md +257 -257
- package/skills/agent-eval/SKILL.md +147 -147
- package/skills/agent-harness-construction/SKILL.md +74 -74
- package/skills/agent-introspection-debugging/SKILL.md +154 -154
- package/skills/agent-payment-x402/SKILL.md +225 -225
- package/skills/agent-self-evaluation/SKILL.md +182 -182
- package/skills/agent-self-evaluation/references/evaluation-criteria.md +71 -71
- package/skills/agent-self-evaluation/references/hook-integration.md +64 -64
- package/skills/agent-sort/SKILL.md +216 -216
- package/skills/agentic-engineering/SKILL.md +64 -64
- package/skills/agentic-os/SKILL.md +388 -388
- package/skills/ai-first-engineering/SKILL.md +52 -52
- package/skills/ai-regression-testing/SKILL.md +386 -386
- package/skills/android-clean-architecture/SKILL.md +340 -340
- package/skills/angular-developer/SKILL.md +155 -155
- package/skills/angular-developer/references/angular-animations.md +160 -160
- package/skills/angular-developer/references/angular-aria.md +410 -410
- package/skills/angular-developer/references/cli.md +86 -86
- package/skills/angular-developer/references/component-harnesses.md +59 -59
- package/skills/angular-developer/references/component-styling.md +91 -91
- package/skills/angular-developer/references/components.md +117 -117
- package/skills/angular-developer/references/creating-services.md +97 -97
- package/skills/angular-developer/references/data-resolvers.md +69 -69
- package/skills/angular-developer/references/define-routes.md +67 -67
- package/skills/angular-developer/references/defining-providers.md +72 -72
- package/skills/angular-developer/references/di-fundamentals.md +120 -120
- package/skills/angular-developer/references/e2e-testing.md +56 -56
- package/skills/angular-developer/references/effects.md +83 -83
- package/skills/angular-developer/references/hierarchical-injectors.md +43 -43
- package/skills/angular-developer/references/host-elements.md +80 -80
- package/skills/angular-developer/references/injection-context.md +63 -63
- package/skills/angular-developer/references/inputs.md +101 -101
- package/skills/angular-developer/references/linked-signal.md +59 -59
- package/skills/angular-developer/references/loading-strategies.md +61 -61
- package/skills/angular-developer/references/mcp.md +108 -108
- package/skills/angular-developer/references/navigate-to-routes.md +69 -69
- package/skills/angular-developer/references/outputs.md +86 -86
- package/skills/angular-developer/references/reactive-forms.md +122 -122
- package/skills/angular-developer/references/rendering-strategies.md +44 -44
- package/skills/angular-developer/references/resource.md +77 -77
- package/skills/angular-developer/references/route-animations.md +56 -56
- package/skills/angular-developer/references/route-guards.md +52 -52
- package/skills/angular-developer/references/router-lifecycle.md +45 -45
- package/skills/angular-developer/references/router-testing.md +87 -87
- package/skills/angular-developer/references/show-routes-with-outlets.md +68 -68
- package/skills/angular-developer/references/signal-forms.md +795 -795
- package/skills/angular-developer/references/signals-overview.md +94 -94
- package/skills/angular-developer/references/tailwind-css.md +69 -69
- package/skills/angular-developer/references/template-driven-forms.md +114 -114
- package/skills/angular-developer/references/testing-fundamentals.md +65 -65
- package/skills/api-connector-builder/SKILL.md +121 -121
- package/skills/api-design/SKILL.md +524 -524
- package/skills/architecture-decision-records/SKILL.md +180 -180
- package/skills/article-writing/SKILL.md +80 -80
- package/skills/automation-audit-ops/SKILL.md +143 -143
- package/skills/autonomous-agent-harness/SKILL.md +274 -274
- package/skills/autonomous-loops/SKILL.md +611 -611
- package/skills/backend-patterns/SKILL.md +562 -562
- package/skills/benchmark/SKILL.md +95 -95
- package/skills/benchmark-methodology/SKILL.md +191 -191
- package/skills/benchmark-optimization-loop/SKILL.md +71 -71
- package/skills/blender-motion-state-inspection/SKILL.md +165 -165
- package/skills/blueprint/SKILL.md +106 -106
- package/skills/brand-discovery/SKILL.md +145 -145
- package/skills/brand-discovery/references/10_purpose-why.md +40 -40
- package/skills/brand-discovery/references/20_positioning.md +44 -44
- package/skills/brand-discovery/references/30_audience-niche.md +52 -52
- package/skills/brand-discovery/references/40_personality-archetype.md +57 -57
- package/skills/brand-discovery/references/50_voice-tone.md +59 -59
- package/skills/brand-discovery/references/60_narrative-story.md +50 -50
- package/skills/brand-discovery/references/70_founder-tension.md +49 -49
- package/skills/brand-discovery/references/90_SYNTHESIS.md +133 -133
- package/skills/brand-voice/SKILL.md +98 -98
- package/skills/brand-voice/references/voice-profile-schema.md +55 -55
- package/skills/browser-qa/SKILL.md +105 -105
- package/skills/bun-runtime/SKILL.md +85 -85
- package/skills/canary-watch/SKILL.md +108 -108
- package/skills/carrier-relationship-management/SKILL.md +212 -212
- package/skills/cisco-ios-patterns/SKILL.md +164 -164
- package/skills/ck/SKILL.md +148 -148
- package/skills/claude-devfleet/SKILL.md +112 -112
- package/skills/click-path-audit/SKILL.md +245 -245
- package/skills/clickhouse-io/SKILL.md +445 -445
- package/skills/code-tour/SKILL.md +254 -254
- package/skills/codebase-onboarding/SKILL.md +234 -234
- package/skills/codehealth-mcp/SKILL.md +167 -167
- package/skills/codeply-taste/LICENSE +21 -0
- package/skills/codeply-taste/SKILL.md +100 -0
- package/skills/codeply-taste/SOURCE.md +10 -0
- package/skills/codeply-taste/assets/base.css +100 -0
- package/skills/codeply-taste/assets/base.js +37 -0
- package/skills/codeply-taste/library/directions.json +1078 -0
- package/skills/codeply-taste/library/heroes.json +894 -0
- package/skills/codeply-taste/library/motions.json +57 -0
- package/skills/codeply-taste/library/palettes.json +1485 -0
- package/skills/codeply-taste/library/photos.json +7240 -0
- package/skills/codeply-taste/library/sections.json +4136 -0
- package/skills/codeply-taste/library/sites.json +7274 -0
- package/skills/codeply-taste/library/type.json +2024 -0
- package/skills/codeply-taste/references/colour.md +141 -0
- package/skills/codeply-taste/references/copy.md +177 -0
- package/skills/codeply-taste/references/critique.md +171 -0
- package/skills/codeply-taste/references/imagery.md +377 -0
- package/skills/codeply-taste/references/layout.md +178 -0
- package/skills/codeply-taste/references/motion.md +292 -0
- package/skills/codeply-taste/references/research.md +306 -0
- package/skills/codeply-taste/references/tells.md +115 -0
- package/skills/codeply-taste/references/type.md +170 -0
- package/skills/codeply-taste/scripts/assets.mjs +865 -0
- package/skills/codeply-taste/scripts/draw.mjs +525 -0
- package/skills/codeply-taste/scripts/inspect.mjs +205 -0
- package/skills/codeply-taste/scripts/lib/cdp.mjs +205 -0
- package/skills/codeply-taste/scripts/lib/probe.mjs +569 -0
- package/skills/codeply-taste/scripts/lib/rules.mjs +137 -0
- package/skills/codeply-taste/scripts/lint.mjs +108 -0
- package/skills/coding-standards/SKILL.md +551 -551
- package/skills/competitive-platform-analysis/SKILL.md +214 -214
- package/skills/competitive-report-structure/SKILL.md +162 -162
- package/skills/compose-multiplatform-patterns/SKILL.md +300 -300
- package/skills/config-gc/SKILL.md +120 -120
- package/skills/configure-ecc/SKILL.md +385 -385
- package/skills/connections-optimizer/SKILL.md +190 -190
- package/skills/content-engine/SKILL.md +132 -132
- package/skills/content-hash-cache-pattern/SKILL.md +162 -162
- package/skills/context-budget/SKILL.md +136 -136
- package/skills/continuous-agent-loop/SKILL.md +46 -46
- package/skills/continuous-learning/SKILL.md +132 -132
- package/skills/continuous-learning-v2/SKILL.md +361 -361
- package/skills/contract-first/SKILL.md +287 -287
- package/skills/cost-aware-llm-pipeline/SKILL.md +184 -184
- package/skills/cost-tracking/SKILL.md +97 -97
- package/skills/council/SKILL.md +204 -204
- package/skills/cpp-coding-standards/SKILL.md +724 -724
- package/skills/cpp-testing/SKILL.md +325 -325
- package/skills/crosspost/SKILL.md +112 -112
- package/skills/csharp-testing/SKILL.md +322 -322
- package/skills/customer-billing-ops/SKILL.md +141 -141
- package/skills/customs-trade-compliance/SKILL.md +263 -263
- package/skills/dart-flutter-patterns/SKILL.md +564 -564
- package/skills/dashboard-builder/SKILL.md +109 -109
- package/skills/data-scraper-agent/SKILL.md +765 -765
- package/skills/data-throughput-accelerator/SKILL.md +74 -74
- package/skills/database-migrations/SKILL.md +430 -430
- package/skills/deep-research/SKILL.md +160 -160
- package/skills/defi-amm-security/SKILL.md +167 -167
- package/skills/delivery-gate/SKILL.md +126 -126
- package/skills/deployment-patterns/SKILL.md +428 -428
- package/skills/design-system/SKILL.md +83 -83
- package/skills/django-celery/SKILL.md +458 -458
- package/skills/django-patterns/SKILL.md +735 -735
- package/skills/django-security/SKILL.md +644 -644
- package/skills/django-tdd/SKILL.md +730 -730
- package/skills/django-verification/SKILL.md +470 -470
- package/skills/dmux-workflows/SKILL.md +192 -192
- package/skills/docker-patterns/SKILL.md +365 -365
- package/skills/documentation-lookup/SKILL.md +91 -91
- package/skills/dotnet-patterns/SKILL.md +322 -322
- package/skills/dynamic-workflow-mode/SKILL.md +124 -124
- package/skills/e2e-testing/SKILL.md +327 -327
- package/skills/ecc-guide/SKILL.md +190 -190
- package/skills/ecc-recipes/SKILL.md +149 -149
- package/skills/ecc-tools-cost-audit/SKILL.md +161 -161
- package/skills/email-ops/SKILL.md +122 -122
- package/skills/energy-procurement/SKILL.md +228 -228
- package/skills/enterprise-agent-ops/SKILL.md +51 -51
- package/skills/error-handling/SKILL.md +377 -377
- package/skills/eval-harness/SKILL.md +271 -271
- package/skills/evm-token-decimals/SKILL.md +131 -131
- package/skills/exa-search/SKILL.md +108 -108
- package/skills/fal-ai-media/SKILL.md +289 -289
- package/skills/fastapi-patterns/SKILL.md +514 -514
- package/skills/finance-billing-ops/SKILL.md +128 -128
- package/skills/flox-environments/SKILL.md +497 -497
- package/skills/flutter-dart-code-review/SKILL.md +436 -436
- package/skills/foundation-models-on-device/SKILL.md +243 -243
- package/skills/frontend-a11y/SKILL.md +446 -446
- package/skills/frontend-design-direction/SKILL.md +93 -93
- package/skills/frontend-patterns/SKILL.md +657 -657
- package/skills/frontend-slides/SKILL.md +185 -185
- package/skills/fsharp-testing/SKILL.md +281 -281
- package/skills/gan-style-harness/SKILL.md +279 -279
- package/skills/gateguard/SKILL.md +133 -133
- package/skills/generating-python-installer/SKILL.md +820 -820
- package/skills/git-workflow/SKILL.md +716 -716
- package/skills/github-ops/SKILL.md +145 -145
- package/skills/golang-patterns/SKILL.md +676 -676
- package/skills/golang-testing/SKILL.md +721 -721
- package/skills/google-workspace-ops/SKILL.md +96 -96
- package/skills/growth-log/SKILL.md +128 -128
- package/skills/healthcare-cdss-patterns/SKILL.md +246 -246
- package/skills/healthcare-emr-patterns/SKILL.md +160 -160
- package/skills/healthcare-eval-harness/SKILL.md +208 -208
- package/skills/healthcare-phi-compliance/SKILL.md +146 -146
- package/skills/hermes-imports/SKILL.md +89 -89
- package/skills/hexagonal-architecture/SKILL.md +277 -277
- package/skills/hipaa-compliance/SKILL.md +79 -79
- package/skills/homelab-network-readiness/SKILL.md +170 -170
- package/skills/homelab-network-setup/SKILL.md +130 -130
- package/skills/homelab-pihole-dns/SKILL.md +275 -275
- package/skills/homelab-vlan-segmentation/SKILL.md +312 -312
- package/skills/homelab-wireguard-vpn/SKILL.md +306 -306
- package/skills/hookify-rules/SKILL.md +128 -128
- package/skills/inherit-legacy-style/SKILL.md +157 -157
- package/skills/intent-driven-development/SKILL.md +360 -360
- package/skills/inventory-demand-planning/SKILL.md +247 -247
- package/skills/investor-materials/SKILL.md +97 -97
- package/skills/investor-outreach/SKILL.md +92 -92
- package/skills/ios-icon-gen/SKILL.md +158 -158
- package/skills/iterative-retrieval/SKILL.md +212 -212
- package/skills/ito-basket-compare/SKILL.md +64 -64
- package/skills/ito-compute/SKILL.md +129 -129
- package/skills/ito-data-atlas-agent/SKILL.md +64 -64
- package/skills/ito-market-intelligence/SKILL.md +61 -61
- package/skills/ito-trade-planner/SKILL.md +68 -68
- package/skills/java-coding-standards/SKILL.md +384 -384
- package/skills/jira-integration/SKILL.md +303 -303
- package/skills/jpa-patterns/SKILL.md +152 -152
- package/skills/knowledge-ops/SKILL.md +155 -155
- package/skills/kotlin-coroutines-flows/SKILL.md +285 -285
- package/skills/kotlin-exposed-patterns/SKILL.md +720 -720
- package/skills/kotlin-ktor-patterns/SKILL.md +690 -690
- package/skills/kotlin-patterns/SKILL.md +712 -712
- package/skills/kotlin-testing/SKILL.md +825 -825
- package/skills/kubernetes-patterns/SKILL.md +756 -756
- package/skills/laravel-patterns/SKILL.md +416 -416
- package/skills/laravel-plugin-discovery/SKILL.md +230 -230
- package/skills/laravel-security/SKILL.md +948 -948
- package/skills/laravel-tdd/SKILL.md +675 -675
- package/skills/laravel-verification/SKILL.md +180 -180
- package/skills/latency-critical-systems/SKILL.md +75 -75
- package/skills/lead-intelligence/SKILL.md +322 -322
- package/skills/liquid-glass-design/SKILL.md +279 -279
- package/skills/llm-trading-agent-security/SKILL.md +147 -147
- package/skills/logistics-exception-management/SKILL.md +222 -222
- package/skills/loop-design-check/SKILL.md +143 -143
- package/skills/mailtrap-email-integration/SKILL.md +77 -77
- package/skills/make-interfaces-feel-better/SKILL.md +152 -152
- package/skills/manim-video/SKILL.md +90 -90
- package/skills/market-research/SKILL.md +76 -76
- package/skills/marketing-campaign/SKILL.md +114 -114
- package/skills/mcp-server-patterns/SKILL.md +70 -70
- package/skills/messages-ops/SKILL.md +105 -105
- package/skills/ml-adoption-playbook/SKILL.md +57 -57
- package/skills/mle-workflow/SKILL.md +348 -348
- package/skills/motion-advanced/SKILL.md +596 -596
- package/skills/motion-foundations/SKILL.md +299 -299
- package/skills/motion-patterns/SKILL.md +434 -434
- package/skills/motion-ui/SKILL.md +576 -576
- package/skills/mysql-patterns/SKILL.md +413 -413
- package/skills/nanoclaw-repl/SKILL.md +34 -34
- package/skills/nestjs-patterns/SKILL.md +231 -231
- package/skills/netmiko-ssh-automation/SKILL.md +174 -174
- package/skills/network-bgp-diagnostics/SKILL.md +168 -168
- package/skills/network-config-validation/SKILL.md +211 -211
- package/skills/network-interface-health/SKILL.md +153 -153
- package/skills/nextjs-turbopack/SKILL.md +58 -58
- package/skills/nodejs-keccak256/SKILL.md +103 -103
- package/skills/nutrient-document-processing/SKILL.md +168 -168
- package/skills/nuxt4-patterns/SKILL.md +101 -101
- package/skills/openclaw-persona-forge/SKILL.md +289 -289
- package/skills/openclaw-persona-forge/references/avatar-style.md +124 -124
- package/skills/openclaw-persona-forge/references/boundary-rules.md +53 -53
- package/skills/openclaw-persona-forge/references/error-handling.md +53 -53
- package/skills/openclaw-persona-forge/references/identity-tension.md +48 -48
- package/skills/openclaw-persona-forge/references/naming-system.md +39 -39
- package/skills/openclaw-persona-forge/references/output-template.md +166 -166
- package/skills/opensource-pipeline/SKILL.md +256 -256
- package/skills/orch-add-feature/SKILL.md +45 -45
- package/skills/orch-build-mvp/SKILL.md +49 -49
- package/skills/orch-change-feature/SKILL.md +43 -43
- package/skills/orch-fix-defect/SKILL.md +43 -43
- package/skills/orch-pipeline/SKILL.md +121 -121
- package/skills/orch-refine-code/SKILL.md +44 -44
- package/skills/parallel-execution-optimizer/SKILL.md +74 -74
- package/skills/perl-patterns/SKILL.md +505 -505
- package/skills/perl-security/SKILL.md +504 -504
- package/skills/perl-testing/SKILL.md +476 -476
- package/skills/plan-canvas/SKILL.md +153 -153
- package/skills/plan-orchestrate/SKILL.md +263 -263
- package/skills/plankton-code-quality/SKILL.md +237 -237
- package/skills/postgres-patterns/SKILL.md +148 -148
- package/skills/prediction-market-oracle-research/SKILL.md +64 -64
- package/skills/prediction-market-risk-review/SKILL.md +61 -61
- package/skills/premium-web-design/SKILL.md +154 -154
- package/skills/prisma-patterns/SKILL.md +401 -401
- package/skills/product-capability/SKILL.md +142 -142
- package/skills/product-lens/SKILL.md +93 -93
- package/skills/production-audit/SKILL.md +207 -207
- package/skills/production-scheduling/SKILL.md +238 -238
- package/skills/project-flow-ops/SKILL.md +112 -112
- package/skills/prompt-optimizer/SKILL.md +398 -398
- package/skills/python-patterns/SKILL.md +751 -751
- package/skills/python-testing/SKILL.md +817 -817
- package/skills/pytorch-patterns/SKILL.md +397 -397
- package/skills/quality-nonconformance/SKILL.md +260 -260
- package/skills/quarkus-patterns/SKILL.md +723 -723
- package/skills/quarkus-security/SKILL.md +468 -468
- package/skills/quarkus-tdd/SKILL.md +812 -812
- package/skills/quarkus-verification/SKILL.md +480 -480
- package/skills/ralphinho-rfc-pipeline/SKILL.md +68 -68
- package/skills/react-native-patterns/SKILL.md +326 -326
- package/skills/react-patterns/SKILL.md +342 -342
- package/skills/react-performance/SKILL.md +575 -575
- package/skills/react-testing/SKILL.md +424 -424
- package/skills/recsys-pipeline-architect/SKILL.md +115 -115
- package/skills/recursive-decision-ledger/SKILL.md +81 -81
- package/skills/redis-patterns/SKILL.md +404 -404
- package/skills/regex-vs-llm-structured-text/SKILL.md +221 -221
- package/skills/remotion-video-creation/SKILL.md +43 -43
- package/skills/repo-scan/SKILL.md +79 -79
- package/skills/research-ops/SKILL.md +113 -113
- package/skills/returns-reverse-logistics/SKILL.md +240 -240
- package/skills/rules-distill/SKILL.md +265 -265
- package/skills/rust-patterns/SKILL.md +500 -500
- package/skills/rust-testing/SKILL.md +501 -501
- package/skills/safety-guard/SKILL.md +76 -76
- package/skills/santa-method/SKILL.md +307 -307
- package/skills/scientific-db-pubmed-database/SKILL.md +176 -176
- package/skills/scientific-db-uspto-database/SKILL.md +178 -178
- package/skills/scientific-pkg-gget/SKILL.md +167 -167
- package/skills/scientific-thinking-literature-review/SKILL.md +193 -193
- package/skills/scientific-thinking-scholar-evaluation/SKILL.md +161 -161
- package/skills/search-first/SKILL.md +183 -183
- package/skills/security-bounty-hunter/SKILL.md +100 -100
- package/skills/security-review/SKILL.md +504 -504
- package/skills/security-scan/SKILL.md +166 -166
- package/skills/seo/SKILL.md +155 -155
- package/skills/skill-comply/SKILL.md +59 -59
- package/skills/skill-scout/SKILL.md +141 -141
- package/skills/skill-stocktake/SKILL.md +195 -195
- package/skills/social-graph-ranker/SKILL.md +155 -155
- package/skills/social-publisher/SKILL.md +130 -130
- package/skills/springboot-patterns/SKILL.md +315 -315
- package/skills/springboot-security/SKILL.md +273 -273
- package/skills/springboot-tdd/SKILL.md +159 -159
- package/skills/springboot-verification/SKILL.md +232 -232
- package/skills/strategic-compact/SKILL.md +142 -142
- package/skills/swift-actor-persistence/SKILL.md +144 -144
- package/skills/swift-concurrency-6-2/SKILL.md +216 -216
- package/skills/swift-protocol-di-testing/SKILL.md +191 -191
- package/skills/swiftui-patterns/SKILL.md +259 -259
- package/skills/taste/SKILL.md +264 -264
- package/skills/taste/references/genre-taxonomy.md +87 -87
- package/skills/tdd-workflow/SKILL.md +583 -583
- package/skills/team-agent-orchestration/SKILL.md +111 -111
- package/skills/team-builder/SKILL.md +169 -169
- package/skills/terminal-ops/SKILL.md +110 -110
- package/skills/tinystruct-patterns/SKILL.md +279 -279
- package/skills/tinystruct-patterns/references/architecture.md +90 -90
- package/skills/tinystruct-patterns/references/data-handling.md +60 -60
- package/skills/tinystruct-patterns/references/database.md +99 -99
- package/skills/tinystruct-patterns/references/routing.md +64 -64
- package/skills/tinystruct-patterns/references/system-usage.md +97 -97
- package/skills/tinystruct-patterns/references/testing.md +72 -72
- package/skills/token-budget-advisor/SKILL.md +134 -134
- package/skills/ui-demo/SKILL.md +466 -466
- package/skills/ui-to-vue/SKILL.md +135 -135
- package/skills/uncloud/SKILL.md +344 -344
- package/skills/unified-memory/SKILL.md +170 -170
- package/skills/unified-notifications-ops/SKILL.md +188 -188
- package/skills/verification-loop/SKILL.md +129 -129
- package/skills/video-editing/SKILL.md +311 -311
- package/skills/videodb/SKILL.md +375 -375
- package/skills/visa-doc-translate/SKILL.md +117 -117
- package/skills/vite-patterns/SKILL.md +450 -450
- package/skills/vue-patterns/SKILL.md +471 -471
- package/skills/windows-desktop-e2e/SKILL.md +888 -888
- package/skills/workspace-surface-audit/SKILL.md +126 -126
- package/skills/x-api/SKILL.md +235 -235
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// tasteskill / inspect — render the page in a real browser, measure it, and photograph it.
|
|
3
|
+
//
|
|
4
|
+
// node inspect.mjs <url | path/to/file.html> [--out DIR] [--widths 1440,768,390] [--allow rule,rule] [--json]
|
|
5
|
+
//
|
|
6
|
+
// Exit code 0 = no FAIL findings. Exit code 1 = at least one FAIL (the page is not done).
|
|
7
|
+
// Zero dependencies: drives an installed Chrome/Edge/Chromium. Node 22+.
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync, createReadStream, readdirSync, unlinkSync } from 'node:fs';
|
|
9
|
+
import { resolve, join, relative, dirname, extname, sep } from 'node:path';
|
|
10
|
+
import { createServer } from 'node:http';
|
|
11
|
+
import { Browser, sleep } from './lib/cdp.mjs';
|
|
12
|
+
import { probe, frame } from './lib/probe.mjs';
|
|
13
|
+
import { judge } from './lib/rules.mjs';
|
|
14
|
+
|
|
15
|
+
const args = process.argv.slice(2);
|
|
16
|
+
const flag = (name, def) => { const i = args.indexOf('--' + name); if (i === -1) return def; const v = args[i + 1]; args.splice(i, 2); return v; };
|
|
17
|
+
const has = (name) => { const i = args.indexOf('--' + name); if (i === -1) return false; args.splice(i, 1); return true; };
|
|
18
|
+
const outDir = resolve(flag('out', '.taste-skill/inspect'));
|
|
19
|
+
const widths = flag('widths', '1440,768,390').split(',').map(Number).filter(Boolean);
|
|
20
|
+
let allow = flag('allow', '').split(',').filter(Boolean);
|
|
21
|
+
const asJson = has('json');
|
|
22
|
+
const target = args[0];
|
|
23
|
+
if (!target || target === '-h' || target.startsWith('--')) {
|
|
24
|
+
console.error('usage: node inspect.mjs <url | file.html> [--out DIR] [--widths 1440,768,390] [--allow rule,rule] [--json]');
|
|
25
|
+
process.exit(2);
|
|
26
|
+
}
|
|
27
|
+
// Allowances granted by the drawn direction (written by draw.mjs) apply automatically.
|
|
28
|
+
for (const p of ['.taste-skill/direction.json', join(outDir, '..', 'direction.json')]) {
|
|
29
|
+
if (existsSync(p)) { try { allow = [...new Set([...allow, ...(JSON.parse(readFileSync(p, 'utf8')).allow || [])])]; } catch {} break; }
|
|
30
|
+
}
|
|
31
|
+
// A local file is served over http from a throwaway server on 127.0.0.1, so the browser never has to
|
|
32
|
+
// open a file path (some sandboxes show Node and the browser different views of the disk) and relative
|
|
33
|
+
// assets, fetch() and module scripts behave as they will when the page is hosted.
|
|
34
|
+
let url = target; let server = null;
|
|
35
|
+
if (!/^https?:\/\//.test(target)) {
|
|
36
|
+
const file = resolve(target);
|
|
37
|
+
if (!existsSync(file)) { console.error('No such file: ' + file); process.exit(2); }
|
|
38
|
+
const cwd = resolve('.');
|
|
39
|
+
const root = file.startsWith(cwd + sep) ? cwd : dirname(file);
|
|
40
|
+
const MIME = { '.html': 'text/html; charset=utf-8', '.htm': 'text/html; charset=utf-8', '.css': 'text/css; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.mjs': 'text/javascript; charset=utf-8', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp', '.avif': 'image/avif', '.gif': 'image/gif', '.ico': 'image/x-icon', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.otf': 'font/otf', '.mp4': 'video/mp4', '.webm': 'video/webm', '.txt': 'text/plain; charset=utf-8', '.xml': 'application/xml', '.webmanifest': 'application/manifest+json' };
|
|
41
|
+
server = createServer((req, res) => {
|
|
42
|
+
try {
|
|
43
|
+
let f = resolve(join(root, decodeURIComponent(new URL(req.url, 'http://x').pathname)));
|
|
44
|
+
if (f !== root && !f.startsWith(root + sep)) { res.writeHead(403); return res.end(); }
|
|
45
|
+
if (statSync(f).isDirectory()) f = join(f, 'index.html');
|
|
46
|
+
res.writeHead(200, { 'content-type': MIME[extname(f).toLowerCase()] || 'application/octet-stream', 'content-length': statSync(f).size, 'cache-control': 'no-store' });
|
|
47
|
+
createReadStream(f).pipe(res);
|
|
48
|
+
} catch { res.writeHead(404); res.end('not found'); }
|
|
49
|
+
});
|
|
50
|
+
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
51
|
+
url = `http://127.0.0.1:${server.address().port}/${relative(root, file).split(sep).map(encodeURIComponent).join('/')}`;
|
|
52
|
+
}
|
|
53
|
+
const HEIGHT = { 1440: 900, 1280: 800, 1024: 768, 768: 1024, 390: 844, 375: 812 };
|
|
54
|
+
const name = (w) => (w >= 1100 ? 'desktop' : w >= 600 ? 'tablet' : 'mobile');
|
|
55
|
+
|
|
56
|
+
mkdirSync(outDir, { recursive: true });
|
|
57
|
+
// Screenshots from an earlier run would sit beside the new ones when the page got shorter.
|
|
58
|
+
for (const f of readdirSync(outDir)) if (/^(desktop|tablet|mobile)-(fold|page-\d+)\.jpg$/.test(f)) unlinkSync(join(outDir, f));
|
|
59
|
+
const browser = await Browser.launch();
|
|
60
|
+
const helper = await browser.newPage({ width: 1600, height: 1000 });
|
|
61
|
+
const report = { url: server ? target : url, at: new Date().toISOString(), allow, viewports: [], findings: [], images: [] };
|
|
62
|
+
|
|
63
|
+
const MOTION_MIN = 8, MOTION_GOOD = 16; // distinct moving elements over load + one scroll through the page
|
|
64
|
+
// Runs in the page before any of its own scripts: counts the distinct elements that actually move while
|
|
65
|
+
// the page loads and is scrolled (CSS animations, CSS transitions, Web Animations, and script-driven
|
|
66
|
+
// transform/opacity changes such as GSAP), so a page that never moves can be told from one that does.
|
|
67
|
+
const MOTION_RECORDER = `(() => {
|
|
68
|
+
const seen = new WeakSet(); const c = { elements: 0, css: 0, transitions: 0, scripted: 0 };
|
|
69
|
+
const note = (t, k) => { if (!(t instanceof Element) || seen.has(t)) return; seen.add(t); c.elements++; c[k]++; };
|
|
70
|
+
addEventListener('animationstart', (e) => note(e.target, 'css'), true);
|
|
71
|
+
addEventListener('transitionrun', (e) => note(e.target, 'transitions'), true);
|
|
72
|
+
const watch = () => new MutationObserver((list) => { for (const m of list) { const s = m.target.getAttribute && m.target.getAttribute('style') || ''; if (/transform|translate|opacity|scale|rotate|clip-path/.test(s)) note(m.target, 'scripted'); } })
|
|
73
|
+
.observe(document.documentElement, { attributes: true, attributeFilter: ['style'], subtree: true });
|
|
74
|
+
if (document.documentElement) watch(); else addEventListener('DOMContentLoaded', watch);
|
|
75
|
+
window.__motionSample = () => { for (const a of document.getAnimations ? document.getAnimations() : []) { const t = a.effect && a.effect.target; if (t) note(t, a.constructor && a.constructor.name === 'CSSTransition' ? 'transitions' : 'css'); } };
|
|
76
|
+
window.__motion = () => { window.__motionSample(); const v = [...document.querySelectorAll('video')]; return { ...c, videos: v.length, playingVideos: v.filter((x) => x.autoplay || !x.paused).length }; };
|
|
77
|
+
})();`;
|
|
78
|
+
|
|
79
|
+
async function sheet(frames, cols, cellW, cellH, path) {
|
|
80
|
+
const rows = Math.ceil(frames.length / cols);
|
|
81
|
+
await helper.setViewport({ width: cols * cellW, height: rows * cellH, dsf: 1 });
|
|
82
|
+
await helper.eval((a) => {
|
|
83
|
+
document.body.style.cssText = `margin:0;background:#888;display:grid;grid-template-columns:repeat(${a.cols},${a.cellW}px)`;
|
|
84
|
+
document.body.innerHTML = a.frames.map((s) => `<img src="data:image/jpeg;base64,${s}" style="width:${a.cellW}px;height:${a.cellH}px;display:block;outline:1px solid #888">`).join('');
|
|
85
|
+
return Promise.all([...document.images].map((i) => i.decode().catch(() => {})));
|
|
86
|
+
}, { frames, cols, cellW, cellH });
|
|
87
|
+
await helper.screenshot({ path, quality: 76 });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
for (const w of widths) {
|
|
92
|
+
const h = HEIGHT[w] || Math.round(w * 0.625);
|
|
93
|
+
const label = name(w);
|
|
94
|
+
const page = await browser.newPage({ width: w, height: h, dsf: 1, mobile: w < 600 });
|
|
95
|
+
await page.send('Page.addScriptToEvaluateOnNewDocument', { source: MOTION_RECORDER }).catch(() => {});
|
|
96
|
+
const how = await page.goto(url, { timeoutMs: 30000, settleMs: 600 });
|
|
97
|
+
await page.eval('document.fonts.ready.then(() => true)').catch(() => {});
|
|
98
|
+
await sleep(1400); // let the load choreography finish before judging the first screen
|
|
99
|
+
const fold = join(outDir, `${label}-fold.jpg`);
|
|
100
|
+
await page.screenshot({ path: fold, quality: 82 });
|
|
101
|
+
const metrics = await page.eval(probe);
|
|
102
|
+
|
|
103
|
+
// walk the page one screen at a time so scroll reveals fire, photographing each screen
|
|
104
|
+
const frames = [readFileSync(fold).toString('base64')];
|
|
105
|
+
const frameChecks = [await page.eval(frame).catch(() => null)];
|
|
106
|
+
const screens = Math.min(11, Math.max(0, Math.ceil(metrics.page.height / h) - 1));
|
|
107
|
+
for (let i = 1; i <= screens; i++) {
|
|
108
|
+
await page.eval(`window.scrollTo({ top: ${i * h}, behavior: 'instant' })`).catch(() => {});
|
|
109
|
+
await page.wheel(2);
|
|
110
|
+
await sleep(650);
|
|
111
|
+
frames.push(await page.screenshot({ quality: 78 }));
|
|
112
|
+
frameChecks.push(await page.eval(frame).catch(() => null));
|
|
113
|
+
await page.eval('window.__motionSample && window.__motionSample()').catch(() => {});
|
|
114
|
+
}
|
|
115
|
+
const motion = await page.eval('window.__motion ? window.__motion() : null').catch(() => null);
|
|
116
|
+
// re-measure after everything has revealed (catches content that only exists post-reveal)
|
|
117
|
+
await page.eval(`window.scrollTo({ top: 0, behavior: 'instant' })`).catch(() => {});
|
|
118
|
+
await sleep(350);
|
|
119
|
+
const after = await page.eval(probe).catch(() => metrics);
|
|
120
|
+
const m = after.headings.length >= metrics.headings.length ? after : metrics;
|
|
121
|
+
|
|
122
|
+
const cols = w >= 1100 ? 2 : w >= 600 ? 3 : 4;
|
|
123
|
+
const cellW = w >= 1100 ? 800 : w >= 600 ? 512 : 390;
|
|
124
|
+
const cellH = Math.round((cellW * h) / w);
|
|
125
|
+
const perSheet = w >= 1100 ? 4 : w >= 600 ? 6 : 8;
|
|
126
|
+
const sheets = [];
|
|
127
|
+
for (let i = 0; i < frames.length; i += perSheet) {
|
|
128
|
+
const p = join(outDir, `${label}-page-${String(i / perSheet + 1).padStart(2, '0')}.jpg`);
|
|
129
|
+
await sheet(frames.slice(i, i + perSheet), cols, cellW, cellH, p);
|
|
130
|
+
sheets.push(p);
|
|
131
|
+
}
|
|
132
|
+
const findings = judge(m, { width: w, allow });
|
|
133
|
+
// what only shows up while scrolling: text hidden under something, and screens with almost nothing on them
|
|
134
|
+
const coveredText = [...new Map(frameChecks.filter(Boolean).flatMap((f, i) => f.covered.map((c) => [c.text, { ...c, screen: i + 1 }]))).values()];
|
|
135
|
+
if (coveredText.length && !allow.includes('covered-text')) findings.push({ level: 'FAIL', id: 'covered-text', width: w, message: `Large text lies under another element at some scroll position: ` + coveredText.slice(0, 4).map((c) => `"${c.text}" (screen ${c.screen}, ${Math.round(c.share * 100)}% hidden)`).join('; '), fix: 'Nothing may sit on top of a headline or statement. Move the picture, shorten the pinned run, or raise the text above it (z-index) and give it a ground.' });
|
|
136
|
+
const clippedText = [...new Map(frameChecks.filter(Boolean).flatMap((f, i) => (f.clipped || []).map((c) => [c.text, { ...c, screen: i + 1 }]))).values()];
|
|
137
|
+
if (clippedText.length && !allow.includes('clipped-text')) findings.push({ level: 'FAIL', id: 'clipped-text', width: w, message: 'Large text is cut off along its top by the box that holds it: ' + clippedText.slice(0, 3).map((c) => `"${c.text}" (screen ${c.screen})`).join('; '), fix: 'Jurors read a wordmark sliced at the top as broken. Give the box room for the ascenders (line-height and padding), or crop at the bottom or a side edge on purpose.' });
|
|
138
|
+
const overPictures = [...new Map(frameChecks.filter(Boolean).flatMap((f, i) => (f.pinnedOver || []).map((c) => [c.text, { ...c, screen: i + 1 }]))).values()];
|
|
139
|
+
if (overPictures.length && !allow.includes('pinned-over-picture')) findings.push({ level: 'WARN', id: 'pinned-over-picture', width: w, message: 'Pinned large text slides across pictures while the page scrolls: ' + overPictures.slice(0, 3).map((c) => `"${c.text}" (screen ${c.screen}, ${Math.round(c.share * 100)}% over a picture)`).join('; '), fix: 'Jurors read a pinned title crossing drawings or photographs as a collision. Keep the pictures in the column the title does not occupy, or unpin it.' });
|
|
140
|
+
const empty = frameChecks.map((f, i) => ({ f, i })).filter(({ f }) => f && f.filled < 0.2);
|
|
141
|
+
// the same large statement on screen for three screens or more in a row
|
|
142
|
+
const runs = new Map();
|
|
143
|
+
frameChecks.forEach((f, i) => { for (const t of f?.big || []) { const r = runs.get(t) || { start: i, last: i - 1, best: 0 }; if (r.last === i - 1) r.last = i; else { r.start = i; r.last = i; } r.best = Math.max(r.best, r.last - r.start + 1); runs.set(t, r); } });
|
|
144
|
+
const longPins = [...runs.entries()].filter(([, r]) => r.best >= 3);
|
|
145
|
+
if (w >= 1100 && longPins.length && !allow.includes('long-pin')) findings.push({ level: 'WARN', id: 'long-pin', width: w, message: `The same large text stays on screen for ${Math.max(...longPins.map(([, r]) => r.best))} screens in a row: ` + longPins.slice(0, 2).map(([t]) => `"${t}"`).join('; '), fix: 'A pinned element earns two screens, and something must change at each. Shorten the run or give every screen its own content.' });
|
|
146
|
+
// a page that barely moves: counted from what really animated while it loaded and was scrolled
|
|
147
|
+
if (w >= 1100 && motion && !allow.includes('still-page')) {
|
|
148
|
+
const moving = motion.elements + motion.playingVideos * 4;
|
|
149
|
+
if (moving < MOTION_MIN) findings.push({ level: 'FAIL', id: 'still-page', width: w, message: `Only ${motion.elements} element(s) moved while the page loaded and scrolled${motion.playingVideos ? ` (plus ${motion.playingVideos} playing video)` : ''}.`, fix: 'Build the motion layer (SKILL.md step 9, references/motion.md): the signature move, an entrance for the parts of every section, feedback on every control, and one moving picture. Transform and opacity only, with a reduced-motion fallback.' });
|
|
150
|
+
else if (moving < MOTION_GOOD) findings.push({ level: 'WARN', id: 'still-page', width: w, message: `Little motion: ${motion.elements} element(s) moved while the page loaded and scrolled.`, fix: 'Give every section an entrance for its parts (not one fade of the whole block) and add one moving picture: a looping clip or a slow move on a photograph.' });
|
|
151
|
+
}
|
|
152
|
+
if (w >= 1100 && empty.length >= 2 && !allow.includes('empty-screens')) findings.push({ level: 'FAIL', id: 'empty-screens', width: w, message: `${empty.length} of ${frameChecks.length} desktop screens are almost empty ground (under 20% of the screen holds text, a picture or a colour field): screens ${empty.map(({ i }) => i + 1).join(', ')}.`, fix: 'An empty screen is a rest only once per page. Elsewhere it means the section is underbuilt: set its content larger, give it the full information, put it on a colour field, or cut it.' });
|
|
153
|
+
report.viewports.push({ width: w, height: h, load: how, screens: frames.length, filled: frameChecks.map((f) => f?.filled ?? null), motion, metrics: m, consoleErrors: page.consoleErrors.slice(0, 5) });
|
|
154
|
+
report.findings.push(...findings);
|
|
155
|
+
report.images.push({ viewport: label, width: w, fold, pages: sheets });
|
|
156
|
+
await page.close();
|
|
157
|
+
}
|
|
158
|
+
} finally {
|
|
159
|
+
await browser.close();
|
|
160
|
+
if (server) server.close();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// de-duplicate findings that repeat across viewports (keep the widest occurrence, note the others)
|
|
164
|
+
const merged = [];
|
|
165
|
+
for (const f of report.findings) {
|
|
166
|
+
const same = merged.find((x) => x.id === f.id && x.level === f.level);
|
|
167
|
+
if (same && f.id !== 'headline-lines' && f.id !== 'overflow') { same.alsoAt = [...(same.alsoAt || []), f.width]; continue; }
|
|
168
|
+
merged.push(f);
|
|
169
|
+
}
|
|
170
|
+
report.findings = merged;
|
|
171
|
+
writeFileSync(join(outDir, 'report.json'), JSON.stringify(report, null, 1));
|
|
172
|
+
|
|
173
|
+
if (asJson) { console.log(JSON.stringify(report, null, 1)); process.exit(merged.some((f) => f.level === 'FAIL') ? 1 : 0); }
|
|
174
|
+
|
|
175
|
+
const d = report.viewports[0].metrics;
|
|
176
|
+
const line = (s = '') => console.log(s);
|
|
177
|
+
line(`TASTESKILL · INSPECT ${report.url}`);
|
|
178
|
+
line('');
|
|
179
|
+
line('MEASURED (first viewport listed)');
|
|
180
|
+
if (d.hero) line(` hero headline "${d.hero.text.slice(0, 70)}" ${d.hero.words} words · ${d.hero.lines} line(s) · ${d.hero.size}px (${d.hero.vw}vw) · ${d.hero.font} ${d.hero.weight} · tracking ${d.hero.tracking}em · leading ${d.hero.leading}`);
|
|
181
|
+
for (const v of report.viewports.slice(1)) if (v.metrics.hero) line(` @${v.width}px ${v.metrics.hero.lines} line(s) · ${v.metrics.hero.size}px`);
|
|
182
|
+
line(` typefaces ${d.type.families.filter((f) => f.share >= 0.02).map((f) => `${f.family} ${Math.round(f.share * 100)}%`).join(', ')}`);
|
|
183
|
+
line(` sizes in use ${d.type.sizes.join(' / ')} px${d.type.body ? ` body ${d.type.body.size}px / ${d.type.body.leading}` : ''}`);
|
|
184
|
+
line(` colour page ${d.color.bodyBg}${d.color.sectionBgs.length ? ' · sections ' + d.color.sectionBgs.map((s) => s.color).slice(0, 6).join(' ') : ''}${d.color.button ? ' · button ' + d.color.button.bg : ''}`);
|
|
185
|
+
line(` page ${d.page.screens} screens · ${d.page.words} words · ${d.page.img} images · hero media ${d.heroMedia[0] ? Math.round(d.heroMedia[0].ratio * 100) + '% ' + d.heroMedia[0].type : 'none'}`);
|
|
186
|
+
line(` headings ${d.headingSummary.count} · longest ${d.headingSummary.maxLines} line(s) · median ${d.headingSummary.medianWords ?? '-'} words`);
|
|
187
|
+
line('');
|
|
188
|
+
const fails = merged.filter((f) => f.level === 'FAIL'), warns = merged.filter((f) => f.level === 'WARN');
|
|
189
|
+
if (!fails.length && !warns.length) line('FINDINGS none. Measurements are clean — now judge the pictures.');
|
|
190
|
+
for (const [title, list] of [['FAIL — fix every one of these', fails], ['WARN — fix, or be able to defend', warns]]) {
|
|
191
|
+
if (!list.length) continue;
|
|
192
|
+
line(title);
|
|
193
|
+
for (const f of list) {
|
|
194
|
+
line(` [${f.id}] @${f.width}px${f.alsoAt ? ' (+' + f.alsoAt.join(', ') + ')' : ''} ${f.message}`);
|
|
195
|
+
line(` → ${f.fix}`);
|
|
196
|
+
}
|
|
197
|
+
line('');
|
|
198
|
+
}
|
|
199
|
+
line('LOOK AT THE RENDER — open these with your image viewer / Read tool before saying anything about quality:');
|
|
200
|
+
const rel = (p) => relative(process.cwd(), p).split('\\').join('/');
|
|
201
|
+
for (const im of report.images) { line(` ${(im.viewport + ' ' + im.width + 'px').padEnd(15)} ${rel(im.fold)}`); for (const p of im.pages) line(` ${''.padEnd(15)} ${rel(p)}`); }
|
|
202
|
+
line('');
|
|
203
|
+
line(`report: ${rel(join(outDir, 'report.json'))}`);
|
|
204
|
+
line(fails.length ? `RESULT: ${fails.length} FAIL, ${warns.length} WARN — not done.` : `RESULT: 0 FAIL, ${warns.length} WARN — measurements pass. Passing the meter is the floor, not the grade: score the screenshots with references/critique.md.`);
|
|
205
|
+
process.exit(fails.length ? 1 : 0);
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// Zero-dependency Chrome DevTools Protocol client.
|
|
2
|
+
// Drives an installed Chrome / Edge / Chromium in headless mode over Node's built-in WebSocket.
|
|
3
|
+
// Requires Node 22+. No npm install.
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
10
|
+
|
|
11
|
+
export function findChrome() {
|
|
12
|
+
const env = process.env.CHROME_PATH || process.env.PUPPETEER_EXECUTABLE_PATH;
|
|
13
|
+
if (env && existsSync(env)) return env;
|
|
14
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
15
|
+
const local = process.env.LOCALAPPDATA || '';
|
|
16
|
+
const pf = process.env.PROGRAMFILES || 'C:\\Program Files';
|
|
17
|
+
const pf86 = process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)';
|
|
18
|
+
const candidates = [
|
|
19
|
+
// Windows
|
|
20
|
+
join(pf, 'Google/Chrome/Application/chrome.exe'),
|
|
21
|
+
join(pf86, 'Google/Chrome/Application/chrome.exe'),
|
|
22
|
+
join(local, 'Google/Chrome/Application/chrome.exe'),
|
|
23
|
+
join(pf86, 'Microsoft/Edge/Application/msedge.exe'),
|
|
24
|
+
join(pf, 'Microsoft/Edge/Application/msedge.exe'),
|
|
25
|
+
join(pf, 'BraveSoftware/Brave-Browser/Application/brave.exe'),
|
|
26
|
+
// macOS
|
|
27
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
28
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
29
|
+
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
30
|
+
'/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
|
|
31
|
+
join(home, 'Applications/Google Chrome.app/Contents/MacOS/Google Chrome'),
|
|
32
|
+
// Linux
|
|
33
|
+
'/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium',
|
|
34
|
+
'/usr/bin/chromium-browser', '/snap/bin/chromium', '/usr/bin/microsoft-edge',
|
|
35
|
+
];
|
|
36
|
+
for (const c of candidates) if (c && existsSync(c)) return c;
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class Browser {
|
|
41
|
+
static async launch({ width = 1440, height = 900, extraArgs = [] } = {}) {
|
|
42
|
+
if (typeof WebSocket === 'undefined') {
|
|
43
|
+
throw new Error('Node 22+ is required (built-in WebSocket missing). Current: ' + process.version);
|
|
44
|
+
}
|
|
45
|
+
const exe = findChrome();
|
|
46
|
+
if (!exe) {
|
|
47
|
+
throw new Error('No Chrome/Edge/Chromium found. Install one or set CHROME_PATH to its executable.');
|
|
48
|
+
}
|
|
49
|
+
const dir = mkdtempSync(join(tmpdir(), 'ad-chrome-'));
|
|
50
|
+
const args = [
|
|
51
|
+
'--headless=new', '--remote-debugging-port=0', `--user-data-dir=${dir}`,
|
|
52
|
+
'--no-first-run', '--no-default-browser-check', '--mute-audio', '--hide-scrollbars',
|
|
53
|
+
'--disable-background-timer-throttling', '--disable-renderer-backgrounding',
|
|
54
|
+
'--disable-backgrounding-occluded-windows', '--disable-features=Translate,MediaRouter',
|
|
55
|
+
'--force-color-profile=srgb', '--autoplay-policy=no-user-gesture-required',
|
|
56
|
+
'--enable-unsafe-swiftshader', `--window-size=${width},${height}`, ...extraArgs, 'about:blank',
|
|
57
|
+
];
|
|
58
|
+
const proc = spawn(exe, args, { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
59
|
+
const wsUrl = await new Promise((resolve, reject) => {
|
|
60
|
+
let buf = '';
|
|
61
|
+
const t = setTimeout(() => reject(new Error('Chrome did not start within 20s')), 20000);
|
|
62
|
+
proc.stderr.on('data', (d) => {
|
|
63
|
+
buf += d.toString();
|
|
64
|
+
const m = buf.match(/DevTools listening on (ws:\/\/[^\s]+)/);
|
|
65
|
+
if (m) { clearTimeout(t); resolve(m[1]); }
|
|
66
|
+
});
|
|
67
|
+
proc.on('exit', (code) => { clearTimeout(t); reject(new Error('Chrome exited early, code ' + code + '\n' + buf.slice(-600))); });
|
|
68
|
+
});
|
|
69
|
+
const b = new Browser();
|
|
70
|
+
b.proc = proc; b.dir = dir; b.nextId = 1; b.pending = new Map(); b.listeners = new Map();
|
|
71
|
+
b.ws = new WebSocket(wsUrl);
|
|
72
|
+
await new Promise((res, rej) => { b.ws.onopen = res; b.ws.onerror = () => rej(new Error('CDP socket failed')); });
|
|
73
|
+
b.ws.onmessage = (ev) => {
|
|
74
|
+
const msg = JSON.parse(typeof ev.data === 'string' ? ev.data : Buffer.from(ev.data).toString());
|
|
75
|
+
if (msg.id && b.pending.has(msg.id)) {
|
|
76
|
+
const { resolve, reject, timer } = b.pending.get(msg.id);
|
|
77
|
+
clearTimeout(timer); b.pending.delete(msg.id);
|
|
78
|
+
if (msg.error) reject(new Error(msg.error.message)); else resolve(msg.result);
|
|
79
|
+
} else if (msg.method) {
|
|
80
|
+
const fns = b.listeners.get((msg.sessionId || '') + '|' + msg.method);
|
|
81
|
+
if (fns) for (const fn of [...fns]) fn(msg.params);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
return b;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
send(method, params = {}, sessionId, timeoutMs = 30000) {
|
|
88
|
+
const id = this.nextId++;
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
const timer = setTimeout(() => { this.pending.delete(id); reject(new Error('CDP timeout: ' + method)); }, timeoutMs);
|
|
91
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
92
|
+
this.ws.send(JSON.stringify(sessionId ? { id, method, params, sessionId } : { id, method, params }));
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
on(sessionId, method, fn) {
|
|
97
|
+
const k = (sessionId || '') + '|' + method;
|
|
98
|
+
if (!this.listeners.has(k)) this.listeners.set(k, new Set());
|
|
99
|
+
this.listeners.get(k).add(fn);
|
|
100
|
+
return () => this.listeners.get(k)?.delete(fn);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async newPage({ width = 1440, height = 900, dsf = 1, mobile = false, reducedMotion = false } = {}) {
|
|
104
|
+
const { targetId } = await this.send('Target.createTarget', { url: 'about:blank', newWindow: true });
|
|
105
|
+
const { sessionId } = await this.send('Target.attachToTarget', { targetId, flatten: true });
|
|
106
|
+
const p = new Page(this, targetId, sessionId);
|
|
107
|
+
await p.send('Page.enable');
|
|
108
|
+
await p.send('Runtime.enable');
|
|
109
|
+
await p.setViewport({ width, height, dsf, mobile });
|
|
110
|
+
if (reducedMotion) {
|
|
111
|
+
await p.send('Emulation.setEmulatedMedia', { features: [{ name: 'prefers-reduced-motion', value: 'reduce' }] });
|
|
112
|
+
}
|
|
113
|
+
return p;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async close() {
|
|
117
|
+
try { await this.send('Browser.close', {}, undefined, 3000); } catch {}
|
|
118
|
+
try { this.ws.close(); } catch {}
|
|
119
|
+
try { this.proc.kill(); } catch {}
|
|
120
|
+
await sleep(300);
|
|
121
|
+
try { rmSync(this.dir, { recursive: true, force: true, maxRetries: 3 }); } catch {}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export class Page {
|
|
126
|
+
constructor(browser, targetId, sessionId) {
|
|
127
|
+
this.browser = browser; this.targetId = targetId; this.sessionId = sessionId;
|
|
128
|
+
this.consoleErrors = [];
|
|
129
|
+
browser.on(sessionId, 'Runtime.exceptionThrown', (p) => {
|
|
130
|
+
this.consoleErrors.push((p.exceptionDetails?.exception?.description || p.exceptionDetails?.text || '').slice(0, 300));
|
|
131
|
+
});
|
|
132
|
+
// Never let a JS dialog block the page.
|
|
133
|
+
browser.on(sessionId, 'Page.javascriptDialogOpening', () => {
|
|
134
|
+
this.send('Page.handleJavaScriptDialog', { accept: false }).catch(() => {});
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
send(method, params = {}, timeoutMs = 30000) {
|
|
139
|
+
return this.browser.send(method, params, this.sessionId, timeoutMs);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async setViewport({ width, height, dsf = 1, mobile = false }) {
|
|
143
|
+
this.viewport = { width, height, dsf, mobile };
|
|
144
|
+
await this.send('Emulation.setDeviceMetricsOverride', { width, height, deviceScaleFactor: dsf, mobile });
|
|
145
|
+
await this.send('Emulation.setTouchEmulationEnabled', { enabled: !!mobile });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Navigates and resolves after the load event (or timeout), then waits `settleMs` for intros to finish.
|
|
149
|
+
async goto(url, { timeoutMs = 25000, settleMs = 1500 } = {}) {
|
|
150
|
+
let off;
|
|
151
|
+
const loaded = new Promise((resolve) => { off = this.browser.on(this.sessionId, 'Page.loadEventFired', () => resolve('load')); });
|
|
152
|
+
const nav = await this.send('Page.navigate', { url }, timeoutMs).catch((e) => ({ errorText: e.message }));
|
|
153
|
+
if (nav.errorText && !/ERR_ABORTED/.test(nav.errorText)) { off(); throw new Error('navigate: ' + nav.errorText); }
|
|
154
|
+
const how = await Promise.race([loaded, sleep(timeoutMs).then(() => 'timeout')]);
|
|
155
|
+
off();
|
|
156
|
+
await sleep(settleMs);
|
|
157
|
+
return how;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Evaluates a function (serialised) or an expression string in the page and returns its JSON value.
|
|
161
|
+
async eval(fnOrExpr, arg) {
|
|
162
|
+
const expression = typeof fnOrExpr === 'function'
|
|
163
|
+
? `(${fnOrExpr.toString()})(${arg === undefined ? '' : JSON.stringify(arg)})`
|
|
164
|
+
: fnOrExpr;
|
|
165
|
+
const r = await this.send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true }, 30000);
|
|
166
|
+
if (r.exceptionDetails) {
|
|
167
|
+
throw new Error('page eval: ' + (r.exceptionDetails.exception?.description || r.exceptionDetails.text));
|
|
168
|
+
}
|
|
169
|
+
return r.result?.value;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async screenshot({ path, format = 'jpeg', quality = 72, fullPage = false, clip } = {}) {
|
|
173
|
+
const params = { format, captureBeyondViewport: !!fullPage };
|
|
174
|
+
if (format !== 'png') params.quality = quality;
|
|
175
|
+
if (fullPage) {
|
|
176
|
+
const m = await this.send('Page.getLayoutMetrics');
|
|
177
|
+
const s = m.cssContentSize || m.contentSize;
|
|
178
|
+
params.clip = { x: 0, y: 0, width: Math.ceil(s.width), height: Math.min(Math.ceil(s.height), 16000), scale: 1 };
|
|
179
|
+
} else if (clip) {
|
|
180
|
+
params.clip = { scale: 1, ...clip };
|
|
181
|
+
}
|
|
182
|
+
const { data } = await this.send('Page.captureScreenshot', params, 45000);
|
|
183
|
+
if (path) writeFileSync(path, Buffer.from(data, 'base64'));
|
|
184
|
+
return data;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Real wheel input, so smooth-scroll libraries and scroll-driven reveals behave as they do for a visitor.
|
|
188
|
+
async wheel(totalDeltaY, { step = 240, pauseMs = 45 } = {}) {
|
|
189
|
+
const { width, height } = this.viewport;
|
|
190
|
+
const dir = Math.sign(totalDeltaY) || 1;
|
|
191
|
+
let left = Math.abs(totalDeltaY);
|
|
192
|
+
while (left > 0) {
|
|
193
|
+
const d = Math.min(step, left);
|
|
194
|
+
await this.send('Input.dispatchMouseEvent', { type: 'mouseWheel', x: Math.round(width / 2), y: Math.round(height / 2), deltaX: 0, deltaY: d * dir }).catch(() => {});
|
|
195
|
+
left -= d;
|
|
196
|
+
await sleep(pauseMs);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async close() {
|
|
201
|
+
try { await this.browser.send('Target.closeTarget', { targetId: this.targetId }, undefined, 5000); } catch {}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export { sleep };
|