@kensaurus/skills 0.0.0-stage → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +53 -0
- package/.claude-plugin/plugin.json +40 -0
- package/.cursor-plugin/plugin.json +38 -0
- package/.mcp.json +28 -0
- package/CHANGELOG.md +1790 -0
- package/LICENSE +21 -0
- package/LICENSE-APACHE +62 -0
- package/NOTICE +13 -0
- package/README.md +821 -2
- package/SECURITY.md +55 -0
- package/agents/code-reviewer.md +60 -0
- package/agents/completion-judge.md +89 -0
- package/agents/db-migrator.md +125 -0
- package/agents/debugger.md +47 -0
- package/agents/deploy-checker.md +100 -0
- package/agents/perf-monitor.md +74 -0
- package/assets/favicon.png +0 -0
- package/assets/logo-light.png +0 -0
- package/assets/logo.png +0 -0
- package/assets/logo.svg +6 -0
- package/assets/og.png +0 -0
- package/bin/install.mjs +1127 -0
- package/bin/kenji.js +2 -0
- package/commands/adr.md +17 -0
- package/commands/aeo-plan.md +18 -0
- package/commands/arch-boundaries.md +17 -0
- package/commands/aso-plan.md +18 -0
- package/commands/auth-flows.md +19 -0
- package/commands/backup-plan.md +17 -0
- package/commands/burndown-full.md +25 -0
- package/commands/capacitor-plan.md +18 -0
- package/commands/codemod-safety.md +22 -0
- package/commands/commit.md +18 -0
- package/commands/complete-everything.md +40 -0
- package/commands/cost-plan.md +19 -0
- package/commands/deadcode-plan.md +26 -0
- package/commands/deadcode.md +32 -0
- package/commands/debug-issue.md +17 -0
- package/commands/deps-plan.md +18 -0
- package/commands/docs-plan.md +17 -0
- package/commands/doctrine.md +19 -0
- package/commands/error-plan.md +19 -0
- package/commands/feedback-to-closure.md +36 -0
- package/commands/fix-issue.md +76 -0
- package/commands/gate-logic.md +26 -0
- package/commands/green-repo.md +36 -0
- package/commands/grill-me.md +19 -0
- package/commands/gtm-plan.md +21 -0
- package/commands/gtm-weekly.md +17 -0
- package/commands/gtm.md +22 -0
- package/commands/handoff.md +15 -0
- package/commands/housekeep-backlog.md +18 -0
- package/commands/housekeep-files.md +22 -0
- package/commands/housekeep-gates.md +18 -0
- package/commands/instant-nav.md +11 -0
- package/commands/integrity-plan.md +19 -0
- package/commands/launch-kit.md +16 -0
- package/commands/mcp-guide.md +40 -0
- package/commands/mobile-plan.md +19 -0
- package/commands/native-rn-monorepo/README.md +78 -0
- package/commands/native-rn-monorepo/android-build.md +26 -0
- package/commands/native-rn-monorepo/android-install.md +32 -0
- package/commands/native-rn-monorepo/android-logcat.md +37 -0
- package/commands/native-rn-monorepo/ios-ci-logs.md +56 -0
- package/commands/native-rn-monorepo/ios-ci-status.md +52 -0
- package/commands/native-rn-monorepo/ios-ci-trigger.md +59 -0
- package/commands/native-rn-monorepo/rn-reset.md +50 -0
- package/commands/native-rn-monorepo/rn-ship-ios.md +67 -0
- package/commands/native-rn-monorepo/rn-verify.md +53 -0
- package/commands/perf-plan.md +18 -0
- package/commands/plan-mode.md +74 -0
- package/commands/pr.md +16 -0
- package/commands/pricing-plan.md +20 -0
- package/commands/privacy-plan.md +18 -0
- package/commands/readability.md +12 -0
- package/commands/readme.md +15 -0
- package/commands/refactor.md +15 -0
- package/commands/release-prep.md +17 -0
- package/commands/research.md +25 -0
- package/commands/responsive-audit.md +21 -0
- package/commands/review-code.md +18 -0
- package/commands/rls-plan.md +18 -0
- package/commands/secrets-plan.md +18 -0
- package/commands/security-plan.md +19 -0
- package/commands/ship-and-observe.md +36 -0
- package/commands/skill-conflicts.md +19 -0
- package/commands/slop-plan.md +18 -0
- package/commands/stub-plan.md +18 -0
- package/commands/test-mutation.md +16 -0
- package/commands/test-plan.md +17 -0
- package/commands/test.md +29 -0
- package/commands/thirdparty-web-interface-guidelines.md +185 -0
- package/commands/uiux-plan.md +18 -0
- package/commands/uiux.md +45 -0
- package/commands/update-deps.md +21 -0
- package/commands/validation-plan.md +19 -0
- package/commands-portable/fix-issue.md +72 -0
- package/commands-portable/plan-mode.md +92 -0
- package/commands-portable/research.md +91 -0
- package/docs/screenshots/README.md +5 -0
- package/docs/screenshots/audit-dark.png +0 -0
- package/docs/screenshots/build-dark.png +0 -0
- package/docs/screenshots/grill-dark.png +0 -0
- package/docs/screenshots/hero-dark.png +0 -0
- package/docs/screenshots/hero-light.png +0 -0
- package/docs/screenshots/ship-dark.png +0 -0
- package/docs/screenshots/src/showcase.html +320 -0
- package/hooks/completion-gate.mjs +258 -0
- package/hooks/cursor-hooks.json +13 -0
- package/hooks/hooks.json +15 -0
- package/install.sh +21 -0
- package/llms.txt +40 -0
- package/mcp/README.md +266 -0
- package/mcp/VERSIONS.md +41 -0
- package/mcp/mcp-full.json.template +124 -0
- package/mcp/mcp.json.template +29 -0
- package/mcp/pinned-versions.json +27 -0
- package/package.json +94 -4
- package/rules/approved-plan-execution.mdc +65 -0
- package/rules/full-stack-ship-discipline.mdc +37 -0
- package/rules/native-rn-monorepo/README.md +63 -0
- package/rules/native-rn-monorepo/_project.mdc +69 -0
- package/rules/native-rn-monorepo/native-android.mdc +72 -0
- package/rules/native-rn-monorepo/native-ios.mdc +61 -0
- package/rules/native-rn-monorepo/react-native-js.mdc +78 -0
- package/rules/native-rn-monorepo/web.mdc +60 -0
- package/rules/project-starter/components.mdc +54 -0
- package/rules/project-starter/data-fetching.mdc +77 -0
- package/rules/project-starter/git.mdc +41 -0
- package/rules/project-starter/supabase.mdc +37 -0
- package/rules/project-starter/tailwind.mdc +48 -0
- package/rules/project-starter/typescript.mdc +36 -0
- package/rules/project-starter/web-performance.mdc +42 -0
- package/rules/senior-engineer.mdc +30 -0
- package/rules/shell-first-search.mdc +19 -0
- package/rules/skill-workflows.mdc +35 -0
- package/rules/verification-before-completion.mdc +57 -0
- package/skills/audit-accessibility/SKILL.md +441 -0
- package/skills/audit-agent-speed/SKILL.md +181 -0
- package/skills/audit-agent-speed/scripts/stop-typecheck.mjs +151 -0
- package/skills/audit-analytics/SKILL.md +138 -0
- package/skills/audit-auth-flows/SKILL.md +267 -0
- package/skills/audit-backend-architecture/SKILL.md +266 -0
- package/skills/audit-backend-architecture/references/patterns.md +386 -0
- package/skills/audit-bundle-size/SKILL.md +295 -0
- package/skills/audit-cicd/SKILL.md +218 -0
- package/skills/audit-code-quality/SKILL.md +314 -0
- package/skills/audit-code-review/SKILL.md +289 -0
- package/skills/audit-codemod-safety/SKILL.md +159 -0
- package/skills/audit-db-schema/SKILL.md +465 -0
- package/skills/audit-db-schema/references/details.md +110 -0
- package/skills/audit-doctrine/SKILL.md +189 -0
- package/skills/audit-env-parity/SKILL.md +133 -0
- package/skills/audit-fe-api/SKILL.md +458 -0
- package/skills/audit-gate-logic/SKILL.md +219 -0
- package/skills/audit-i18n/SKILL.md +339 -0
- package/skills/audit-infra-cost/SKILL.md +142 -0
- package/skills/audit-langfuse-llm/SKILL.md +468 -0
- package/skills/audit-langfuse-llm/references/details.md +226 -0
- package/skills/audit-llm-security/SKILL.md +147 -0
- package/skills/audit-monetization-iap/SKILL.md +137 -0
- package/skills/audit-payment-system/SKILL.md +268 -0
- package/skills/audit-payment-system/references/checklist.md +283 -0
- package/skills/audit-performance/SKILL.md +383 -0
- package/skills/audit-performance/references/loading-priority-2026.md +81 -0
- package/skills/audit-realworld/SKILL.md +287 -0
- package/skills/audit-registry-listing/SKILL.md +122 -0
- package/skills/audit-resilience/SKILL.md +153 -0
- package/skills/audit-responsive/SKILL.md +221 -0
- package/skills/audit-responsive/references/checklist.md +166 -0
- package/skills/audit-security/SKILL.md +289 -0
- package/skills/audit-skill-conflicts/SKILL.md +178 -0
- package/skills/audit-ui-states/SKILL.md +146 -0
- package/skills/audit-uiux-design-system/SKILL.md +475 -0
- package/skills/audit-uiux-design-system/references/details.md +71 -0
- package/skills/audit-ux/SKILL.md +381 -0
- package/skills/audit-ux/references/details.md +245 -0
- package/skills/audit-ux-journeys/SKILL.md +215 -0
- package/skills/audit-ux-journeys/references/checklist.md +179 -0
- package/skills/backend-db-performance/SKILL.md +441 -0
- package/skills/backend-error-handling/SKILL.md +489 -0
- package/skills/backend-error-handling/references/details.md +58 -0
- package/skills/backend-observability/SKILL.md +88 -0
- package/skills/backend-patterns/SKILL.md +499 -0
- package/skills/backend-patterns/references/architecture-patterns.md +298 -0
- package/skills/backend-realtime/SKILL.md +403 -0
- package/skills/backend-realtime/references/patterns.md +74 -0
- package/skills/burndown-full/SKILL.md +174 -0
- package/skills/complete-everything/SKILL.md +295 -0
- package/skills/data-pipeline/SKILL.md +109 -0
- package/skills/data-visualization/SKILL.md +488 -0
- package/skills/debug-error/SKILL.md +322 -0
- package/skills/debug-fe-be-integration/SKILL.md +459 -0
- package/skills/debug-sentry-monitor/SKILL.md +497 -0
- package/skills/debug-sentry-monitor/references/details.md +165 -0
- package/skills/deploy-npm/SKILL.md +394 -0
- package/skills/deploy-npm/references/example-mushi-mushi.md +52 -0
- package/skills/deploy-verify/SKILL.md +489 -0
- package/skills/design-api/SKILL.md +379 -0
- package/skills/design-canvas/SKILL.md +155 -0
- package/skills/design-email/SKILL.md +370 -0
- package/skills/design-frontend/SKILL.md +143 -0
- package/skills/design-generative-art/SKILL.md +474 -0
- package/skills/design-mobile-first/SKILL.md +506 -0
- package/skills/design-motion/SKILL.md +333 -0
- package/skills/design-motion/references/delight-interactions.md +191 -0
- package/skills/design-prd/SKILL.md +443 -0
- package/skills/design-system/SKILL.md +457 -0
- package/skills/design-theme/SKILL.md +226 -0
- package/skills/design-theme/themes/tsumagoi-ranch.md +150 -0
- package/skills/docs-adr/SKILL.md +168 -0
- package/skills/docs-coauthor/SKILL.md +368 -0
- package/skills/docs-comparison-pages/SKILL.md +117 -0
- package/skills/docs-domain-modeling/SKILL.md +97 -0
- package/skills/docs-launch-kit/SKILL.md +139 -0
- package/skills/docs-writer/SKILL.md +469 -0
- package/skills/enhance-agent-guardrails/SKILL.md +164 -0
- package/skills/enhance-arch-boundaries/SKILL.md +154 -0
- package/skills/enhance-capacitor-ui/SKILL.md +463 -0
- package/skills/enhance-capacitor-ui/references/details.md +750 -0
- package/skills/enhance-email-deliverability/SKILL.md +143 -0
- package/skills/enhance-growth-loops/SKILL.md +122 -0
- package/skills/enhance-lifecycle-email/SKILL.md +130 -0
- package/skills/enhance-motion/SKILL.md +193 -0
- package/skills/enhance-onboarding/SKILL.md +148 -0
- package/skills/enhance-pwa/SKILL.md +303 -0
- package/skills/enhance-readability/SKILL.md +146 -0
- package/skills/enhance-readme/SKILL.md +496 -0
- package/skills/enhance-readme/package-lock.json +187 -0
- package/skills/enhance-readme/package.json +17 -0
- package/skills/enhance-readme/scripts/generate-readme-blocks.mjs +199 -0
- package/skills/enhance-readme/scripts/record-readme-tour.mjs +442 -0
- package/skills/enhance-skill-prompts/SKILL.md +167 -0
- package/skills/enhance-skill-prompts/references/exemplar-audit-auth-flows.md +311 -0
- package/skills/enhance-ux-laws/SKILL.md +409 -0
- package/skills/enhance-web-conversion/SKILL.md +155 -0
- package/skills/enhance-web-forms/SKILL.md +154 -0
- package/skills/enhance-web-instant-nav/SKILL.md +138 -0
- package/skills/enhance-web-instant-nav/references/bfcache-blockers.md +23 -0
- package/skills/enhance-web-instant-nav/references/early-hints.md +33 -0
- package/skills/enhance-web-instant-nav/references/speculation-rules.md +44 -0
- package/skills/enhance-web-landing/SKILL.md +459 -0
- package/skills/enhance-web-landing/references/details.md +773 -0
- package/skills/enhance-web-redesign/SKILL.md +228 -0
- package/skills/enhance-web-seo/SKILL.md +276 -0
- package/skills/enhance-web-ui/SKILL.md +473 -0
- package/skills/enhance-web-ui/references/details.md +674 -0
- package/skills/enhance-web-ux/HEURISTICS.md +248 -0
- package/skills/enhance-web-ux/PATTERNS.md +375 -0
- package/skills/enhance-web-ux/SKILL.md +464 -0
- package/skills/enhance-web-ux/examples.md +222 -0
- package/skills/enhance-web-ux/references/details.md +406 -0
- package/skills/enhance-web-web3d/SKILL.md +397 -0
- package/skills/enhance-web-web3d/references/css-canvas-effects.md +180 -0
- package/skills/handoff/SKILL.md +66 -0
- package/skills/housekeep-backlog/SKILL.md +149 -0
- package/skills/housekeep-dead-code/SKILL.md +387 -0
- package/skills/housekeep-dead-code/references/ratchet-ci.md +205 -0
- package/skills/housekeep-dead-code/references/supabase-hygiene.md +152 -0
- package/skills/housekeep-design/SKILL.md +207 -0
- package/skills/housekeep-files/SKILL.md +220 -0
- package/skills/housekeep-files/references/naming-and-catalog.md +86 -0
- package/skills/housekeep-files/scripts/housekeep-files.ps1 +360 -0
- package/skills/housekeep-files/scripts/housekeep-files.sh +238 -0
- package/skills/housekeep-gates/SKILL.md +174 -0
- package/skills/iterate-agent-harness/SKILL.md +137 -0
- package/skills/iterate-gtm-weekly/SKILL.md +103 -0
- package/skills/iterate-post-launch/SKILL.md +292 -0
- package/skills/meta-mcp-builder/SKILL.md +313 -0
- package/skills/meta-skill-creator/SKILL.md +304 -0
- package/skills/mobile-capacitor-platform/SKILL.md +103 -0
- package/skills/mobile-emulator-start/SKILL.md +296 -0
- package/skills/mobile-emulator-test/SKILL.md +491 -0
- package/skills/mobile-emulator-test/references/details.md +478 -0
- package/skills/mobile-rn-performance/SKILL.md +107 -0
- package/skills/mobile-rn-screen/SKILL.md +476 -0
- package/skills/mobile-rn-screen/references/details.md +785 -0
- package/skills/mushi-health/SKILL.md +206 -0
- package/skills/mushi-integration/SKILL.md +257 -0
- package/skills/plan-aeo-readiness/SKILL.md +166 -0
- package/skills/plan-antislop/SKILL.md +281 -0
- package/skills/plan-aso/SKILL.md +149 -0
- package/skills/plan-backup-dr/SKILL.md +131 -0
- package/skills/plan-capacitor-hardening/SKILL.md +217 -0
- package/skills/plan-data-integrity/SKILL.md +187 -0
- package/skills/plan-dead-code/SKILL.md +386 -0
- package/skills/plan-dead-code/references/knip-config.md +214 -0
- package/skills/plan-dead-code/references/output-templates.md +133 -0
- package/skills/plan-dead-code/references/preservation-contract.md +50 -0
- package/skills/plan-dead-code/references/residue-greps.md +84 -0
- package/skills/plan-dependency-provenance/SKILL.md +200 -0
- package/skills/plan-docs-sync/SKILL.md +143 -0
- package/skills/plan-docs-sync/references/drift-taxonomy.md +43 -0
- package/skills/plan-docs-sync/references/output-templates.md +33 -0
- package/skills/plan-docs-sync/references/preservation-contract.md +17 -0
- package/skills/plan-error-handling/SKILL.md +205 -0
- package/skills/plan-gtm/SKILL.md +276 -0
- package/skills/plan-gtm/references/benchmarks-2026.md +183 -0
- package/skills/plan-input-validation/SKILL.md +179 -0
- package/skills/plan-llm-cost-guardrails/SKILL.md +176 -0
- package/skills/plan-mobile-readiness/SKILL.md +171 -0
- package/skills/plan-perf-audit/SKILL.md +145 -0
- package/skills/plan-perf-audit/references/audit-scope.md +51 -0
- package/skills/plan-perf-audit/references/output-templates.md +33 -0
- package/skills/plan-perf-audit/references/preservation-contract.md +13 -0
- package/skills/plan-pricing/SKILL.md +173 -0
- package/skills/plan-privacy-compliance/SKILL.md +148 -0
- package/skills/plan-rls-audit/SKILL.md +231 -0
- package/skills/plan-secrets-audit/SKILL.md +181 -0
- package/skills/plan-security-audit/SKILL.md +168 -0
- package/skills/plan-security-audit/references/output-templates.md +36 -0
- package/skills/plan-security-audit/references/owasp-supabase-scope.md +55 -0
- package/skills/plan-security-audit/references/preservation-contract.md +18 -0
- package/skills/plan-stub-checker/SKILL.md +216 -0
- package/skills/plan-stub-checker/references/detection-methodology.md +75 -0
- package/skills/plan-stub-checker/references/detection-taxonomy.md +34 -0
- package/skills/plan-stub-checker/references/output-templates.md +63 -0
- package/skills/plan-stub-checker/references/preservation-contract.md +24 -0
- package/skills/plan-test-coverage/SKILL.md +170 -0
- package/skills/plan-test-coverage/references/methodology.md +54 -0
- package/skills/plan-test-coverage/references/output-templates.md +34 -0
- package/skills/plan-test-coverage/references/preservation-contract.md +15 -0
- package/skills/plan-uiux-unification/SKILL.md +230 -0
- package/skills/plan-uiux-unification/references/output-templates.md +67 -0
- package/skills/plan-uiux-unification/references/phase-workbook.md +85 -0
- package/skills/plan-uiux-unification/references/preservation-contract.md +24 -0
- package/skills/protocol-browser-anti-stall/SKILL.md +211 -0
- package/skills/protocol-browser-anti-stall/references/mcp-to-cli-map.md +113 -0
- package/skills/protocol-browser-anti-stall/references/playwright-session-coordination.md +170 -0
- package/skills/research/SKILL.md +422 -0
- package/skills/test-exploratory/SKILL.md +165 -0
- package/skills/test-exploratory/references/charter-template.md +29 -0
- package/skills/test-load/SKILL.md +126 -0
- package/skills/test-mutation/SKILL.md +160 -0
- package/skills/test-playwright/SKILL.md +354 -0
- package/skills/test-qa/SKILL.md +364 -0
- package/skills/test-qa/references/details.md +268 -0
- package/skills/test-red-team/SKILL.md +386 -0
- package/skills/test-red-team/references/owasp-attack-checklist.md +193 -0
- package/skills/test-unit/SKILL.md +259 -0
- package/skills/test-unit/references/details.md +267 -0
- package/skills/test-visual-regression/SKILL.md +132 -0
- package/skills/thirdparty-emil-design-eng/ATTRIBUTION.md +20 -0
- package/skills/thirdparty-emil-design-eng/SKILL.md +21 -0
- package/skills/thirdparty-emil-design-eng/references/emil-design-eng.md +676 -0
- package/skills/thirdparty-ui-ux-pro-max/ATTRIBUTION.md +22 -0
- package/skills/thirdparty-ui-ux-pro-max/SKILL.md +304 -0
- package/skills/thirdparty-ui-ux-pro-max/data/charts.csv +26 -0
- package/skills/thirdparty-ui-ux-pro-max/data/colors.csv +97 -0
- package/skills/thirdparty-ui-ux-pro-max/data/icons.csv +101 -0
- package/skills/thirdparty-ui-ux-pro-max/data/landing.csv +31 -0
- package/skills/thirdparty-ui-ux-pro-max/data/products.csv +97 -0
- package/skills/thirdparty-ui-ux-pro-max/data/react-performance.csv +45 -0
- package/skills/thirdparty-ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/skills/thirdparty-ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/skills/thirdparty-ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/skills/thirdparty-ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/skills/thirdparty-ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/skills/thirdparty-ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/skills/thirdparty-ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/skills/thirdparty-ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/skills/thirdparty-ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/skills/thirdparty-ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/skills/thirdparty-ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/skills/thirdparty-ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/skills/thirdparty-ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/skills/thirdparty-ui-ux-pro-max/data/styles.csv +68 -0
- package/skills/thirdparty-ui-ux-pro-max/data/typography.csv +58 -0
- package/skills/thirdparty-ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/skills/thirdparty-ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/skills/thirdparty-ui-ux-pro-max/data/web-interface.csv +31 -0
- package/skills/thirdparty-ui-ux-pro-max/scripts/core.py +253 -0
- package/skills/thirdparty-ui-ux-pro-max/scripts/design_system.py +1067 -0
- package/skills/thirdparty-ui-ux-pro-max/scripts/search.py +114 -0
- package/skills/thirdparty-web-interface-guidelines/ATTRIBUTION.md +23 -0
- package/skills/thirdparty-web-interface-guidelines/SKILL.md +190 -0
- package/skills/workflow-build-feature/SKILL.md +118 -0
- package/skills/workflow-coding-discipline/SKILL.md +140 -0
- package/skills/workflow-environment-ready/SKILL.md +128 -0
- package/skills/workflow-feature-flag/SKILL.md +262 -0
- package/skills/workflow-feedback-to-closure/SKILL.md +165 -0
- package/skills/workflow-fix-and-ship/SKILL.md +136 -0
- package/skills/workflow-git-commit/SKILL.md +200 -0
- package/skills/workflow-green-repo/SKILL.md +166 -0
- package/skills/workflow-grilling/SKILL.md +73 -0
- package/skills/workflow-gtm/SKILL.md +153 -0
- package/skills/workflow-housekeep/SKILL.md +453 -0
- package/skills/workflow-housekeep/references/templates.md +109 -0
- package/skills/workflow-launch-ready/SKILL.md +145 -0
- package/skills/workflow-merge-conflicts/SKILL.md +62 -0
- package/skills/workflow-onboard/SKILL.md +99 -0
- package/skills/workflow-parallel-agents/SKILL.md +164 -0
- package/skills/workflow-pr/SKILL.md +197 -0
- package/skills/workflow-quality-gate/SKILL.md +147 -0
- package/skills/workflow-refactor/SKILL.md +274 -0
- package/skills/workflow-release-prep/SKILL.md +207 -0
- package/skills/workflow-ship-and-observe/SKILL.md +164 -0
- package/skills/workflow-spec-tdd/SKILL.md +141 -0
- package/skills/workflow-spec-tdd/references/spec-template.md +126 -0
- package/skills/workflow-spec-tdd/references/tdd-patterns.md +167 -0
- package/skills-cursor/babysit/SKILL.md +17 -0
- package/skills-cursor/canvas/SKILL.md +142 -0
- package/skills-cursor/canvas/sdk/canvas-tokens.d.ts +235 -0
- package/skills-cursor/canvas/sdk/chart-primitives.d.ts +200 -0
- package/skills-cursor/canvas/sdk/dag-layout.d.ts +102 -0
- package/skills-cursor/canvas/sdk/diff-view.d.ts +130 -0
- package/skills-cursor/canvas/sdk/form-primitives.d.ts +194 -0
- package/skills-cursor/canvas/sdk/hooks.d.ts +117 -0
- package/skills-cursor/canvas/sdk/index.d.ts +47 -0
- package/skills-cursor/canvas/sdk/theme.d.ts +61 -0
- package/skills-cursor/canvas/sdk/todo-list.d.ts +49 -0
- package/skills-cursor/canvas/sdk/ui-primitives.d.ts +549 -0
- package/skills-cursor/canvas/sdk/ui-primitives.test.d.ts +2 -0
- package/skills-cursor/create-hook/SKILL.md +238 -0
- package/skills-cursor/create-rule/SKILL.md +185 -0
- package/skills-cursor/create-skill/SKILL.md +269 -0
- package/skills-cursor/create-skill/references/authoring-guide.md +182 -0
- package/skills-cursor/create-subagent/SKILL.md +228 -0
- package/skills-cursor/migrate-to-skills/SKILL.md +121 -0
- package/skills-cursor/shell/SKILL.md +22 -0
- package/skills-cursor/split-to-prs/SKILL.md +47 -0
- package/skills-cursor/statusline/SKILL.md +193 -0
- package/skills-cursor/update-cli-config/SKILL.md +85 -0
- package/skills-cursor/update-cursor-settings/SKILL.md +137 -0
- package/skills.sh.json +297 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "enhance-readme",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [],
|
|
10
|
+
"author": "",
|
|
11
|
+
"license": "ISC",
|
|
12
|
+
"type": "commonjs",
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
|
15
|
+
"playwright": "^1.59.1"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Generate hero <picture> + Tour grid markdown from docs/screenshots/
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// node generate-readme-blocks.mjs [--dir=docs/screenshots] [--demo-url=https://example.com] [--hero=<basename-without-ext>]
|
|
6
|
+
//
|
|
7
|
+
// Output:
|
|
8
|
+
// - Prints two ready-to-paste blocks: HERO and TOUR
|
|
9
|
+
// - Prints size/limit warnings to stderr (10 MB hard limit per file on GitHub)
|
|
10
|
+
// - Lists unpaired dark/light screenshots so you can fix them
|
|
11
|
+
|
|
12
|
+
import { readdirSync, statSync } from "node:fs";
|
|
13
|
+
import { join, parse, relative, sep, posix } from "node:path";
|
|
14
|
+
|
|
15
|
+
const args = Object.fromEntries(
|
|
16
|
+
process.argv.slice(2).map((a) => {
|
|
17
|
+
const [k, ...rest] = a.replace(/^--/, "").split("=");
|
|
18
|
+
return [k, rest.length ? rest.join("=") : true];
|
|
19
|
+
}),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const SCREENSHOT_DIR = args.dir || "docs/screenshots";
|
|
23
|
+
const DEMO_URL = args["demo-url"] || "https://your-live-demo.example.com/";
|
|
24
|
+
const FORCED_HERO = args.hero || null;
|
|
25
|
+
const HARD_LIMIT_MB = 10;
|
|
26
|
+
const SOFT_LIMIT_MB = 5;
|
|
27
|
+
|
|
28
|
+
let files;
|
|
29
|
+
try {
|
|
30
|
+
files = readdirSync(SCREENSHOT_DIR)
|
|
31
|
+
.filter((f) => /\.(png|jpe?g|webp|gif)$/i.test(f))
|
|
32
|
+
.map((f) => {
|
|
33
|
+
const full = join(SCREENSHOT_DIR, f);
|
|
34
|
+
const { size } = statSync(full);
|
|
35
|
+
return { name: f, full, size, base: parse(f).name };
|
|
36
|
+
})
|
|
37
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.error(`error: cannot read ${SCREENSHOT_DIR} — ${err.message}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (files.length === 0) {
|
|
44
|
+
console.error(`error: no images found in ${SCREENSHOT_DIR}`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const toPosix = (p) => p.split(sep).join(posix.sep);
|
|
49
|
+
const human = (bytes) =>
|
|
50
|
+
bytes >= 1024 * 1024
|
|
51
|
+
? `${(bytes / 1024 / 1024).toFixed(2)} MB`
|
|
52
|
+
: `${(bytes / 1024).toFixed(0)} KB`;
|
|
53
|
+
|
|
54
|
+
const oversized = files.filter((f) => f.size > HARD_LIMIT_MB * 1024 * 1024);
|
|
55
|
+
const warnSized = files.filter(
|
|
56
|
+
(f) =>
|
|
57
|
+
f.size > SOFT_LIMIT_MB * 1024 * 1024 && f.size <= HARD_LIMIT_MB * 1024 * 1024,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
if (oversized.length) {
|
|
61
|
+
console.error(`\nERROR: ${oversized.length} file(s) exceed GitHub's 10 MB hard limit:`);
|
|
62
|
+
for (const f of oversized) console.error(` ${f.name} — ${human(f.size)}`);
|
|
63
|
+
}
|
|
64
|
+
if (warnSized.length) {
|
|
65
|
+
console.error(`\nWARN: ${warnSized.length} file(s) exceed ${SOFT_LIMIT_MB} MB (consider compressing):`);
|
|
66
|
+
for (const f of warnSized) console.error(` ${f.name} — ${human(f.size)}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const pairs = new Map();
|
|
70
|
+
const orphans = [];
|
|
71
|
+
for (const f of files) {
|
|
72
|
+
const m = f.base.match(/^(.+)-(dark|light)$/i);
|
|
73
|
+
if (!m) {
|
|
74
|
+
orphans.push(f);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const [, key, mode] = m;
|
|
78
|
+
if (!pairs.has(key)) pairs.set(key, {});
|
|
79
|
+
pairs.get(key)[mode.toLowerCase()] = f;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const completePairs = [...pairs.entries()].filter(
|
|
83
|
+
([, v]) => v.dark && v.light,
|
|
84
|
+
);
|
|
85
|
+
const halfPairs = [...pairs.entries()].filter(([, v]) => !v.dark || !v.light);
|
|
86
|
+
|
|
87
|
+
if (halfPairs.length) {
|
|
88
|
+
console.error(`\nWARN: ${halfPairs.length} screenshot(s) missing dark/light pair:`);
|
|
89
|
+
for (const [k, v] of halfPairs) {
|
|
90
|
+
const have = v.dark ? "dark" : "light";
|
|
91
|
+
const need = v.dark ? "light" : "dark";
|
|
92
|
+
console.error(` ${k}: have ${have}.png, missing ${k}-${need}.png`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const heroPriority = ["hero", "dashboard", "home", "landing", "overview", "main"];
|
|
97
|
+
const pickHero = () => {
|
|
98
|
+
if (FORCED_HERO) {
|
|
99
|
+
const found = pairs.get(FORCED_HERO);
|
|
100
|
+
if (!found?.dark) {
|
|
101
|
+
console.error(`error: --hero=${FORCED_HERO} not found as a dark/light pair`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
return [FORCED_HERO, found];
|
|
105
|
+
}
|
|
106
|
+
for (const keyword of heroPriority) {
|
|
107
|
+
const match = completePairs.find(([k]) => k.toLowerCase().includes(keyword));
|
|
108
|
+
if (match) return match;
|
|
109
|
+
}
|
|
110
|
+
return completePairs[0] || null;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const hero = pickHero();
|
|
114
|
+
|
|
115
|
+
// Tour cells: any *-dark.png (paired or orphan) except the hero's dark.
|
|
116
|
+
// Then append the hero's light variant as a "Light mode" showcase cell.
|
|
117
|
+
const tourCells = [];
|
|
118
|
+
for (const f of files) {
|
|
119
|
+
const m = f.base.match(/^(.+)-dark$/i);
|
|
120
|
+
if (!m) continue;
|
|
121
|
+
const key = m[1];
|
|
122
|
+
if (hero && key === hero[0]) continue;
|
|
123
|
+
tourCells.push({ key, image: f, captionKey: key });
|
|
124
|
+
}
|
|
125
|
+
if (hero) {
|
|
126
|
+
tourCells.push({
|
|
127
|
+
key: `${hero[0]}-light`,
|
|
128
|
+
image: hero[1].light,
|
|
129
|
+
captionKey: "light-mode",
|
|
130
|
+
isLightShowcase: true,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const heroBlock = hero
|
|
135
|
+
? `<div align="center">
|
|
136
|
+
|
|
137
|
+
<a href="${DEMO_URL}" title="Open the live demo">
|
|
138
|
+
<picture>
|
|
139
|
+
<source media="(prefers-color-scheme: dark)" srcset="${toPosix(hero[1].dark.full)}">
|
|
140
|
+
<source media="(prefers-color-scheme: light)" srcset="${toPosix(hero[1].light.full)}">
|
|
141
|
+
<img alt="${hero[0].replace(/-/g, " ")} — hero screenshot" src="${toPosix(hero[1].dark.full)}" width="100%">
|
|
142
|
+
</picture>
|
|
143
|
+
</a>
|
|
144
|
+
|
|
145
|
+
<sub>↑ click to open the live demo · the image swaps with your system theme</sub>
|
|
146
|
+
|
|
147
|
+
</div>`
|
|
148
|
+
: `(no dark/light hero pair found — add a *-dark.png + *-light.png to ${SCREENSHOT_DIR}/)`;
|
|
149
|
+
|
|
150
|
+
const titleCase = (s) =>
|
|
151
|
+
s.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
152
|
+
|
|
153
|
+
const captionFor = (cell) =>
|
|
154
|
+
cell.isLightShowcase
|
|
155
|
+
? `<b>Light mode</b> · how the app looks in daytime`
|
|
156
|
+
: `<b>${titleCase(cell.captionKey)}</b> · [add caption]`;
|
|
157
|
+
|
|
158
|
+
const renderedCells = tourCells.map((cell) => {
|
|
159
|
+
return ` <td width="50%" align="center">
|
|
160
|
+
<a href="${DEMO_URL}">
|
|
161
|
+
<img alt="${titleCase(cell.captionKey)} screenshot" src="${toPosix(cell.image.full)}" width="100%">
|
|
162
|
+
</a>
|
|
163
|
+
<br>
|
|
164
|
+
<sub>${captionFor(cell)}</sub>
|
|
165
|
+
</td>`;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
let tourBlock = "";
|
|
169
|
+
if (renderedCells.length === 0) {
|
|
170
|
+
tourBlock = `(no additional screenshots beyond the hero — add more *-dark.png to ${SCREENSHOT_DIR}/ to populate the tour grid)`;
|
|
171
|
+
} else {
|
|
172
|
+
const rows = [];
|
|
173
|
+
for (let i = 0; i < renderedCells.length; i += 2) {
|
|
174
|
+
const row = renderedCells.slice(i, i + 2).join("\n");
|
|
175
|
+
rows.push(` <tr>\n${row}\n </tr>`);
|
|
176
|
+
}
|
|
177
|
+
tourBlock = `## Tour\n\nA quick look at the rooms inside. Click any panel to land on it in the live demo.\n\n<table>\n${rows.join("\n")}\n</table>`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
console.log("=== HERO BLOCK (paste under the badges in README.md) ===\n");
|
|
181
|
+
console.log(heroBlock);
|
|
182
|
+
console.log("\n=== TOUR BLOCK (paste below the hero, above ## Demo) ===\n");
|
|
183
|
+
console.log(tourBlock);
|
|
184
|
+
|
|
185
|
+
console.log("\n=== SUMMARY ===");
|
|
186
|
+
console.log(`screenshots: ${files.length}`);
|
|
187
|
+
console.log(`complete pairs: ${completePairs.length}`);
|
|
188
|
+
console.log(`hero: ${hero ? hero[0] : "(none)"}`);
|
|
189
|
+
console.log(`tour cells: ${tourCells.length}`);
|
|
190
|
+
console.log(
|
|
191
|
+
`total size: ${human(files.reduce((sum, f) => sum + f.size, 0))}`,
|
|
192
|
+
);
|
|
193
|
+
if (orphans.length) {
|
|
194
|
+
console.log(
|
|
195
|
+
`orphans (no -dark/-light suffix, ignored): ${orphans.map((o) => o.name).join(", ")}`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (oversized.length) process.exit(2);
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Record a guided-tour GIF for any web app and inline it in the README.
|
|
3
|
+
//
|
|
4
|
+
// Mirrors the captureVideo() flow from the kensaur.us portfolio capture
|
|
5
|
+
// script, but generic: any URL, optional auth, optional viewport size,
|
|
6
|
+
// optional output directory. Produces a `.webm` (intermediate, deletable)
|
|
7
|
+
// and a `.gif` (the actual deliverable — autoplays inline on github.com).
|
|
8
|
+
//
|
|
9
|
+
// Usage:
|
|
10
|
+
// node record-readme-tour.mjs --url=https://demo.example.com/ \
|
|
11
|
+
// [--out=docs/screenshots/tour.gif] \
|
|
12
|
+
// [--width=1280] [--height=800] \
|
|
13
|
+
// [--duration=9000] [--fps=15] \
|
|
14
|
+
// [--user=demo] [--pass=demo] \
|
|
15
|
+
// [--keep-webm]
|
|
16
|
+
//
|
|
17
|
+
// The script:
|
|
18
|
+
// 1. Opens Chromium with recordVideo, navigates to URL.
|
|
19
|
+
// 2. Best-effort dismisses common onboarding overlays (intro.js, joyride,
|
|
20
|
+
// shepherd.js, generic close icons, cookie banners, "Skip" / "Got it"
|
|
21
|
+
// / "閉じる" buttons).
|
|
22
|
+
// 3. If --user/--pass given, fills the first email/text + password input
|
|
23
|
+
// and submits.
|
|
24
|
+
// 4. Calculates real page geometry, executes a 4-stop scroll tour
|
|
25
|
+
// (25 → 50 → 75 → 100% scrollHeight) with smooth transitions, OR
|
|
26
|
+
// holds steady for a single-screen layout.
|
|
27
|
+
// 5. ffmpeg trims the dismiss/login prefix, then runs a two-pass GIF
|
|
28
|
+
// conversion with palette dithering for the best size/quality ratio.
|
|
29
|
+
//
|
|
30
|
+
// Output size: at 800x500 @ 15 fps for a 9 s clip, expect 4-7 MB. GitHub's
|
|
31
|
+
// hard cap is 10 MB. Tune --width and --fps if you blow the budget.
|
|
32
|
+
|
|
33
|
+
import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
|
34
|
+
import { existsSync, statSync } from "node:fs";
|
|
35
|
+
import { dirname, join, resolve } from "node:path";
|
|
36
|
+
import { spawn } from "node:child_process";
|
|
37
|
+
import { chromium } from "playwright";
|
|
38
|
+
import ffmpegInstaller from "@ffmpeg-installer/ffmpeg";
|
|
39
|
+
|
|
40
|
+
const args = Object.fromEntries(
|
|
41
|
+
process.argv.slice(2).map((a) => {
|
|
42
|
+
const [k, ...rest] = a.replace(/^--/, "").split("=");
|
|
43
|
+
return [k, rest.length ? rest.join("=") : true];
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const URL = args.url;
|
|
48
|
+
if (!URL || URL === true) {
|
|
49
|
+
console.error("error: --url=<demo-url> is required");
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const OUT = resolve(args.out || "docs/screenshots/tour.gif");
|
|
54
|
+
const WIDTH = parseInt(args.width || "1280", 10);
|
|
55
|
+
const HEIGHT = parseInt(args.height || "800", 10);
|
|
56
|
+
const DURATION_MS = parseInt(args.duration || "9000", 10);
|
|
57
|
+
const FPS = parseInt(args.fps || "15", 10);
|
|
58
|
+
const USER = args.user;
|
|
59
|
+
const PASS = args.pass;
|
|
60
|
+
const KEEP_WEBM = !!args["keep-webm"];
|
|
61
|
+
// Comma-separated list of paths or absolute URLs to walk through after login.
|
|
62
|
+
// e.g. --routes=/,/reports,/fixes,/judge,/repo → 5 stops, ~equal time each.
|
|
63
|
+
// When omitted the tour just scrolls the landing page (the original behaviour).
|
|
64
|
+
const ROUTES = (args.routes && args.routes !== true)
|
|
65
|
+
? String(args.routes).split(",").map((s) => s.trim()).filter(Boolean)
|
|
66
|
+
: null;
|
|
67
|
+
|
|
68
|
+
// JSON object of localStorage entries to seed BEFORE the page loads. Use to
|
|
69
|
+
// pre-dismiss first-run tours, force a specific theme/density/admin-mode, or
|
|
70
|
+
// hide "what's new" popovers — anything the app gates on a localStorage key.
|
|
71
|
+
// e.g. --storage='{"mushi:mode":"advanced","mushi:tour-v1-completed":"true"}'
|
|
72
|
+
let STORAGE_SEED = null;
|
|
73
|
+
if (args.storage && args.storage !== true) {
|
|
74
|
+
try {
|
|
75
|
+
const parsed = JSON.parse(args.storage);
|
|
76
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
77
|
+
STORAGE_SEED = parsed;
|
|
78
|
+
}
|
|
79
|
+
} catch (e) {
|
|
80
|
+
console.error(`[tour][err] --storage must be valid JSON: ${e.message}`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const SOFT_LIMIT_BYTES = 5 * 1024 * 1024;
|
|
86
|
+
const HARD_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
87
|
+
const SETTLE_MS = 1500;
|
|
88
|
+
const MAX_RECORDING_MS = 35000;
|
|
89
|
+
const FFMPEG = ffmpegInstaller.path;
|
|
90
|
+
|
|
91
|
+
function log(...m) { console.log("[tour]", ...m); }
|
|
92
|
+
function warn(...m) { console.warn("[tour][warn]", ...m); }
|
|
93
|
+
function err(...m) { console.error("[tour][err]", ...m); }
|
|
94
|
+
|
|
95
|
+
function runCommand(bin, args) {
|
|
96
|
+
return new Promise((resolveCmd, rejectCmd) => {
|
|
97
|
+
const child = spawn(bin, args, { stdio: ["ignore", "inherit", "inherit"] });
|
|
98
|
+
child.on("error", rejectCmd);
|
|
99
|
+
child.on("exit", (code) => {
|
|
100
|
+
if (code === 0) resolveCmd();
|
|
101
|
+
else rejectCmd(new Error(`${bin} exited with code ${code}`));
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Dismiss common onboarding / cookie overlays before recording the tour. ──
|
|
107
|
+
// Mirrors the portfolio capture script's dismissOverlays() but trimmed to the
|
|
108
|
+
// patterns most likely to appear on a generic SaaS demo. Best-effort: every
|
|
109
|
+
// click is wrapped in .catch so a missing selector never aborts the run.
|
|
110
|
+
//
|
|
111
|
+
// `safe=true` skips the aggressive text-pattern fallback. Use safe mode after
|
|
112
|
+
// route navigation: app pages are full of inline ✕ icons for "dismiss row /
|
|
113
|
+
// remove tag / clear filter" that match the universal close-glyph regex but
|
|
114
|
+
// would trigger a real destructive action (e.g. dismissing a triage row,
|
|
115
|
+
// removing a project member). Pre-tour dismiss can stay aggressive — at that
|
|
116
|
+
// point the app is in its post-login default state and the only ✕ buttons
|
|
117
|
+
// are real modal close buttons.
|
|
118
|
+
async function dismissOverlays(page, { safe = false } = {}) {
|
|
119
|
+
const selectors = [
|
|
120
|
+
'[data-test-id="button-skip"]', // react-joyride
|
|
121
|
+
'.introjs-skipbutton', // intro.js
|
|
122
|
+
'.driver-popover-close-btn', // driver.js
|
|
123
|
+
'.shepherd-cancel-icon', // shepherd.js
|
|
124
|
+
'[role="dialog"] [aria-label*="close" i]',
|
|
125
|
+
'button[aria-label*="close" i]',
|
|
126
|
+
'button[aria-label*="dismiss" i]',
|
|
127
|
+
'button[aria-label*="skip" i]',
|
|
128
|
+
'[id*="cookie" i] button',
|
|
129
|
+
'[class*="cookie" i] button',
|
|
130
|
+
];
|
|
131
|
+
const textPatterns = [
|
|
132
|
+
/^skip\b/i, /^got it!?$/i, /^continue$/i, /^done$/i, /^close$/i,
|
|
133
|
+
/^[\u00d7\u2715\u2716]$/, /^dismiss$/i, /^later$/i, /^no thanks$/i,
|
|
134
|
+
/^accept all$/i, /^reject all$/i,
|
|
135
|
+
/^閉じる$/, /^スキップ/, /^完了$/, /^OK$/i,
|
|
136
|
+
/^guest$/i, /^try as guest$/i,
|
|
137
|
+
];
|
|
138
|
+
// Press Escape first — almost every tour/modal lib (intro.js, driver.js,
|
|
139
|
+
// shepherd.js, react-joyride, headlessui Dialog, Radix Dialog, custom
|
|
140
|
+
// first-run tours) binds Esc-to-close. Way more reliable than guessing the
|
|
141
|
+
// close-button selector. Cheap and idempotent.
|
|
142
|
+
await page.keyboard.press("Escape").catch(() => {});
|
|
143
|
+
await page.waitForTimeout(150);
|
|
144
|
+
|
|
145
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
146
|
+
let clicked = false;
|
|
147
|
+
for (const sel of selectors) {
|
|
148
|
+
const loc = page.locator(sel).first();
|
|
149
|
+
if (await loc.isVisible({ timeout: 250 }).catch(() => false)) {
|
|
150
|
+
await loc.click({ timeout: 1500, force: true }).catch(() => {});
|
|
151
|
+
clicked = true;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (!clicked && !safe) {
|
|
156
|
+
const candidates = await page
|
|
157
|
+
.locator('button:visible, [role="button"]:visible, a:visible')
|
|
158
|
+
.all()
|
|
159
|
+
.catch(() => []);
|
|
160
|
+
for (const btn of candidates) {
|
|
161
|
+
const text = (await btn.textContent({ timeout: 250 }).catch(() => "")).trim();
|
|
162
|
+
if (!text || text.length > 80) continue;
|
|
163
|
+
if (textPatterns.some((re) => re.test(text))) {
|
|
164
|
+
await btn.click({ timeout: 1500, force: true }).catch(() => {});
|
|
165
|
+
clicked = true;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (!clicked) break;
|
|
171
|
+
await page.waitForTimeout(250);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function maybeLogin(page) {
|
|
176
|
+
if (!USER || !PASS) return;
|
|
177
|
+
const userInput = page
|
|
178
|
+
.locator('input[type="email"], input[name*="email" i], input[name*="user" i], input[type="text"]')
|
|
179
|
+
.first();
|
|
180
|
+
const passInput = page.locator('input[type="password"]').first();
|
|
181
|
+
if (!(await userInput.isVisible({ timeout: 1500 }).catch(() => false))) return;
|
|
182
|
+
log("filling login form");
|
|
183
|
+
await userInput.fill(USER).catch(() => {});
|
|
184
|
+
await passInput.fill(PASS).catch(() => {});
|
|
185
|
+
const submit = page
|
|
186
|
+
.getByRole("button", { name: /sign\s*in|log\s*in|login|continue|submit|ログイン/i })
|
|
187
|
+
.first();
|
|
188
|
+
if (await submit.isVisible({ timeout: 800 }).catch(() => false)) {
|
|
189
|
+
await submit.click({ timeout: 2000 }).catch(() => {});
|
|
190
|
+
} else {
|
|
191
|
+
await passInput.press("Enter").catch(() => {});
|
|
192
|
+
}
|
|
193
|
+
await page.waitForLoadState("networkidle", { timeout: 12000 }).catch(() => {});
|
|
194
|
+
await page.waitForTimeout(1500);
|
|
195
|
+
await dismissOverlays(page);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Settle the page after login: wait for late-appearing modals to mount,
|
|
199
|
+
// dismiss them, press Escape as a fallback. Returns wall-clock millis spent
|
|
200
|
+
// here so the caller can include this in the ffmpeg trim prefix and avoid
|
|
201
|
+
// a GIF that opens with a half-dismissed coach-mark.
|
|
202
|
+
async function performTourSettle(page) {
|
|
203
|
+
const t0 = Date.now();
|
|
204
|
+
// Late-appearing modals (first-run tours, post-login coach-marks, "what's
|
|
205
|
+
// new" announcements) often mount via async useEffect AFTER the login
|
|
206
|
+
// network call resolves — i.e. after maybeLogin's dismissOverlays already
|
|
207
|
+
// ran. Wait a beat then sweep again so the tour doesn't end up filming a
|
|
208
|
+
// still of a coach-mark.
|
|
209
|
+
await page.waitForTimeout(1500);
|
|
210
|
+
await dismissOverlays(page);
|
|
211
|
+
// Send Escape one more time after the dismiss in case a late-mount modal
|
|
212
|
+
// appeared between the wait and the click loop (e.g. a "what's new"
|
|
213
|
+
// popover that fires only after the project list resolves).
|
|
214
|
+
await page.keyboard.press("Escape").catch(() => {});
|
|
215
|
+
await page.waitForTimeout(500);
|
|
216
|
+
return Date.now() - t0;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function performTour(page, startMs) {
|
|
220
|
+
// Multi-route tour: walk through a sequence of pages, dwelling on each
|
|
221
|
+
// long enough to read the headline number. Way more compelling than
|
|
222
|
+
// scrolling a single short dashboard. Resolves each entry against the
|
|
223
|
+
// initial URL so callers can pass either absolute URLs or relative paths.
|
|
224
|
+
if (ROUTES && ROUTES.length > 0) {
|
|
225
|
+
const baseUrl = new globalThis.URL(URL);
|
|
226
|
+
const perRouteMs = Math.max(
|
|
227
|
+
1500,
|
|
228
|
+
Math.floor((DURATION_MS - 500) / ROUTES.length),
|
|
229
|
+
);
|
|
230
|
+
log(`multi-route tour: ${ROUTES.length} stops · ${perRouteMs}ms each`);
|
|
231
|
+
for (let i = 0; i < ROUTES.length; i++) {
|
|
232
|
+
if (Date.now() - startMs > MAX_RECORDING_MS - 1500) break;
|
|
233
|
+
const route = ROUTES[i];
|
|
234
|
+
const target = /^https?:\/\//i.test(route)
|
|
235
|
+
? route
|
|
236
|
+
: new globalThis.URL(route, baseUrl).toString();
|
|
237
|
+
const isFirst = i === 0 && (route === "/" || target === URL);
|
|
238
|
+
if (!isFirst) {
|
|
239
|
+
// SPA-friendly navigation: prefer clicking an in-page link to the
|
|
240
|
+
// target so React Router / Vue Router / SvelteKit handle it as a
|
|
241
|
+
// client-side transition (no white flash, no full re-mount). Fall
|
|
242
|
+
// back to a hard goto if no nav link exists for this path.
|
|
243
|
+
const targetPath = new globalThis.URL(target).pathname;
|
|
244
|
+
const navLink = page
|
|
245
|
+
.locator(
|
|
246
|
+
`a[href="${targetPath}"], a[href="${targetPath}/"], a[href$="${targetPath}"]`,
|
|
247
|
+
)
|
|
248
|
+
.first();
|
|
249
|
+
const clicked = await navLink
|
|
250
|
+
.isVisible({ timeout: 600 })
|
|
251
|
+
.then((v) => (v ? navLink.click({ timeout: 2000 }).then(() => true).catch(() => false) : false))
|
|
252
|
+
.catch(() => false);
|
|
253
|
+
if (!clicked) {
|
|
254
|
+
await page
|
|
255
|
+
.goto(target, { waitUntil: "domcontentloaded", timeout: 15000 })
|
|
256
|
+
.catch(() => {});
|
|
257
|
+
}
|
|
258
|
+
// Wait for the new route to settle. networkidle covers SPA data
|
|
259
|
+
// fetches; the short timeout keeps a slow XHR from blowing the
|
|
260
|
+
// per-route budget — most SPAs hit interactive in <1.5 s.
|
|
261
|
+
await page.waitForLoadState("networkidle", { timeout: 2000 }).catch(() => {});
|
|
262
|
+
// Best-effort dismiss any per-route modal/coach-mark that mounts on
|
|
263
|
+
// navigation. Safe mode: selectors only — real app pages have inline
|
|
264
|
+
// ✕ icons for "dismiss row / clear filter" that would otherwise get
|
|
265
|
+
// hit by the close-glyph text regex and trigger destructive actions.
|
|
266
|
+
await page.waitForTimeout(300);
|
|
267
|
+
await dismissOverlays(page, { safe: true });
|
|
268
|
+
}
|
|
269
|
+
await page.waitForTimeout(perRouteMs);
|
|
270
|
+
}
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Real geometry → editorial tour. Short pages dwell in place.
|
|
275
|
+
const scrollHeight = await page
|
|
276
|
+
.evaluate(() =>
|
|
277
|
+
Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight ?? 0),
|
|
278
|
+
)
|
|
279
|
+
.catch(() => HEIGHT);
|
|
280
|
+
const maxScrollY = Math.max(0, scrollHeight - HEIGHT);
|
|
281
|
+
const isShortPage = maxScrollY < HEIGHT * 0.5;
|
|
282
|
+
|
|
283
|
+
const TOP_DWELL_MS = 1200;
|
|
284
|
+
const STOP_DWELL_MS = 800;
|
|
285
|
+
const SMOOTH_SCROLL_MS = 600;
|
|
286
|
+
const RETURN_DWELL_MS = 1000;
|
|
287
|
+
|
|
288
|
+
if (isShortPage) {
|
|
289
|
+
// Single-screen layout — hold steady. Better than jittering a non-scrolling page.
|
|
290
|
+
await page.waitForTimeout(DURATION_MS - 500);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
await page.waitForTimeout(TOP_DWELL_MS);
|
|
295
|
+
const stops = [0.25, 0.5, 0.75, 1.0];
|
|
296
|
+
for (const pct of stops) {
|
|
297
|
+
if (Date.now() - startMs > MAX_RECORDING_MS - 1500) break;
|
|
298
|
+
const target = Math.round(maxScrollY * pct);
|
|
299
|
+
await page.evaluate(
|
|
300
|
+
({ y, ms }) => {
|
|
301
|
+
return new Promise((res) => {
|
|
302
|
+
const start = window.scrollY;
|
|
303
|
+
const dist = y - start;
|
|
304
|
+
const t0 = performance.now();
|
|
305
|
+
function step(now) {
|
|
306
|
+
const t = Math.min(1, (now - t0) / ms);
|
|
307
|
+
const eased = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
|
|
308
|
+
window.scrollTo(0, start + dist * eased);
|
|
309
|
+
if (t < 1) requestAnimationFrame(step);
|
|
310
|
+
else res();
|
|
311
|
+
}
|
|
312
|
+
requestAnimationFrame(step);
|
|
313
|
+
});
|
|
314
|
+
},
|
|
315
|
+
{ y: target, ms: SMOOTH_SCROLL_MS },
|
|
316
|
+
).catch(() => {});
|
|
317
|
+
await page.waitForTimeout(STOP_DWELL_MS);
|
|
318
|
+
}
|
|
319
|
+
// Return to hero so the loop end matches the loop start visually.
|
|
320
|
+
await page.evaluate(() => window.scrollTo({ top: 0, behavior: "smooth" })).catch(() => {});
|
|
321
|
+
await page.waitForTimeout(RETURN_DWELL_MS);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function recordWebm(tmpDir) {
|
|
325
|
+
const browser = await chromium.launch();
|
|
326
|
+
const ctx = await browser.newContext({
|
|
327
|
+
viewport: { width: WIDTH, height: HEIGHT },
|
|
328
|
+
deviceScaleFactor: 1,
|
|
329
|
+
colorScheme: "dark",
|
|
330
|
+
recordVideo: { dir: tmpDir, size: { width: WIDTH, height: HEIGHT } },
|
|
331
|
+
});
|
|
332
|
+
// Seed localStorage BEFORE any page script runs. Playwright's addInitScript
|
|
333
|
+
// injects into every navigation in this context, so the values are present
|
|
334
|
+
// on the very first React render — perfect for skipping first-run tours,
|
|
335
|
+
// forcing dark mode, or unlocking advanced UI on a fresh test account.
|
|
336
|
+
if (STORAGE_SEED) {
|
|
337
|
+
const entries = JSON.stringify(STORAGE_SEED);
|
|
338
|
+
await ctx.addInitScript(`(() => {
|
|
339
|
+
try {
|
|
340
|
+
const e = ${entries};
|
|
341
|
+
for (const k of Object.keys(e)) {
|
|
342
|
+
const v = e[k];
|
|
343
|
+
window.localStorage.setItem(k, typeof v === "string" ? v : JSON.stringify(v));
|
|
344
|
+
}
|
|
345
|
+
} catch {}
|
|
346
|
+
})();`);
|
|
347
|
+
log(`seeded localStorage: ${Object.keys(STORAGE_SEED).join(", ")}`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const page = await ctx.newPage();
|
|
351
|
+
const startMs = Date.now();
|
|
352
|
+
|
|
353
|
+
log(`navigating to ${URL}`);
|
|
354
|
+
await page.goto(URL, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
355
|
+
await page.waitForTimeout(SETTLE_MS);
|
|
356
|
+
|
|
357
|
+
await dismissOverlays(page);
|
|
358
|
+
await maybeLogin(page);
|
|
359
|
+
|
|
360
|
+
// performTour does its own late-overlay sweep first; mark the prefix AFTER
|
|
361
|
+
// that sweep so the GIF doesn't lead with a half-dismissed coach-mark.
|
|
362
|
+
// performTour returns the wall-clock millis it spent settling the page so
|
|
363
|
+
// we can fold that into the prefix the ffmpeg trim seeks past.
|
|
364
|
+
const settleStart = Date.now();
|
|
365
|
+
const settleMs = await performTourSettle(page);
|
|
366
|
+
const settleSec = settleMs / 1000;
|
|
367
|
+
const prefixSec = (settleStart - startMs) / 1000 + settleSec;
|
|
368
|
+
log(`prefix ${prefixSec.toFixed(2)}s — running guided tour`);
|
|
369
|
+
|
|
370
|
+
await performTour(page, startMs);
|
|
371
|
+
|
|
372
|
+
await page.close();
|
|
373
|
+
await ctx.close();
|
|
374
|
+
await browser.close();
|
|
375
|
+
|
|
376
|
+
// The webm filename is decided by Playwright; pick the latest .webm in tmpDir.
|
|
377
|
+
const entries = (await readdir(tmpDir)).filter((f) => f.endsWith(".webm"));
|
|
378
|
+
if (entries.length === 0) throw new Error("no .webm produced — recording context never closed?");
|
|
379
|
+
const rawWebm = join(tmpDir, entries[entries.length - 1]);
|
|
380
|
+
return { rawWebm, prefixSec };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function convertToGif(rawWebm, prefixSec) {
|
|
384
|
+
await mkdir(dirname(OUT), { recursive: true });
|
|
385
|
+
// Two-pass palette generation gives an order-of-magnitude better quality
|
|
386
|
+
// at a given size than the single-pass default. The dither=bayer:bayer_scale=5
|
|
387
|
+
// pattern is the recommended setting from ffmpeg's gif documentation —
|
|
388
|
+
// smooth gradients without the speckled "diff" stats_mode artifacts.
|
|
389
|
+
const filter =
|
|
390
|
+
`fps=${FPS},scale=${WIDTH}:-1:flags=lanczos,` +
|
|
391
|
+
`split[s0][s1];[s0]palettegen=stats_mode=diff[p];` +
|
|
392
|
+
`[s1][p]paletteuse=dither=bayer:bayer_scale=5`;
|
|
393
|
+
log(`converting webm → gif (${WIDTH}px @ ${FPS} fps, ${(DURATION_MS / 1000).toFixed(1)} s)`);
|
|
394
|
+
await runCommand(FFMPEG, [
|
|
395
|
+
"-y",
|
|
396
|
+
"-ss", prefixSec.toFixed(2),
|
|
397
|
+
"-i", rawWebm,
|
|
398
|
+
"-t", String(DURATION_MS / 1000),
|
|
399
|
+
"-vf", filter,
|
|
400
|
+
"-loop", "0",
|
|
401
|
+
"-loglevel", "error",
|
|
402
|
+
OUT,
|
|
403
|
+
]);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
(async () => {
|
|
407
|
+
if (!existsSync(FFMPEG)) {
|
|
408
|
+
err(`ffmpeg binary missing at ${FFMPEG} — re-install @ffmpeg-installer/ffmpeg`);
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
const tmpDir = resolve(`.tour-tmp-${Date.now()}`);
|
|
412
|
+
await mkdir(tmpDir, { recursive: true });
|
|
413
|
+
try {
|
|
414
|
+
const { rawWebm, prefixSec } = await recordWebm(tmpDir);
|
|
415
|
+
await convertToGif(rawWebm, prefixSec);
|
|
416
|
+
if (KEEP_WEBM) {
|
|
417
|
+
const keptWebm = OUT.replace(/\.gif$/, ".webm");
|
|
418
|
+
await runCommand("cp", [rawWebm, keptWebm]).catch(async () => {
|
|
419
|
+
// cp is missing on Windows shells; fall back to a node-native copy.
|
|
420
|
+
const buf = await import("node:fs/promises").then((m) => m.readFile(rawWebm));
|
|
421
|
+
await writeFile(keptWebm, buf);
|
|
422
|
+
});
|
|
423
|
+
log(`kept webm at ${keptWebm}`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const { size } = statSync(OUT);
|
|
427
|
+
const mb = size / 1024 / 1024;
|
|
428
|
+
log(`wrote ${OUT} (${mb.toFixed(2)} MB)`);
|
|
429
|
+
if (size > HARD_LIMIT_BYTES) {
|
|
430
|
+
err(`gif is ${mb.toFixed(2)} MB — exceeds GitHub's 10 MB inline cap. Re-run with smaller --width or lower --fps.`);
|
|
431
|
+
process.exit(2);
|
|
432
|
+
}
|
|
433
|
+
if (size > SOFT_LIMIT_BYTES) {
|
|
434
|
+
warn(`gif is ${mb.toFixed(2)} MB (>${(SOFT_LIMIT_BYTES / 1024 / 1024).toFixed(0)} MB). Consider lowering --width or --fps for faster repo clones.`);
|
|
435
|
+
}
|
|
436
|
+
} finally {
|
|
437
|
+
await rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
|
438
|
+
}
|
|
439
|
+
})().catch((e) => {
|
|
440
|
+
err(e.stack || e.message || String(e));
|
|
441
|
+
process.exit(1);
|
|
442
|
+
});
|