dsh-desktop-kit 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 (269) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +152 -2
  3. package/client.js +4101 -0
  4. package/cordis.patch.yml +5 -0
  5. package/docs/ARCHITECTURE.md +114 -0
  6. package/docs/COMPATIBILITY.md +61 -0
  7. package/docs/INSTALL.md +84 -0
  8. package/docs/PRIVACY.md +50 -0
  9. package/docs/ROADMAP.md +57 -0
  10. package/docs/TOKENS.md +118 -0
  11. package/docs/TROUBLESHOOTING.md +68 -0
  12. package/docs/images/hero.png +0 -0
  13. package/index.js +84 -0
  14. package/package.json +70 -4
  15. package/plugins/dsh-gkb-ru-ui/cordis.patch.yml +6 -0
  16. package/plugins/dsh-gkb-ru-ui/lib/client.js +1331 -0
  17. package/plugins/dsh-gkb-ru-ui/lib/index.js +186 -0
  18. package/plugins/dsh-gkb-ru-ui/lib/skills.js +570 -0
  19. package/plugins/dsh-gkb-ru-ui/package.json +28 -0
  20. package/plugins/dsh-gkb-ru-ui/screenshots/01-home-tiles-1440-light.png +0 -0
  21. package/plugins/dsh-gkb-ru-ui/screenshots/02-skills-1440-light.png +0 -0
  22. package/plugins/dsh-gkb-ru-ui/screenshots/03-skills-editor-1440-light.png +0 -0
  23. package/plugins/dsh-gkb-ru-ui/screenshots/04-skills-1440-dark.png +0 -0
  24. package/plugins/dsh-gkb-ru-ui/screenshots/05-skills-390-light.png +0 -0
  25. package/plugins/dsh-gkb-ru-ui/screenshots/06-home-tiles-1440-dark.png +0 -0
  26. package/plugins/dsh-peak-guard/README.md +70 -0
  27. package/plugins/dsh-peak-guard/client.js +741 -0
  28. package/plugins/dsh-peak-guard/cordis.patch.yml +5 -0
  29. package/plugins/dsh-peak-guard/icon.svg +12 -0
  30. package/plugins/dsh-peak-guard/index.js +2 -0
  31. package/plugins/dsh-peak-guard/locale/en.json +6 -0
  32. package/plugins/dsh-peak-guard/locale/ru.json +6 -0
  33. package/plugins/dsh-peak-guard/locale/zh.json +6 -0
  34. package/plugins/dsh-peak-guard/package.json +33 -0
  35. package/plugins/dsh-peak-guard/screenshots/01-real-time-peak-start.png +0 -0
  36. package/plugins/dsh-peak-guard/screenshots/02-real-time-peak-end.png +0 -0
  37. package/plugins/dsh-peak-guard/screenshots/03-peak-card-1440-light.png +0 -0
  38. package/plugins/dsh-peak-guard/screenshots/04-peak-card-1440-dark.png +0 -0
  39. package/plugins/dsh-peak-guard/screenshots/05-peak-card-390-light.png +0 -0
  40. package/plugins/dsh-peak-guard/screenshots/05-peak-card-768-light.png +0 -0
  41. package/plugins/dsh-peak-guard/screenshots/06-peak-card-390-light.png +0 -0
  42. package/plugins/dsh-peak-guard/screenshots/07-live-preview-check.png +0 -0
  43. package/plugins/dsh-peak-guard/screenshots/07-pre-peak-1440-light.png +0 -0
  44. package/plugins/dsh-peak-guard/screenshots/08-peak-start-1440-light.png +0 -0
  45. package/plugins/dsh-peak-guard/screenshots/09-peak-end-1440-light.png +0 -0
  46. package/plugins/dsh-peak-guard/screenshots/10-pre-peak-card-1440-light.png +0 -0
  47. package/plugins/dsh-peak-guard/screenshots/11-pre-peak-card-1440-dark.png +0 -0
  48. package/plugins/dsh-peak-guard/screenshots/12-pre-peak-card-390-light.png +0 -0
  49. package/plugins/dsh-peak-guard/screenshots/qa-report.json +74 -0
  50. package/plugins/dsh-profile-transfer/README.md +62 -0
  51. package/plugins/dsh-profile-transfer/bootstrap/01-INSTALL.cmd +7 -0
  52. package/plugins/dsh-profile-transfer/bootstrap/README-ARCHIVE.md +54 -0
  53. package/plugins/dsh-profile-transfer/bootstrap/README-/320/243/320/241/320/242/320/220/320/235/320/236/320/222/320/232/320/220.md +42 -0
  54. package/plugins/dsh-profile-transfer/bootstrap/install-on-clean-machine.ps1 +119 -0
  55. package/plugins/dsh-profile-transfer/bootstrap//320/243/320/241/320/242/320/220/320/235/320/236/320/222/320/230/320/242/320/254.cmd +5 -0
  56. package/plugins/dsh-profile-transfer/client.js +806 -0
  57. package/plugins/dsh-profile-transfer/cordis.patch.yml +3 -0
  58. package/plugins/dsh-profile-transfer/icon.svg +13 -0
  59. package/plugins/dsh-profile-transfer/index.js +375 -0
  60. package/plugins/dsh-profile-transfer/locale/en.json +6 -0
  61. package/plugins/dsh-profile-transfer/locale/ru.json +6 -0
  62. package/plugins/dsh-profile-transfer/locale/zh.json +6 -0
  63. package/plugins/dsh-profile-transfer/package.json +36 -0
  64. package/plugins/dsh-profile-transfer/screenshots/01-settings-transfer-1440-light.png +0 -0
  65. package/plugins/dsh-profile-transfer/screenshots/02-settings-transfer-preview-1440-light.png +0 -0
  66. package/plugins/dsh-profile-transfer/screenshots/03-settings-transfer-1440-dark.png +0 -0
  67. package/plugins/dsh-profile-transfer/screenshots/04-settings-transfer-390-light.png +0 -0
  68. package/plugins/dsh-profile-transfer/screenshots/05-settings-transfer-768-light.png +0 -0
  69. package/plugins/dsh-profile-transfer/screenshots/06-settings-transfer-390-dark.png +0 -0
  70. package/plugins/dsh-profile-transfer/screenshots/07-settings-transfer-export-result.png +0 -0
  71. package/plugins/dsh-profile-transfer/screenshots/08-settings-transfer-workspaces-1440-light.png +0 -0
  72. package/plugins/dsh-profile-transfer/screenshots/09-settings-transfer-workspaces-1440-dark.png +0 -0
  73. package/plugins/dsh-profile-transfer/screenshots/10-settings-transfer-workspaces-390-light.png +0 -0
  74. package/plugins/dsh-profile-transfer/transfer.js +1004 -0
  75. package/plugins/dsh-project-memory/README.md +71 -0
  76. package/plugins/dsh-project-memory/cordis.patch.yml +4 -0
  77. package/plugins/dsh-project-memory/icon.svg +12 -0
  78. package/plugins/dsh-project-memory/index.js +1240 -0
  79. package/plugins/dsh-project-memory/locale/en.json +6 -0
  80. package/plugins/dsh-project-memory/locale/ru.json +6 -0
  81. package/plugins/dsh-project-memory/locale/zh.json +6 -0
  82. package/plugins/dsh-project-memory/package.json +26 -0
  83. package/plugins/dsh-session-move/README.md +67 -0
  84. package/plugins/dsh-session-move/client.js +719 -0
  85. package/plugins/dsh-session-move/cordis.patch.yml +5 -0
  86. package/plugins/dsh-session-move/icon.svg +14 -0
  87. package/plugins/dsh-session-move/index.js +497 -0
  88. package/plugins/dsh-session-move/locale/en.json +6 -0
  89. package/plugins/dsh-session-move/locale/ru.json +6 -0
  90. package/plugins/dsh-session-move/locale/zh.json +6 -0
  91. package/plugins/dsh-session-move/package.json +31 -0
  92. package/plugins/dsh-session-move/screenshots/01-row-menu-1440-light.png +0 -0
  93. package/plugins/dsh-session-move/screenshots/02-dialog-1440-light.png +0 -0
  94. package/plugins/dsh-session-move/screenshots/03-dialog-390-light.png +0 -0
  95. package/plugins/dsh-session-move/screenshots/04-dialog-1440-dark.png +0 -0
  96. package/plugins/dsh-session-move/screenshots/05-drag-highlight-1440-light.png +0 -0
  97. package/plugins/dsh-session-move/screenshots/06-drop-ungrouped-highlight.png +0 -0
  98. package/plugins/dsh-session-move/screenshots/07-after-drop-ungrouped.png +0 -0
  99. package/plugins/dsh-session-move/screenshots/08-move-dialog.png +0 -0
  100. package/plugins/dsh-session-move/screenshots/09-moved-to-second-workspace.png +0 -0
  101. package/plugins/dsh-session-move/screenshots/qa-report.json +62 -0
  102. package/plugins/dsh-skill-autopilot/cordis.patch.yml +3 -0
  103. package/plugins/dsh-skill-autopilot/index.js +444 -0
  104. package/plugins/dsh-skill-autopilot/package.json +19 -0
  105. package/plugins/dsh-token-lab/cordis.patch.yml +3 -0
  106. package/plugins/dsh-token-lab/lib/client.js +504 -0
  107. package/plugins/dsh-token-lab/lib/index.js +275 -0
  108. package/plugins/dsh-token-lab/package.json +26 -0
  109. package/plugins/dsh-token-lab/screenshots/01-token-lab-1440-light.png +0 -0
  110. package/plugins/dsh-token-lab/screenshots/02-token-lab-1440-dark.png +0 -0
  111. package/skills/code-review-checklist/SKILL.md +55 -0
  112. package/skills/content-writing/README.md +87 -0
  113. package/skills/content-writing/SKILL.md +85 -0
  114. package/skills/content-writing/assets/checklists/fact-check.md +53 -0
  115. package/skills/content-writing/assets/checklists/publish.md +65 -0
  116. package/skills/content-writing/assets/checklists/social.md +40 -0
  117. package/skills/content-writing/assets/templates/article.md +47 -0
  118. package/skills/content-writing/assets/templates/brief.md +44 -0
  119. package/skills/content-writing/assets/templates/doctor-card.md +41 -0
  120. package/skills/content-writing/assets/templates/image-prompts.md +128 -0
  121. package/skills/content-writing/assets/templates/post-telegram.md +36 -0
  122. package/skills/content-writing/assets/templates/post-vk.md +42 -0
  123. package/skills/content-writing/assets/templates/press-release.md +56 -0
  124. package/skills/content-writing/assets/templates/science-pop-note.md +55 -0
  125. package/skills/content-writing/assets/templates/service-card.md +46 -0
  126. package/skills/content-writing/references/anti-ai-style.md +248 -0
  127. package/skills/content-writing/references/editing.md +185 -0
  128. package/skills/content-writing/references/fact-checking.md +205 -0
  129. package/skills/content-writing/references/government-medical.md +246 -0
  130. package/skills/content-writing/references/images.md +212 -0
  131. package/skills/content-writing/references/journalism.md +252 -0
  132. package/skills/content-writing/references/process.md +123 -0
  133. package/skills/content-writing/references/quick-recipes.md +99 -0
  134. package/skills/content-writing/references/rules.md +85 -0
  135. package/skills/content-writing/references/science-popular.md +218 -0
  136. package/skills/content-writing/references/seo-content.md +172 -0
  137. package/skills/content-writing/references/social-media.md +253 -0
  138. package/skills/content-writing/references/sources.md +49 -0
  139. package/skills/content-writing/references/templates.md +291 -0
  140. package/skills/context-economy/SKILL.md +60 -0
  141. package/skills/context-economy/references/long-session.md +35 -0
  142. package/skills/context-economy/references/token-math.md +42 -0
  143. package/skills/context-economy/references/tool-recipes.md +34 -0
  144. package/skills/database-safety/SKILL.md +47 -0
  145. package/skills/debugging-playbook/SKILL.md +42 -0
  146. package/skills/design-critique/SKILL.md +65 -0
  147. package/skills/design-critique/references/a11y.md +25 -0
  148. package/skills/design-critique/references/diagnoses.md +33 -0
  149. package/skills/design-critique/references/quick-tests.md +30 -0
  150. package/skills/design-critique/references/report-template.md +63 -0
  151. package/skills/design-critique/references/rubric-fast.md +73 -0
  152. package/skills/dev-workflow/SKILL.md +48 -0
  153. package/skills/dev-workflow/references/definition-of-done.md +44 -0
  154. package/skills/dev-workflow/references/memory-and-boundaries.md +31 -0
  155. package/skills/dev-workflow/references/stages.md +67 -0
  156. package/skills/git-and-ci/SKILL.md +60 -0
  157. package/skills/pg-upgrade-verify/SKILL.md +67 -0
  158. package/skills/project-deep-dive/SKILL.md +45 -0
  159. package/skills/project-deep-dive/references/recon-commands.md +90 -0
  160. package/skills/project-deep-dive/references/section-templates.md +104 -0
  161. package/skills/project-deep-dive/references/tool-and-process.md +86 -0
  162. package/skills/project-memory-maintenance/SKILL.md +55 -0
  163. package/skills/project-memory-maintenance/references/api-and-checklist.md +99 -0
  164. package/skills/project-memory-maintenance/references/journal-template.md +50 -0
  165. package/skills/project-memory-maintenance/references/section-rules.md +59 -0
  166. package/skills/release-and-deploy/SKILL.md +53 -0
  167. package/skills/requirements-and-planning/SKILL.md +49 -0
  168. package/skills/requirements-and-planning/references/antipatterns-and-checklist.md +20 -0
  169. package/skills/requirements-and-planning/references/process-steps.md +52 -0
  170. package/skills/requirements-and-planning/references/templates-and-estimates.md +29 -0
  171. package/skills/security-review/SKILL.md +40 -0
  172. package/skills/seo/README.md +60 -0
  173. package/skills/seo/SKILL.md +77 -0
  174. package/skills/seo/assets/README.md +66 -0
  175. package/skills/seo/assets/scripts/LICENSE-playwright-core.txt +202 -0
  176. package/skills/seo/assets/scripts/README.md +179 -0
  177. package/skills/seo/assets/scripts/lib/args.mjs +105 -0
  178. package/skills/seo/assets/scripts/lib/browser.mjs +620 -0
  179. package/skills/seo/assets/scripts/lib/zip.mjs +191 -0
  180. package/skills/seo/assets/scripts/seo-audit.mjs +1675 -0
  181. package/skills/seo/assets/snippets/jsonld-breadcrumb.json +30 -0
  182. package/skills/seo/assets/snippets/jsonld-faq.json +39 -0
  183. package/skills/seo/assets/snippets/jsonld-medical-clinic.json +49 -0
  184. package/skills/seo/assets/snippets/jsonld-medical-webpage.json +33 -0
  185. package/skills/seo/assets/snippets/jsonld-organization.json +74 -0
  186. package/skills/seo/assets/snippets/jsonld-physician.json +46 -0
  187. package/skills/seo/assets/snippets/meta.blade.php +61 -0
  188. package/skills/seo/assets/snippets/robots.txt +61 -0
  189. package/skills/seo/assets/snippets/sitemap-urlset-example.xml +72 -0
  190. package/skills/seo/assets/snippets/sitemap.xml +36 -0
  191. package/skills/seo/references/antipatterns.md +66 -0
  192. package/skills/seo/references/audit-checklist.md +319 -0
  193. package/skills/seo/references/content-seo.md +204 -0
  194. package/skills/seo/references/eeat-medical.md +219 -0
  195. package/skills/seo/references/measurement.md +250 -0
  196. package/skills/seo/references/on-page.md +214 -0
  197. package/skills/seo/references/page-templates.md +56 -0
  198. package/skills/seo/references/stack-recipes.md +695 -0
  199. package/skills/seo/references/structured-data.md +487 -0
  200. package/skills/seo/references/technical-seo.md +296 -0
  201. package/skills/service-desk-conventions/SKILL.md +52 -0
  202. package/skills/service-desk-db-safety/SKILL.md +63 -0
  203. package/skills/service-desk-release/SKILL.md +61 -0
  204. package/skills/service-desk-testing/SKILL.md +61 -0
  205. package/skills/service-desk-ui/SKILL.md +52 -0
  206. package/skills/skills-router/SKILL.md +53 -0
  207. package/skills/skills-router/references/applying-skills.md +25 -0
  208. package/skills/skills-router/references/routing-matrix.md +52 -0
  209. package/skills/testing-strategy/SKILL.md +52 -0
  210. package/skills/ui-design-system/SKILL.md +50 -0
  211. package/skills/ui-design-system/references/accessibility-and-responsive.md +50 -0
  212. package/skills/ui-design-system/references/tokens-and-components.md +67 -0
  213. package/skills/ui-design-system/references/visual-qa.md +44 -0
  214. package/skills/web-research/SKILL.md +47 -0
  215. package/skills/web-research/references/answer-and-citations.md +60 -0
  216. package/skills/web-research/references/query-recipes.md +44 -0
  217. package/skills/wow-design/SKILL.md +89 -0
  218. package/skills/wow-design/assets/kit/README.md +195 -0
  219. package/skills/wow-design/assets/kit/assets/css/base.css +158 -0
  220. package/skills/wow-design/assets/kit/assets/css/components.css +1001 -0
  221. package/skills/wow-design/assets/kit/assets/css/fonts.css +73 -0
  222. package/skills/wow-design/assets/kit/assets/css/pages.css +737 -0
  223. package/skills/wow-design/assets/kit/assets/css/tokens.css +218 -0
  224. package/skills/wow-design/assets/kit/assets/fonts/LICENSE-golos-text.txt +93 -0
  225. package/skills/wow-design/assets/kit/assets/fonts/LICENSE-lora.txt +93 -0
  226. package/skills/wow-design/assets/kit/assets/fonts/LICENSE-unbounded.txt +93 -0
  227. package/skills/wow-design/assets/kit/assets/fonts/golos-text-cyrillic-400-normal.woff2 +0 -0
  228. package/skills/wow-design/assets/kit/assets/fonts/golos-text-cyrillic-500-normal.woff2 +0 -0
  229. package/skills/wow-design/assets/kit/assets/fonts/golos-text-cyrillic-600-normal.woff2 +0 -0
  230. package/skills/wow-design/assets/kit/assets/fonts/golos-text-cyrillic-700-normal.woff2 +0 -0
  231. package/skills/wow-design/assets/kit/assets/fonts/golos-text-latin-400-normal.woff2 +0 -0
  232. package/skills/wow-design/assets/kit/assets/fonts/golos-text-latin-500-normal.woff2 +0 -0
  233. package/skills/wow-design/assets/kit/assets/fonts/golos-text-latin-600-normal.woff2 +0 -0
  234. package/skills/wow-design/assets/kit/assets/fonts/golos-text-latin-700-normal.woff2 +0 -0
  235. package/skills/wow-design/assets/kit/assets/fonts/lora-cyrillic-400-italic.woff2 +0 -0
  236. package/skills/wow-design/assets/kit/assets/fonts/lora-cyrillic-400-normal.woff2 +0 -0
  237. package/skills/wow-design/assets/kit/assets/fonts/lora-cyrillic-500-normal.woff2 +0 -0
  238. package/skills/wow-design/assets/kit/assets/fonts/lora-cyrillic-600-normal.woff2 +0 -0
  239. package/skills/wow-design/assets/kit/assets/fonts/lora-latin-400-normal.woff2 +0 -0
  240. package/skills/wow-design/assets/kit/assets/fonts/lora-latin-500-normal.woff2 +0 -0
  241. package/skills/wow-design/assets/kit/assets/fonts/lora-latin-600-normal.woff2 +0 -0
  242. package/skills/wow-design/assets/kit/assets/fonts/unbounded-cyrillic-400-normal.woff2 +0 -0
  243. package/skills/wow-design/assets/kit/assets/fonts/unbounded-cyrillic-500-normal.woff2 +0 -0
  244. package/skills/wow-design/assets/kit/assets/fonts/unbounded-cyrillic-600-normal.woff2 +0 -0
  245. package/skills/wow-design/assets/kit/assets/fonts/unbounded-latin-400-normal.woff2 +0 -0
  246. package/skills/wow-design/assets/kit/assets/fonts/unbounded-latin-500-normal.woff2 +0 -0
  247. package/skills/wow-design/assets/kit/assets/fonts/unbounded-latin-600-normal.woff2 +0 -0
  248. package/skills/wow-design/assets/kit/assets/js/kit.js +550 -0
  249. package/skills/wow-design/assets/kit/pages/dashboard.html +539 -0
  250. package/skills/wow-design/assets/kit/pages/hospital.html +430 -0
  251. package/skills/wow-design/assets/kit/showcase.html +1164 -0
  252. package/skills/wow-design/assets/scripts/LICENSE-axe-core.txt +362 -0
  253. package/skills/wow-design/assets/scripts/LICENSE-playwright-core.txt +202 -0
  254. package/skills/wow-design/assets/scripts/README.md +312 -0
  255. package/skills/wow-design/assets/scripts/audit.mjs +1200 -0
  256. package/skills/wow-design/assets/scripts/capture.mjs +337 -0
  257. package/skills/wow-design/assets/scripts/package.json +30 -0
  258. package/skills/wow-design/references/anti-patterns.md +252 -0
  259. package/skills/wow-design/references/critique-rubric.md +262 -0
  260. package/skills/wow-design/references/design-brief-template.md +48 -0
  261. package/skills/wow-design/references/inspiration.md +203 -0
  262. package/skills/wow-design/references/presets.md +99 -0
  263. package/skills/wow-design/references/principles.md +371 -0
  264. package/skills/wow-design/references/quick-presets.md +50 -0
  265. package/skills/wow-design/references/report-template.md +21 -0
  266. package/skills/wow-design/references/stack-recipes.md +96 -0
  267. package/skills/wow-design/references/visual-qa.md +82 -0
  268. package/skills/wow-design/references/wow-levers.md +43 -0
  269. package/tools/skill-cost.mjs +76 -0
@@ -0,0 +1,620 @@
1
+ // Общая инфраструктура для capture.mjs / audit.mjs:
2
+ // - подключение к уже запущенному headless Chrome по CDP (http://127.0.0.1:9333),
3
+ // fallback — самостоятельный запуск chrome.exe с временным профилем;
4
+ // - создание страницы/контекста с нужным viewport/DPR/темой;
5
+ // - подготовка страницы к скриншоту: тема, шрифты, отключение анимаций,
6
+ // прокрутка для reveal/lazy, стабилизация;
7
+ // - сбор ошибок консоли/сети;
8
+ // - локальный статический сервер (для страниц с ES-модулями, которым нужен http://).
9
+
10
+ import fs from 'node:fs';
11
+ import os from 'node:os';
12
+ import net from 'node:net';
13
+ import http from 'node:http';
14
+ import path from 'node:path';
15
+ import { spawn } from 'node:child_process';
16
+ import { pathToFileURL } from 'node:url';
17
+ import { chromium } from 'playwright-core';
18
+ import { UsageError } from './args.mjs';
19
+
20
+ export const DEFAULT_CDP_URL = process.env.CDP_URL || 'http://127.0.0.1:9333';
21
+
22
+ export const VIEWPORTS = {
23
+ desktop: { id: 'desktop', width: 1440, height: 900 },
24
+ tablet: { id: 'tablet', width: 768, height: 1024 },
25
+ mobile: { id: 'mobile', width: 390, height: 844 },
26
+ };
27
+
28
+ export const THEMES = ['light', 'dark'];
29
+
30
+ const CHROME_CANDIDATES = [
31
+ process.env.CHROME_PATH,
32
+ 'C:\\Users\\adm\\AppData\\Local\\ms-playwright\\chromium-1248\\chrome-win64\\chrome.exe',
33
+ path.join(os.homedir(), 'AppData', 'Local', 'ms-playwright', 'chromium-1248', 'chrome-win64', 'chrome.exe'),
34
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
35
+ 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
36
+ 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
37
+ 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
38
+ ];
39
+
40
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
41
+
42
+ export function findChrome() {
43
+ // 1) явный путь из окружения / список кандидатов
44
+ for (const candidate of CHROME_CANDIDATES) {
45
+ if (candidate && fs.existsSync(candidate)) return candidate;
46
+ }
47
+ // 2) поиск chromium-* в ms-playwright (на случай другой ревизии)
48
+ const pwDir = path.join(os.homedir(), 'AppData', 'Local', 'ms-playwright');
49
+ try {
50
+ const dirs = fs
51
+ .readdirSync(pwDir, { withFileTypes: true })
52
+ .filter((d) => d.isDirectory() && d.name.startsWith('chromium-'))
53
+ .map((d) => path.join(pwDir, d.name, 'chrome-win64', 'chrome.exe'))
54
+ .filter((p) => fs.existsSync(p))
55
+ .sort()
56
+ .reverse();
57
+ if (dirs.length) return dirs[0];
58
+ } catch {
59
+ /* нет каталога — не страшно */
60
+ }
61
+ return null;
62
+ }
63
+
64
+ export async function probeCdp(cdpUrl, timeoutMs = 1500) {
65
+ try {
66
+ const base = String(cdpUrl).replace(/\/+$/, '');
67
+ const res = await fetch(`${base}/json/version`, { signal: AbortSignal.timeout(timeoutMs) });
68
+ if (!res.ok) return null;
69
+ const json = await res.json();
70
+ return json && json.webSocketDebuggerUrl ? json : null;
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ export async function getFreePort() {
77
+ return new Promise((resolve, reject) => {
78
+ const srv = net.createServer();
79
+ srv.unref();
80
+ srv.on('error', reject);
81
+ srv.listen(0, '127.0.0.1', () => {
82
+ const port = srv.address().port;
83
+ srv.close(() => resolve(port));
84
+ });
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Подключиться к CDP или (если недоступен) запустить собственный Chromium.
90
+ * @returns {{browser: import('playwright-core').Browser, launched: boolean, endpoint: string, version: any, child?: any, profileDir?: string}}
91
+ */
92
+ export async function connectChrome({ cdpUrl = DEFAULT_CDP_URL, log = () => {}, allowLaunch = true } = {}) {
93
+ let version = await probeCdp(cdpUrl);
94
+ if (version) {
95
+ const browser = await chromium.connectOverCDP(cdpUrl);
96
+ return { browser, launched: false, endpoint: cdpUrl, version };
97
+ }
98
+
99
+ if (!allowLaunch) throw new Error(`CDP недоступен: ${cdpUrl}`);
100
+
101
+ log(`[qa] CDP ${cdpUrl} недоступен — fallback: запускаю собственный headless Chromium`);
102
+ const chrome = findChrome();
103
+ if (!chrome) {
104
+ throw new Error(
105
+ 'Не найден chrome.exe. Укажите переменную CHROME_PATH или запустите Chrome с --remote-debugging-port=9333',
106
+ );
107
+ }
108
+ const port = await getFreePort();
109
+ const profileDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-qa-chrome-'));
110
+ const args = [
111
+ '--headless=new',
112
+ `--remote-debugging-port=${port}`,
113
+ `--user-data-dir=${profileDir}`,
114
+ '--no-first-run',
115
+ '--no-default-browser-check',
116
+ '--disable-gpu',
117
+ '--hide-scrollbars',
118
+ '--allow-file-access-from-files',
119
+ '--force-color-profile=srgb',
120
+ '--disable-background-timer-throttling',
121
+ '--disable-renderer-backgrounding',
122
+ '--disable-dev-shm-usage',
123
+ 'about:blank',
124
+ ];
125
+ const child = spawn(chrome, args, { stdio: 'ignore', windowsHide: true });
126
+ child.on('error', () => {});
127
+ const endpoint = `http://127.0.0.1:${port}`;
128
+
129
+ let ready = null;
130
+ const deadline = Date.now() + 40_000;
131
+ while (Date.now() < deadline) {
132
+ ready = await probeCdp(endpoint, 1000);
133
+ if (ready) break;
134
+ await sleep(250);
135
+ }
136
+ if (!ready) {
137
+ try {
138
+ child.kill();
139
+ } catch {
140
+ /* ignore */
141
+ }
142
+ throw new Error('Не удалось дождаться CDP fallback-Chromium (40 c)');
143
+ }
144
+ const browser = await chromium.connectOverCDP(endpoint);
145
+ return { browser, launched: true, endpoint, version: ready, child, profileDir };
146
+ }
147
+
148
+ /** Аккуратно закрыть соединение: чужой Chrome не убиваем, свой — гасим. */
149
+ export async function disposeChrome(conn) {
150
+ if (!conn) return;
151
+ try {
152
+ // Для connectOverCDP закрытие соединения не завершает чужой браузер (проверено).
153
+ await Promise.race([conn.browser.close(), sleep(4000)]);
154
+ } catch {
155
+ /* ignore */
156
+ }
157
+ if (conn.child) {
158
+ try {
159
+ conn.child.kill();
160
+ } catch {
161
+ /* ignore */
162
+ }
163
+ }
164
+ if (conn.profileDir) {
165
+ await sleep(300);
166
+ try {
167
+ fs.rmSync(conn.profileDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 });
168
+ } catch {
169
+ /* временный профиль может остаться — не критично */
170
+ }
171
+ }
172
+ }
173
+
174
+ /** Примерно 15-17px скроллбара в чужом Chrome ломают layout-замеры — убираем его для QA. */
175
+ const SCROLLBAR_CSS = `
176
+ html { scrollbar-width: none !important; }
177
+ html::-webkit-scrollbar, body::-webkit-scrollbar, *::-webkit-scrollbar {
178
+ width: 0 !important; height: 0 !important; display: none !important;
179
+ }`;
180
+
181
+ /** Гасим анимации/переходы — скриншот должен быть детерминированным. */
182
+ const ANIMATION_CSS = `
183
+ *, *::before, *::after {
184
+ animation-duration: 0s !important;
185
+ animation-delay: 0s !important;
186
+ animation-iteration-count: 1 !important;
187
+ transition-duration: 0s !important;
188
+ transition-delay: 0s !important;
189
+ scroll-behavior: auto !important;
190
+ }
191
+ html { scroll-behavior: auto !important; }
192
+ @keyframes qa-freeze { from { opacity: 1; } to { opacity: 1; } }
193
+ `;
194
+
195
+ /**
196
+ * Создать страницу под конкретный скриншот/аудит.
197
+ * Нужные viewport/DPR/тема задаются на уровне контекста (работает через connectOverCDP),
198
+ * при невозможности — fallback: страница из существующего контекста + CDP-эмуляция.
199
+ */
200
+ export async function openPage(browser, { viewport = VIEWPORTS.desktop, dpr = 1, theme = 'light', reducedMotion = 'reduce' } = {}) {
201
+ try {
202
+ const context = await browser.newContext({
203
+ viewport: { width: viewport.width, height: viewport.height },
204
+ deviceScaleFactor: dpr,
205
+ colorScheme: theme,
206
+ reducedMotion,
207
+ locale: 'ru-RU',
208
+ timezoneId: 'Europe/Moscow',
209
+ });
210
+ const page = await context.newPage();
211
+ return { context, page, owned: true, dpr, viewport };
212
+ } catch {
213
+ // Старые/ограниченные сборки: работаем в первом доступном контексте.
214
+ const context = browser.contexts()[0] ?? (await browser.newContext());
215
+ const page = await context.newPage();
216
+ await page.setViewportSize({ width: viewport.width, height: viewport.height });
217
+ await page.emulateMedia({ colorScheme: theme, reducedMotion });
218
+ try {
219
+ const cdp = await context.newCDPSession(page);
220
+ await cdp.send('Emulation.setDeviceMetricsOverride', {
221
+ width: viewport.width,
222
+ height: viewport.height,
223
+ deviceScaleFactor: dpr,
224
+ mobile: false,
225
+ });
226
+ } catch {
227
+ /* DPR останется дефолтным */
228
+ }
229
+ return { context, page, owned: false, dpr, viewport };
230
+ }
231
+ }
232
+
233
+ export async function closePage(handle) {
234
+ if (!handle) return;
235
+ try {
236
+ if (handle.owned && handle.context && !handle.context.isClosed?.()) {
237
+ await handle.context.close();
238
+ } else {
239
+ await handle.page.close();
240
+ }
241
+ } catch {
242
+ /* ignore */
243
+ }
244
+ }
245
+
246
+ /** Подписаться на консоль/ошибки/сетевые сбои страницы. */
247
+ export function wireDiagnostics(page) {
248
+ const diag = { consoleErrors: [], pageErrors: [], failedRequests: [] };
249
+ page.on('console', (msg) => {
250
+ if (msg.type() !== 'error') return;
251
+ diag.consoleErrors.push({
252
+ type: 'console.error',
253
+ text: cleanText(msg.text()),
254
+ location: msg.location?.() ?? null,
255
+ });
256
+ });
257
+ page.on('pageerror', (err) => {
258
+ diag.pageErrors.push({
259
+ type: 'pageerror',
260
+ text: cleanText(`${err?.name || 'Error'}: ${err?.message || err}`),
261
+ });
262
+ });
263
+ page.on('requestfailed', (req) => {
264
+ const url = req.url();
265
+ if (url.startsWith('data:')) return;
266
+ diag.failedRequests.push({
267
+ type: 'requestfailed',
268
+ url: url.slice(0, 400),
269
+ error: req.failure()?.errorText || 'failed',
270
+ resourceType: req.resourceType(),
271
+ });
272
+ });
273
+ page.on('response', (resp) => {
274
+ if (resp.status() < 400) return;
275
+ const url = resp.url();
276
+ if (url.startsWith('data:')) return;
277
+ diag.failedRequests.push({
278
+ type: 'http',
279
+ url: url.slice(0, 400),
280
+ status: resp.status(),
281
+ resourceType: resp.request().resourceType(),
282
+ });
283
+ });
284
+ return diag;
285
+ }
286
+
287
+ function cleanText(text) {
288
+ return String(text ?? '')
289
+ .replace(/\u001b\[[0-9;]*m/g, '')
290
+ .trim()
291
+ .slice(0, 1200);
292
+ }
293
+
294
+ /** Дождаться загрузки веб-шрифтов; вернуть статус document.fonts. */
295
+ export async function waitForFonts(page) {
296
+ try {
297
+ return await page.evaluate(async () => {
298
+ try {
299
+ await document.fonts.ready;
300
+ } catch {
301
+ /* ignore */
302
+ }
303
+ const faces = [];
304
+ try {
305
+ document.fonts.forEach((f) => faces.push({ family: f.family, weight: f.weight, style: f.style, status: f.status }));
306
+ } catch {
307
+ /* ignore */
308
+ }
309
+ return { status: document.fonts.status, size: document.fonts.size, faces: faces.slice(0, 40) };
310
+ });
311
+ } catch {
312
+ return { status: 'unknown', size: 0, faces: [] };
313
+ }
314
+ }
315
+
316
+ /** Прокрутить страницу до конца и обратно — чтобы сработали reveal-анимации и lazy-load. */
317
+ export async function scrollThroughPage(page, { stepRatio = 0.8, pause = 70, maxSteps = 150 } = {}) {
318
+ try {
319
+ await page.evaluate(
320
+ async ({ stepRatio, pause, maxSteps }) => {
321
+ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
322
+ const docHeight = () =>
323
+ Math.max(
324
+ document.documentElement?.scrollHeight || 0,
325
+ document.body?.scrollHeight || 0,
326
+ window.innerHeight || 0,
327
+ );
328
+ const vh = window.innerHeight || 800;
329
+ window.scrollTo(0, 0);
330
+ await wait(pause);
331
+ let y = 0;
332
+ for (let step = 0; step < maxSteps; step++) {
333
+ window.scrollTo(0, y);
334
+ await wait(pause);
335
+ const h = docHeight();
336
+ if (y + vh >= h) break;
337
+ y += Math.max(160, Math.floor(vh * stepRatio));
338
+ }
339
+ // доскроллить до самого низа и вернуться наверх
340
+ window.scrollTo(0, docHeight());
341
+ await wait(pause * 2);
342
+ window.scrollTo(0, 0);
343
+ await wait(pause * 2);
344
+ },
345
+ { stepRatio, pause, maxSteps },
346
+ );
347
+ } catch {
348
+ /* страница могла закрыться/перейти — не критично */
349
+ }
350
+ }
351
+
352
+ /** Дождаться декодирования изображений (включая lazy). */
353
+ export async function loadAllImages(page) {
354
+ try {
355
+ await page.evaluate(async () => {
356
+ const images = Array.from(document.images || []);
357
+ for (const img of images) {
358
+ if (img.loading === 'lazy') img.loading = 'eager';
359
+ }
360
+ await Promise.all(
361
+ images.map((img) => {
362
+ if (img.complete && img.naturalWidth > 0) return Promise.resolve();
363
+ return new Promise((resolve) => {
364
+ const done = () => resolve();
365
+ img.addEventListener('load', done, { once: true });
366
+ img.addEventListener('error', done, { once: true });
367
+ setTimeout(done, 3000);
368
+ if (img.decode) img.decode().then(done, done);
369
+ });
370
+ }),
371
+ );
372
+ });
373
+ } catch {
374
+ /* ignore */
375
+ }
376
+ }
377
+
378
+ async function nextFrames(page, count = 3) {
379
+ try {
380
+ await page.evaluate(
381
+ (n) =>
382
+ new Promise((resolve) => {
383
+ let i = 0;
384
+ const tick = () => {
385
+ if (++i >= n) resolve();
386
+ else requestAnimationFrame(tick);
387
+ };
388
+ requestAnimationFrame(tick);
389
+ }),
390
+ count,
391
+ );
392
+ } catch {
393
+ /* ignore */
394
+ }
395
+ }
396
+
397
+ /** Применить data-theme/data-preset и переждать перерисовку. */
398
+ export async function applyTheme(page, { theme, preset } = {}) {
399
+ if (!theme && !preset) return;
400
+ try {
401
+ await page.evaluate(
402
+ ({ theme, preset }) => {
403
+ const el = document.documentElement;
404
+ if (!el) return;
405
+ if (theme) el.setAttribute('data-theme', theme);
406
+ if (preset) el.setAttribute('data-preset', preset);
407
+ if (theme) el.style.colorScheme = theme;
408
+ try {
409
+ window.dispatchEvent(new CustomEvent('qa:theme', { detail: { theme, preset } }));
410
+ } catch {
411
+ /* ignore */
412
+ }
413
+ },
414
+ { theme, preset: preset || null },
415
+ );
416
+ } catch {
417
+ /* ignore */
418
+ }
419
+ await sleep(120);
420
+ }
421
+
422
+ /** Зарегистрировать data-theme/data-preset до первой отрисовки (против FOUC/override от JS страницы). */
423
+ export async function addThemeInitScript(page, { theme, preset } = {}) {
424
+ try {
425
+ await page.addInitScript(
426
+ ({ theme, preset }) => {
427
+ const apply = () => {
428
+ const el = document.documentElement;
429
+ if (!el) return;
430
+ if (theme) el.setAttribute('data-theme', theme);
431
+ if (preset) el.setAttribute('data-preset', preset);
432
+ if (theme) el.style.colorScheme = theme;
433
+ };
434
+ apply();
435
+ document.addEventListener('DOMContentLoaded', apply, { once: true });
436
+ },
437
+ { theme: theme || null, preset: preset || null },
438
+ );
439
+ } catch {
440
+ /* ignore */
441
+ }
442
+ }
443
+
444
+ /** Полная подготовка страницы к скриншоту. Возвращает метрики для манифеста. */
445
+ export async function preparePage(page, { theme, preset, animations = true, scroll = true, log = () => {} } = {}) {
446
+ await applyTheme(page, { theme, preset });
447
+ await waitForFonts(page);
448
+ await nextFrames(page, 2);
449
+
450
+ await page.addStyleTag({ content: SCROLLBAR_CSS }).catch(() => {});
451
+ if (animations) await page.addStyleTag({ content: ANIMATION_CSS }).catch(() => {});
452
+
453
+ if (scroll) {
454
+ await scrollThroughPage(page);
455
+ await loadAllImages(page);
456
+ }
457
+ await nextFrames(page, 3);
458
+ await waitForFonts(page);
459
+ await applyTheme(page, { theme, preset }); // страница могла перезаписать тему своим JS
460
+ await nextFrames(page, 2);
461
+
462
+ const metrics = await page
463
+ .evaluate(() => {
464
+ const de = document.documentElement;
465
+ return {
466
+ title: document.title || '',
467
+ url: location.href,
468
+ innerWidth: window.innerWidth,
469
+ innerHeight: window.innerHeight,
470
+ clientWidth: de?.clientWidth || 0,
471
+ scrollWidth: Math.max(de?.scrollWidth || 0, document.body?.scrollWidth || 0),
472
+ scrollHeight: Math.max(de?.scrollHeight || 0, document.body?.scrollHeight || 0),
473
+ devicePixelRatio: window.devicePixelRatio,
474
+ theme: de?.getAttribute('data-theme') || null,
475
+ preset: de?.getAttribute('data-preset') || null,
476
+ lang: de?.getAttribute('lang') || null,
477
+ };
478
+ })
479
+ .catch(() => ({}));
480
+ return metrics;
481
+ }
482
+
483
+ /** Открыть target (файл/URL) с обработкой file:// и локального сервера. */
484
+ export async function gotoPage(page, url, { timeout = 30_000 } = {}) {
485
+ const response = await page.goto(url, { waitUntil: 'load', timeout });
486
+ await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {});
487
+ return response;
488
+ }
489
+
490
+ /** Разрешить цель: путь к html -> file URL (или http URL при --serve). */
491
+ export function resolveTarget(target, { serve = false, root = process.cwd() } = {}) {
492
+ if (/^(https?:|file:)/i.test(target)) {
493
+ return { source: target, url: target, abs: null, kind: 'url' };
494
+ }
495
+ if (/^data:/i.test(target)) {
496
+ return { source: target, url: target, abs: null, kind: 'data' };
497
+ }
498
+ const abs = path.resolve(root, target);
499
+ let stat;
500
+ try {
501
+ stat = fs.statSync(abs);
502
+ } catch {
503
+ throw new UsageError(`Файл не найден: ${abs}`);
504
+ }
505
+ if (stat.isDirectory()) throw new UsageError(`Это каталог, нужен HTML-файл: ${abs}`);
506
+ if (!serve) return { source: abs, url: pathToFileURL(abs).href, abs, kind: 'file' };
507
+ return { source: abs, url: null, abs, kind: 'file-serve' };
508
+ }
509
+
510
+ const MIME = {
511
+ '.html': 'text/html; charset=utf-8',
512
+ '.htm': 'text/html; charset=utf-8',
513
+ '.css': 'text/css; charset=utf-8',
514
+ '.js': 'text/javascript; charset=utf-8',
515
+ '.mjs': 'text/javascript; charset=utf-8',
516
+ '.json': 'application/json; charset=utf-8',
517
+ '.svg': 'image/svg+xml',
518
+ '.png': 'image/png',
519
+ '.jpg': 'image/jpeg',
520
+ '.jpeg': 'image/jpeg',
521
+ '.webp': 'image/webp',
522
+ '.gif': 'image/gif',
523
+ '.ico': 'image/x-icon',
524
+ '.woff2': 'font/woff2',
525
+ '.woff': 'font/woff',
526
+ '.ttf': 'font/ttf',
527
+ '.otf': 'font/otf',
528
+ '.txt': 'text/plain; charset=utf-8',
529
+ '.md': 'text/markdown; charset=utf-8',
530
+ '.map': 'application/json; charset=utf-8',
531
+ '.mp4': 'video/mp4',
532
+ '.webm': 'video/webm',
533
+ };
534
+
535
+ /**
536
+ * Локальный статический сервер — нужен страницам с <script type="module">,
537
+ * которые в Chrome не грузятся с file:// (CORS).
538
+ * Возвращает { origin, urlFor(absPath), close }.
539
+ */
540
+ export async function startStaticServer(rootDir) {
541
+ const root = path.resolve(rootDir);
542
+ const server = http.createServer((req, res) => {
543
+ try {
544
+ const pathname = decodeURIComponent(new URL(req.url, 'http://localhost').pathname);
545
+ // favicon.ico — не отдаём 404, иначе браузер пишет console.error, которого на странице нет
546
+ if (pathname === '/favicon.ico') {
547
+ res.writeHead(204).end();
548
+ return;
549
+ }
550
+ const filePath = path.resolve(root, '.' + pathname);
551
+ if (!filePath.startsWith(root)) {
552
+ res.writeHead(403).end('forbidden');
553
+ return;
554
+ }
555
+ const stat = fs.statSync(filePath);
556
+ const finalPath = stat.isDirectory() ? path.join(filePath, 'index.html') : filePath;
557
+ const body = fs.readFileSync(finalPath);
558
+ res.writeHead(200, {
559
+ 'Content-Type': MIME[path.extname(finalPath).toLowerCase()] || 'application/octet-stream',
560
+ 'Cache-Control': 'no-store',
561
+ });
562
+ res.end(body);
563
+ } catch {
564
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }).end('not found');
565
+ }
566
+ });
567
+ const port = await getFreePort();
568
+ await new Promise((resolve) => server.listen(port, '127.0.0.1', resolve));
569
+ const origin = `http://127.0.0.1:${port}`;
570
+ return {
571
+ origin,
572
+ urlFor(absPath) {
573
+ const rel = path.relative(root, path.resolve(absPath)).split(path.sep).join('/');
574
+ return `${origin}/${rel}`;
575
+ },
576
+ close() {
577
+ return new Promise((resolve) => server.close(() => resolve()));
578
+ },
579
+ };
580
+ }
581
+
582
+ /** Парсинг строки viewport: "desktop,tablet,390x844,1440x900". */
583
+ export function parseViewportList(value) {
584
+ const items = String(value || 'desktop')
585
+ .split(',')
586
+ .map((s) => s.trim())
587
+ .filter(Boolean);
588
+ const out = [];
589
+ for (const item of items) {
590
+ const named = VIEWPORTS[item.toLowerCase()];
591
+ if (named) {
592
+ out.push({ ...named });
593
+ continue;
594
+ }
595
+ const m = item.match(/^(\d{2,5})x(\d{2,5})$/i);
596
+ if (m) {
597
+ const width = Number(m[1]);
598
+ const height = Number(m[2]);
599
+ out.push({ id: `${width}x${height}`, width, height });
600
+ continue;
601
+ }
602
+ throw new UsageError(
603
+ `Неизвестный viewport «${item}». Доступно: ${Object.keys(VIEWPORTS).join(', ')} или <ширина>x<высота>`,
604
+ );
605
+ }
606
+ if (!out.length) throw new UsageError('Пустой список viewports');
607
+ return out;
608
+ }
609
+
610
+ /** Парсинг списка тем: "light,dark". */
611
+ export function parseThemeList(value) {
612
+ const items = String(value || 'light,dark')
613
+ .split(',')
614
+ .map((s) => s.trim().toLowerCase())
615
+ .filter(Boolean);
616
+ for (const t of items) {
617
+ if (!['light', 'dark'].includes(t)) throw new UsageError(`Неизвестная тема «${t}». Доступно: light, dark`);
618
+ }
619
+ return [...new Set(items)];
620
+ }