@kensaurus/skills 0.0.0-stage → 2.0.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.
Files changed (423) hide show
  1. package/.claude-plugin/marketplace.json +53 -0
  2. package/.claude-plugin/plugin.json +40 -0
  3. package/.cursor-plugin/plugin.json +38 -0
  4. package/.mcp.json +28 -0
  5. package/CHANGELOG.md +1761 -0
  6. package/LICENSE +21 -0
  7. package/NOTICE +13 -0
  8. package/README.md +820 -2
  9. package/SECURITY.md +55 -0
  10. package/agents/code-reviewer.md +60 -0
  11. package/agents/completion-judge.md +89 -0
  12. package/agents/db-migrator.md +125 -0
  13. package/agents/debugger.md +47 -0
  14. package/agents/deploy-checker.md +100 -0
  15. package/agents/perf-monitor.md +74 -0
  16. package/assets/favicon.png +0 -0
  17. package/assets/logo-light.png +0 -0
  18. package/assets/logo.png +0 -0
  19. package/assets/logo.svg +6 -0
  20. package/assets/og.png +0 -0
  21. package/bin/install.mjs +1127 -0
  22. package/bin/kenji.js +2 -0
  23. package/commands/adr.md +17 -0
  24. package/commands/aeo-plan.md +18 -0
  25. package/commands/arch-boundaries.md +17 -0
  26. package/commands/aso-plan.md +18 -0
  27. package/commands/auth-flows.md +19 -0
  28. package/commands/backup-plan.md +17 -0
  29. package/commands/burndown-full.md +25 -0
  30. package/commands/capacitor-plan.md +18 -0
  31. package/commands/codemod-safety.md +22 -0
  32. package/commands/commit.md +18 -0
  33. package/commands/complete-everything.md +40 -0
  34. package/commands/cost-plan.md +19 -0
  35. package/commands/deadcode-plan.md +26 -0
  36. package/commands/deadcode.md +32 -0
  37. package/commands/debug-issue.md +17 -0
  38. package/commands/deps-plan.md +18 -0
  39. package/commands/docs-plan.md +17 -0
  40. package/commands/doctrine.md +19 -0
  41. package/commands/error-plan.md +19 -0
  42. package/commands/feedback-to-closure.md +36 -0
  43. package/commands/fix-issue.md +76 -0
  44. package/commands/gate-logic.md +26 -0
  45. package/commands/green-repo.md +36 -0
  46. package/commands/grill-me.md +19 -0
  47. package/commands/gtm-plan.md +21 -0
  48. package/commands/gtm-weekly.md +17 -0
  49. package/commands/gtm.md +22 -0
  50. package/commands/handoff.md +15 -0
  51. package/commands/housekeep-backlog.md +18 -0
  52. package/commands/housekeep-files.md +22 -0
  53. package/commands/housekeep-gates.md +18 -0
  54. package/commands/instant-nav.md +11 -0
  55. package/commands/integrity-plan.md +19 -0
  56. package/commands/launch-kit.md +16 -0
  57. package/commands/mcp-guide.md +40 -0
  58. package/commands/mobile-plan.md +19 -0
  59. package/commands/native-rn-monorepo/README.md +78 -0
  60. package/commands/native-rn-monorepo/android-build.md +26 -0
  61. package/commands/native-rn-monorepo/android-install.md +32 -0
  62. package/commands/native-rn-monorepo/android-logcat.md +37 -0
  63. package/commands/native-rn-monorepo/ios-ci-logs.md +56 -0
  64. package/commands/native-rn-monorepo/ios-ci-status.md +52 -0
  65. package/commands/native-rn-monorepo/ios-ci-trigger.md +59 -0
  66. package/commands/native-rn-monorepo/rn-reset.md +50 -0
  67. package/commands/native-rn-monorepo/rn-ship-ios.md +67 -0
  68. package/commands/native-rn-monorepo/rn-verify.md +53 -0
  69. package/commands/perf-plan.md +18 -0
  70. package/commands/plan-mode.md +74 -0
  71. package/commands/pr.md +16 -0
  72. package/commands/pricing-plan.md +20 -0
  73. package/commands/privacy-plan.md +18 -0
  74. package/commands/readability.md +12 -0
  75. package/commands/readme.md +15 -0
  76. package/commands/refactor.md +15 -0
  77. package/commands/release-prep.md +17 -0
  78. package/commands/research.md +25 -0
  79. package/commands/responsive-audit.md +21 -0
  80. package/commands/review-code.md +18 -0
  81. package/commands/rls-plan.md +18 -0
  82. package/commands/secrets-plan.md +18 -0
  83. package/commands/security-plan.md +19 -0
  84. package/commands/ship-and-observe.md +36 -0
  85. package/commands/skill-conflicts.md +19 -0
  86. package/commands/slop-plan.md +18 -0
  87. package/commands/stub-plan.md +18 -0
  88. package/commands/test-mutation.md +16 -0
  89. package/commands/test-plan.md +17 -0
  90. package/commands/test.md +29 -0
  91. package/commands/thirdparty-web-interface-guidelines.md +185 -0
  92. package/commands/uiux-plan.md +18 -0
  93. package/commands/uiux.md +45 -0
  94. package/commands/update-deps.md +21 -0
  95. package/commands/validation-plan.md +19 -0
  96. package/commands-portable/fix-issue.md +72 -0
  97. package/commands-portable/plan-mode.md +92 -0
  98. package/commands-portable/research.md +91 -0
  99. package/docs/screenshots/README.md +5 -0
  100. package/docs/screenshots/audit-dark.png +0 -0
  101. package/docs/screenshots/build-dark.png +0 -0
  102. package/docs/screenshots/grill-dark.png +0 -0
  103. package/docs/screenshots/hero-dark.png +0 -0
  104. package/docs/screenshots/hero-light.png +0 -0
  105. package/docs/screenshots/ship-dark.png +0 -0
  106. package/docs/screenshots/src/showcase.html +320 -0
  107. package/hooks/completion-gate.mjs +258 -0
  108. package/hooks/cursor-hooks.json +13 -0
  109. package/hooks/hooks.json +15 -0
  110. package/install.sh +21 -0
  111. package/llms.txt +40 -0
  112. package/mcp/README.md +266 -0
  113. package/mcp/VERSIONS.md +41 -0
  114. package/mcp/mcp-full.json.template +124 -0
  115. package/mcp/mcp.json.template +29 -0
  116. package/mcp/pinned-versions.json +27 -0
  117. package/package.json +93 -4
  118. package/rules/approved-plan-execution.mdc +65 -0
  119. package/rules/full-stack-ship-discipline.mdc +37 -0
  120. package/rules/native-rn-monorepo/README.md +63 -0
  121. package/rules/native-rn-monorepo/_project.mdc +69 -0
  122. package/rules/native-rn-monorepo/native-android.mdc +72 -0
  123. package/rules/native-rn-monorepo/native-ios.mdc +61 -0
  124. package/rules/native-rn-monorepo/react-native-js.mdc +78 -0
  125. package/rules/native-rn-monorepo/web.mdc +60 -0
  126. package/rules/project-starter/components.mdc +54 -0
  127. package/rules/project-starter/data-fetching.mdc +77 -0
  128. package/rules/project-starter/git.mdc +41 -0
  129. package/rules/project-starter/supabase.mdc +37 -0
  130. package/rules/project-starter/tailwind.mdc +48 -0
  131. package/rules/project-starter/typescript.mdc +36 -0
  132. package/rules/project-starter/web-performance.mdc +42 -0
  133. package/rules/senior-engineer.mdc +30 -0
  134. package/rules/shell-first-search.mdc +19 -0
  135. package/rules/skill-workflows.mdc +35 -0
  136. package/rules/verification-before-completion.mdc +57 -0
  137. package/skills/audit-accessibility/SKILL.md +441 -0
  138. package/skills/audit-agent-speed/SKILL.md +181 -0
  139. package/skills/audit-agent-speed/scripts/stop-typecheck.mjs +151 -0
  140. package/skills/audit-analytics/SKILL.md +138 -0
  141. package/skills/audit-auth-flows/SKILL.md +267 -0
  142. package/skills/audit-backend-architecture/SKILL.md +266 -0
  143. package/skills/audit-backend-architecture/references/patterns.md +386 -0
  144. package/skills/audit-bundle-size/SKILL.md +296 -0
  145. package/skills/audit-cicd/SKILL.md +218 -0
  146. package/skills/audit-code-quality/SKILL.md +314 -0
  147. package/skills/audit-code-review/SKILL.md +289 -0
  148. package/skills/audit-codemod-safety/SKILL.md +159 -0
  149. package/skills/audit-db-schema/SKILL.md +465 -0
  150. package/skills/audit-db-schema/references/details.md +110 -0
  151. package/skills/audit-doctrine/SKILL.md +189 -0
  152. package/skills/audit-env-parity/SKILL.md +133 -0
  153. package/skills/audit-fe-api/SKILL.md +458 -0
  154. package/skills/audit-gate-logic/SKILL.md +219 -0
  155. package/skills/audit-i18n/SKILL.md +339 -0
  156. package/skills/audit-infra-cost/SKILL.md +142 -0
  157. package/skills/audit-langfuse-llm/SKILL.md +468 -0
  158. package/skills/audit-langfuse-llm/references/details.md +226 -0
  159. package/skills/audit-llm-security/SKILL.md +147 -0
  160. package/skills/audit-monetization-iap/SKILL.md +137 -0
  161. package/skills/audit-payment-system/SKILL.md +268 -0
  162. package/skills/audit-payment-system/references/checklist.md +283 -0
  163. package/skills/audit-performance/SKILL.md +383 -0
  164. package/skills/audit-performance/references/loading-priority-2026.md +81 -0
  165. package/skills/audit-realworld/SKILL.md +287 -0
  166. package/skills/audit-registry-listing/SKILL.md +122 -0
  167. package/skills/audit-resilience/SKILL.md +154 -0
  168. package/skills/audit-responsive/SKILL.md +221 -0
  169. package/skills/audit-responsive/references/checklist.md +166 -0
  170. package/skills/audit-security/SKILL.md +289 -0
  171. package/skills/audit-skill-conflicts/SKILL.md +178 -0
  172. package/skills/audit-ui-states/SKILL.md +146 -0
  173. package/skills/audit-uiux-design-system/SKILL.md +475 -0
  174. package/skills/audit-uiux-design-system/references/details.md +71 -0
  175. package/skills/audit-ux/SKILL.md +379 -0
  176. package/skills/audit-ux/references/details.md +245 -0
  177. package/skills/audit-ux-journeys/SKILL.md +215 -0
  178. package/skills/audit-ux-journeys/references/checklist.md +179 -0
  179. package/skills/backend-db-performance/SKILL.md +441 -0
  180. package/skills/backend-error-handling/SKILL.md +489 -0
  181. package/skills/backend-error-handling/references/details.md +58 -0
  182. package/skills/backend-observability/SKILL.md +88 -0
  183. package/skills/backend-patterns/SKILL.md +499 -0
  184. package/skills/backend-patterns/references/architecture-patterns.md +298 -0
  185. package/skills/backend-realtime/SKILL.md +403 -0
  186. package/skills/backend-realtime/references/patterns.md +74 -0
  187. package/skills/burndown-full/SKILL.md +174 -0
  188. package/skills/complete-everything/SKILL.md +295 -0
  189. package/skills/data-pipeline/SKILL.md +109 -0
  190. package/skills/data-visualization/SKILL.md +488 -0
  191. package/skills/debug-error/SKILL.md +322 -0
  192. package/skills/debug-fe-be-integration/SKILL.md +459 -0
  193. package/skills/debug-sentry-monitor/SKILL.md +497 -0
  194. package/skills/debug-sentry-monitor/references/details.md +165 -0
  195. package/skills/deploy-npm/SKILL.md +394 -0
  196. package/skills/deploy-npm/references/example-mushi-mushi.md +52 -0
  197. package/skills/deploy-verify/SKILL.md +489 -0
  198. package/skills/design-api/SKILL.md +379 -0
  199. package/skills/design-canvas/SKILL.md +155 -0
  200. package/skills/design-email/SKILL.md +370 -0
  201. package/skills/design-frontend/SKILL.md +143 -0
  202. package/skills/design-generative-art/SKILL.md +474 -0
  203. package/skills/design-mobile-first/SKILL.md +506 -0
  204. package/skills/design-motion/SKILL.md +333 -0
  205. package/skills/design-motion/references/delight-interactions.md +191 -0
  206. package/skills/design-prd/SKILL.md +443 -0
  207. package/skills/design-system/SKILL.md +457 -0
  208. package/skills/design-theme/SKILL.md +226 -0
  209. package/skills/design-theme/themes/tsumagoi-ranch.md +150 -0
  210. package/skills/docs-adr/SKILL.md +168 -0
  211. package/skills/docs-coauthor/SKILL.md +368 -0
  212. package/skills/docs-comparison-pages/SKILL.md +117 -0
  213. package/skills/docs-domain-modeling/SKILL.md +97 -0
  214. package/skills/docs-launch-kit/SKILL.md +139 -0
  215. package/skills/docs-writer/SKILL.md +469 -0
  216. package/skills/enhance-agent-guardrails/SKILL.md +164 -0
  217. package/skills/enhance-arch-boundaries/SKILL.md +154 -0
  218. package/skills/enhance-capacitor-ui/SKILL.md +463 -0
  219. package/skills/enhance-capacitor-ui/references/details.md +750 -0
  220. package/skills/enhance-email-deliverability/SKILL.md +143 -0
  221. package/skills/enhance-growth-loops/SKILL.md +122 -0
  222. package/skills/enhance-lifecycle-email/SKILL.md +130 -0
  223. package/skills/enhance-motion/SKILL.md +193 -0
  224. package/skills/enhance-onboarding/SKILL.md +148 -0
  225. package/skills/enhance-pwa/SKILL.md +304 -0
  226. package/skills/enhance-readability/SKILL.md +146 -0
  227. package/skills/enhance-readme/SKILL.md +496 -0
  228. package/skills/enhance-readme/package-lock.json +187 -0
  229. package/skills/enhance-readme/package.json +17 -0
  230. package/skills/enhance-readme/scripts/generate-readme-blocks.mjs +199 -0
  231. package/skills/enhance-readme/scripts/record-readme-tour.mjs +442 -0
  232. package/skills/enhance-skill-prompts/SKILL.md +167 -0
  233. package/skills/enhance-skill-prompts/references/exemplar-audit-auth-flows.md +311 -0
  234. package/skills/enhance-web-conversion/SKILL.md +155 -0
  235. package/skills/enhance-web-forms/SKILL.md +154 -0
  236. package/skills/enhance-web-instant-nav/SKILL.md +138 -0
  237. package/skills/enhance-web-instant-nav/references/bfcache-blockers.md +23 -0
  238. package/skills/enhance-web-instant-nav/references/early-hints.md +33 -0
  239. package/skills/enhance-web-instant-nav/references/speculation-rules.md +44 -0
  240. package/skills/enhance-web-landing/SKILL.md +459 -0
  241. package/skills/enhance-web-landing/references/details.md +773 -0
  242. package/skills/enhance-web-redesign/SKILL.md +228 -0
  243. package/skills/enhance-web-seo/SKILL.md +276 -0
  244. package/skills/enhance-web-ui/SKILL.md +473 -0
  245. package/skills/enhance-web-ui/references/details.md +674 -0
  246. package/skills/enhance-web-ux/HEURISTICS.md +242 -0
  247. package/skills/enhance-web-ux/PATTERNS.md +375 -0
  248. package/skills/enhance-web-ux/SKILL.md +464 -0
  249. package/skills/enhance-web-ux/examples.md +222 -0
  250. package/skills/enhance-web-ux/references/details.md +406 -0
  251. package/skills/enhance-web-web3d/SKILL.md +397 -0
  252. package/skills/enhance-web-web3d/references/css-canvas-effects.md +180 -0
  253. package/skills/handoff/SKILL.md +66 -0
  254. package/skills/housekeep-backlog/SKILL.md +149 -0
  255. package/skills/housekeep-dead-code/SKILL.md +387 -0
  256. package/skills/housekeep-dead-code/references/ratchet-ci.md +205 -0
  257. package/skills/housekeep-dead-code/references/supabase-hygiene.md +152 -0
  258. package/skills/housekeep-design/SKILL.md +207 -0
  259. package/skills/housekeep-files/SKILL.md +220 -0
  260. package/skills/housekeep-files/references/naming-and-catalog.md +86 -0
  261. package/skills/housekeep-files/scripts/housekeep-files.ps1 +360 -0
  262. package/skills/housekeep-files/scripts/housekeep-files.sh +238 -0
  263. package/skills/housekeep-gates/SKILL.md +174 -0
  264. package/skills/iterate-agent-harness/SKILL.md +137 -0
  265. package/skills/iterate-gtm-weekly/SKILL.md +103 -0
  266. package/skills/iterate-post-launch/SKILL.md +292 -0
  267. package/skills/meta-mcp-builder/SKILL.md +313 -0
  268. package/skills/meta-skill-creator/SKILL.md +304 -0
  269. package/skills/mobile-capacitor-platform/SKILL.md +104 -0
  270. package/skills/mobile-emulator-start/SKILL.md +296 -0
  271. package/skills/mobile-emulator-test/SKILL.md +491 -0
  272. package/skills/mobile-emulator-test/references/details.md +478 -0
  273. package/skills/mobile-rn-performance/SKILL.md +107 -0
  274. package/skills/mobile-rn-screen/SKILL.md +476 -0
  275. package/skills/mobile-rn-screen/references/details.md +785 -0
  276. package/skills/mushi-health/SKILL.md +206 -0
  277. package/skills/mushi-integration/SKILL.md +257 -0
  278. package/skills/plan-aeo-readiness/SKILL.md +166 -0
  279. package/skills/plan-antislop/SKILL.md +281 -0
  280. package/skills/plan-aso/SKILL.md +149 -0
  281. package/skills/plan-backup-dr/SKILL.md +131 -0
  282. package/skills/plan-capacitor-hardening/SKILL.md +217 -0
  283. package/skills/plan-data-integrity/SKILL.md +187 -0
  284. package/skills/plan-dead-code/SKILL.md +386 -0
  285. package/skills/plan-dead-code/references/knip-config.md +214 -0
  286. package/skills/plan-dead-code/references/output-templates.md +133 -0
  287. package/skills/plan-dead-code/references/preservation-contract.md +50 -0
  288. package/skills/plan-dead-code/references/residue-greps.md +84 -0
  289. package/skills/plan-dependency-provenance/SKILL.md +200 -0
  290. package/skills/plan-docs-sync/SKILL.md +143 -0
  291. package/skills/plan-docs-sync/references/drift-taxonomy.md +43 -0
  292. package/skills/plan-docs-sync/references/output-templates.md +33 -0
  293. package/skills/plan-docs-sync/references/preservation-contract.md +17 -0
  294. package/skills/plan-error-handling/SKILL.md +205 -0
  295. package/skills/plan-gtm/SKILL.md +276 -0
  296. package/skills/plan-gtm/references/benchmarks-2026.md +183 -0
  297. package/skills/plan-input-validation/SKILL.md +179 -0
  298. package/skills/plan-llm-cost-guardrails/SKILL.md +176 -0
  299. package/skills/plan-mobile-readiness/SKILL.md +171 -0
  300. package/skills/plan-perf-audit/SKILL.md +145 -0
  301. package/skills/plan-perf-audit/references/audit-scope.md +51 -0
  302. package/skills/plan-perf-audit/references/output-templates.md +33 -0
  303. package/skills/plan-perf-audit/references/preservation-contract.md +13 -0
  304. package/skills/plan-pricing/SKILL.md +173 -0
  305. package/skills/plan-privacy-compliance/SKILL.md +148 -0
  306. package/skills/plan-rls-audit/SKILL.md +231 -0
  307. package/skills/plan-secrets-audit/SKILL.md +181 -0
  308. package/skills/plan-security-audit/SKILL.md +168 -0
  309. package/skills/plan-security-audit/references/output-templates.md +36 -0
  310. package/skills/plan-security-audit/references/owasp-supabase-scope.md +55 -0
  311. package/skills/plan-security-audit/references/preservation-contract.md +18 -0
  312. package/skills/plan-stub-checker/SKILL.md +216 -0
  313. package/skills/plan-stub-checker/references/detection-methodology.md +75 -0
  314. package/skills/plan-stub-checker/references/detection-taxonomy.md +34 -0
  315. package/skills/plan-stub-checker/references/output-templates.md +63 -0
  316. package/skills/plan-stub-checker/references/preservation-contract.md +24 -0
  317. package/skills/plan-test-coverage/SKILL.md +170 -0
  318. package/skills/plan-test-coverage/references/methodology.md +54 -0
  319. package/skills/plan-test-coverage/references/output-templates.md +34 -0
  320. package/skills/plan-test-coverage/references/preservation-contract.md +15 -0
  321. package/skills/plan-uiux-unification/SKILL.md +230 -0
  322. package/skills/plan-uiux-unification/references/output-templates.md +67 -0
  323. package/skills/plan-uiux-unification/references/phase-workbook.md +85 -0
  324. package/skills/plan-uiux-unification/references/preservation-contract.md +24 -0
  325. package/skills/protocol-browser-anti-stall/SKILL.md +211 -0
  326. package/skills/protocol-browser-anti-stall/references/mcp-to-cli-map.md +113 -0
  327. package/skills/protocol-browser-anti-stall/references/playwright-session-coordination.md +170 -0
  328. package/skills/research/SKILL.md +422 -0
  329. package/skills/test-exploratory/SKILL.md +165 -0
  330. package/skills/test-exploratory/references/charter-template.md +29 -0
  331. package/skills/test-load/SKILL.md +126 -0
  332. package/skills/test-mutation/SKILL.md +160 -0
  333. package/skills/test-playwright/SKILL.md +354 -0
  334. package/skills/test-qa/SKILL.md +364 -0
  335. package/skills/test-qa/references/details.md +268 -0
  336. package/skills/test-red-team/SKILL.md +387 -0
  337. package/skills/test-red-team/references/owasp-attack-checklist.md +193 -0
  338. package/skills/test-unit/SKILL.md +259 -0
  339. package/skills/test-unit/references/details.md +267 -0
  340. package/skills/test-visual-regression/SKILL.md +132 -0
  341. package/skills/thirdparty-emil-design-eng/ATTRIBUTION.md +20 -0
  342. package/skills/thirdparty-emil-design-eng/SKILL.md +21 -0
  343. package/skills/thirdparty-emil-design-eng/references/emil-design-eng.md +676 -0
  344. package/skills/thirdparty-ui-ux-pro-max/ATTRIBUTION.md +22 -0
  345. package/skills/thirdparty-ui-ux-pro-max/SKILL.md +304 -0
  346. package/skills/thirdparty-ui-ux-pro-max/data/charts.csv +26 -0
  347. package/skills/thirdparty-ui-ux-pro-max/data/colors.csv +97 -0
  348. package/skills/thirdparty-ui-ux-pro-max/data/icons.csv +101 -0
  349. package/skills/thirdparty-ui-ux-pro-max/data/landing.csv +31 -0
  350. package/skills/thirdparty-ui-ux-pro-max/data/products.csv +97 -0
  351. package/skills/thirdparty-ui-ux-pro-max/data/react-performance.csv +45 -0
  352. package/skills/thirdparty-ui-ux-pro-max/data/stacks/astro.csv +54 -0
  353. package/skills/thirdparty-ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  354. package/skills/thirdparty-ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  355. package/skills/thirdparty-ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
  356. package/skills/thirdparty-ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  357. package/skills/thirdparty-ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  358. package/skills/thirdparty-ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  359. package/skills/thirdparty-ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  360. package/skills/thirdparty-ui-ux-pro-max/data/stacks/react.csv +54 -0
  361. package/skills/thirdparty-ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
  362. package/skills/thirdparty-ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  363. package/skills/thirdparty-ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  364. package/skills/thirdparty-ui-ux-pro-max/data/stacks/vue.csv +50 -0
  365. package/skills/thirdparty-ui-ux-pro-max/data/styles.csv +68 -0
  366. package/skills/thirdparty-ui-ux-pro-max/data/typography.csv +58 -0
  367. package/skills/thirdparty-ui-ux-pro-max/data/ui-reasoning.csv +101 -0
  368. package/skills/thirdparty-ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  369. package/skills/thirdparty-ui-ux-pro-max/data/web-interface.csv +31 -0
  370. package/skills/thirdparty-ui-ux-pro-max/scripts/core.py +253 -0
  371. package/skills/thirdparty-ui-ux-pro-max/scripts/design_system.py +1067 -0
  372. package/skills/thirdparty-ui-ux-pro-max/scripts/search.py +114 -0
  373. package/skills/thirdparty-web-interface-guidelines/ATTRIBUTION.md +23 -0
  374. package/skills/thirdparty-web-interface-guidelines/SKILL.md +190 -0
  375. package/skills/workflow-build-feature/SKILL.md +118 -0
  376. package/skills/workflow-coding-discipline/SKILL.md +140 -0
  377. package/skills/workflow-environment-ready/SKILL.md +128 -0
  378. package/skills/workflow-feature-flag/SKILL.md +262 -0
  379. package/skills/workflow-feedback-to-closure/SKILL.md +165 -0
  380. package/skills/workflow-fix-and-ship/SKILL.md +136 -0
  381. package/skills/workflow-git-commit/SKILL.md +200 -0
  382. package/skills/workflow-green-repo/SKILL.md +166 -0
  383. package/skills/workflow-grilling/SKILL.md +73 -0
  384. package/skills/workflow-gtm/SKILL.md +153 -0
  385. package/skills/workflow-housekeep/SKILL.md +453 -0
  386. package/skills/workflow-housekeep/references/templates.md +109 -0
  387. package/skills/workflow-launch-ready/SKILL.md +145 -0
  388. package/skills/workflow-merge-conflicts/SKILL.md +62 -0
  389. package/skills/workflow-onboard/SKILL.md +99 -0
  390. package/skills/workflow-parallel-agents/SKILL.md +164 -0
  391. package/skills/workflow-pr/SKILL.md +197 -0
  392. package/skills/workflow-quality-gate/SKILL.md +147 -0
  393. package/skills/workflow-refactor/SKILL.md +274 -0
  394. package/skills/workflow-release-prep/SKILL.md +207 -0
  395. package/skills/workflow-ship-and-observe/SKILL.md +164 -0
  396. package/skills/workflow-spec-tdd/SKILL.md +141 -0
  397. package/skills/workflow-spec-tdd/references/spec-template.md +126 -0
  398. package/skills/workflow-spec-tdd/references/tdd-patterns.md +167 -0
  399. package/skills-cursor/babysit/SKILL.md +17 -0
  400. package/skills-cursor/canvas/SKILL.md +142 -0
  401. package/skills-cursor/canvas/sdk/canvas-tokens.d.ts +235 -0
  402. package/skills-cursor/canvas/sdk/chart-primitives.d.ts +200 -0
  403. package/skills-cursor/canvas/sdk/dag-layout.d.ts +102 -0
  404. package/skills-cursor/canvas/sdk/diff-view.d.ts +130 -0
  405. package/skills-cursor/canvas/sdk/form-primitives.d.ts +194 -0
  406. package/skills-cursor/canvas/sdk/hooks.d.ts +117 -0
  407. package/skills-cursor/canvas/sdk/index.d.ts +47 -0
  408. package/skills-cursor/canvas/sdk/theme.d.ts +61 -0
  409. package/skills-cursor/canvas/sdk/todo-list.d.ts +49 -0
  410. package/skills-cursor/canvas/sdk/ui-primitives.d.ts +549 -0
  411. package/skills-cursor/canvas/sdk/ui-primitives.test.d.ts +2 -0
  412. package/skills-cursor/create-hook/SKILL.md +238 -0
  413. package/skills-cursor/create-rule/SKILL.md +185 -0
  414. package/skills-cursor/create-skill/SKILL.md +269 -0
  415. package/skills-cursor/create-skill/references/authoring-guide.md +182 -0
  416. package/skills-cursor/create-subagent/SKILL.md +228 -0
  417. package/skills-cursor/migrate-to-skills/SKILL.md +121 -0
  418. package/skills-cursor/shell/SKILL.md +22 -0
  419. package/skills-cursor/split-to-prs/SKILL.md +47 -0
  420. package/skills-cursor/statusline/SKILL.md +193 -0
  421. package/skills-cursor/update-cli-config/SKILL.md +85 -0
  422. package/skills-cursor/update-cursor-settings/SKILL.md +137 -0
  423. package/skills.sh.json +296 -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
+ });