dsh-desktop-kit 2.2.1 → 2.7.2

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 (125) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +113 -27
  3. package/client.js +3121 -32
  4. package/docs/AGENT-HARNESS.md +144 -0
  5. package/docs/ARCHITECTURE.md +38 -7
  6. package/docs/COMPATIBILITY.md +4 -2
  7. package/docs/DEVELOPMENT.md +56 -0
  8. package/docs/INSTALL.md +20 -11
  9. package/docs/LAPTOP-INSTALL.md +96 -0
  10. package/docs/ROADMAP-3.0.md +353 -0
  11. package/docs/ROADMAP.md +1 -1
  12. package/docs/SUBAGENTS.md +25 -0
  13. package/docs/TEST-0.2.2.md +5 -5
  14. package/docs/TEST-2.3.0.md +49 -0
  15. package/docs/TEST-2.7.2.md +46 -0
  16. package/docs/TOKENS.md +2 -2
  17. package/docs/TROUBLESHOOTING.md +27 -0
  18. package/docs/UI-QA.md +28 -0
  19. package/docs/images/peak-toast-peak.png +0 -0
  20. package/docs/images/peak-toast-soon.png +0 -0
  21. package/docs/preview/peak-toast.html +155 -0
  22. package/index.js +18 -1
  23. package/package.json +26 -6
  24. package/plugins/dsh-agent-extensions/README.md +99 -0
  25. package/plugins/dsh-agent-extensions/index.js +167 -0
  26. package/plugins/dsh-agent-extensions/lib/agents.js +76 -0
  27. package/plugins/dsh-agent-extensions/lib/checkpoints.js +103 -0
  28. package/plugins/dsh-agent-extensions/lib/commands.js +255 -0
  29. package/plugins/dsh-agent-extensions/lib/hooks.js +182 -0
  30. package/plugins/dsh-agent-extensions/lib/policy.js +84 -0
  31. package/plugins/dsh-agent-extensions/lib/project.js +144 -0
  32. package/plugins/dsh-agent-extensions/lib/scaffold.js +151 -0
  33. package/plugins/dsh-agent-extensions/package.json +9 -0
  34. package/plugins/dsh-browser-qa/README.md +221 -0
  35. package/plugins/dsh-browser-qa/cordis.patch.yml +4 -0
  36. package/plugins/dsh-browser-qa/index.js +165 -0
  37. package/plugins/dsh-browser-qa/lib/cdp.js +224 -0
  38. package/plugins/dsh-browser-qa/lib/chrome.js +466 -0
  39. package/plugins/dsh-browser-qa/lib/pages.js +552 -0
  40. package/plugins/dsh-browser-qa/lib/qa.js +827 -0
  41. package/plugins/dsh-browser-qa/lib/ui-gate.js +256 -0
  42. package/plugins/dsh-browser-qa/package.json +31 -0
  43. package/plugins/dsh-checkpoints/README.md +22 -0
  44. package/plugins/dsh-checkpoints/cordis.patch.yml +3 -0
  45. package/plugins/dsh-checkpoints/index.js +80 -0
  46. package/plugins/dsh-checkpoints/lib/checkpoints.js +111 -0
  47. package/plugins/dsh-checkpoints/package.json +25 -0
  48. package/plugins/dsh-github-review/README.md +131 -0
  49. package/plugins/dsh-github-review/cordis.patch.yml +3 -0
  50. package/plugins/dsh-github-review/index.js +536 -0
  51. package/plugins/dsh-github-review/lib/gh.js +494 -0
  52. package/plugins/dsh-github-review/lib/normalize.js +662 -0
  53. package/plugins/dsh-github-review/package.json +28 -0
  54. package/plugins/dsh-guardrails/README.md +264 -0
  55. package/plugins/dsh-guardrails/cordis.patch.yml +5 -0
  56. package/plugins/dsh-guardrails/index.js +389 -0
  57. package/plugins/dsh-guardrails/lib/hooks.js +262 -0
  58. package/plugins/dsh-guardrails/lib/modes.js +77 -0
  59. package/plugins/dsh-guardrails/lib/policy.js +592 -0
  60. package/plugins/dsh-guardrails/lib/scan.js +451 -0
  61. package/plugins/dsh-guardrails/lib/secrets.js +274 -0
  62. package/plugins/dsh-guardrails/package.json +26 -0
  63. package/plugins/dsh-mcp-manager/README.md +78 -0
  64. package/plugins/dsh-mcp-manager/client.js +373 -0
  65. package/plugins/dsh-mcp-manager/index.js +159 -0
  66. package/plugins/dsh-mcp-manager/lib/api.js +250 -0
  67. package/plugins/dsh-mcp-manager/lib/client.js +219 -0
  68. package/plugins/dsh-mcp-manager/lib/config.js +217 -0
  69. package/plugins/dsh-mcp-manager/lib/manager.js +146 -0
  70. package/plugins/dsh-mcp-manager/package.json +18 -0
  71. package/plugins/dsh-mcp-manager/screenshots/modal-390.png +0 -0
  72. package/plugins/dsh-mcp-manager/screenshots/settings-1440.png +0 -0
  73. package/plugins/dsh-mcp-manager/screenshots/settings-390.png +0 -0
  74. package/plugins/dsh-memory-bridge/README.md +161 -0
  75. package/plugins/dsh-memory-bridge/client.js +2264 -0
  76. package/plugins/dsh-memory-bridge/cordis.patch.yml +5 -0
  77. package/plugins/dsh-memory-bridge/docs/images/conflicts.png +0 -0
  78. package/plugins/dsh-memory-bridge/docs/images/hero.png +0 -0
  79. package/plugins/dsh-memory-bridge/docs/images/settings-dark.png +0 -0
  80. package/plugins/dsh-memory-bridge/docs/images/settings-light.png +0 -0
  81. package/plugins/dsh-memory-bridge/docs/images/settings-mobile.png +0 -0
  82. package/plugins/dsh-memory-bridge/docs/images/setup.png +0 -0
  83. package/plugins/dsh-memory-bridge/icon.svg +13 -0
  84. package/plugins/dsh-memory-bridge/index.js +378 -0
  85. package/plugins/dsh-memory-bridge/lib/bridge.js +636 -0
  86. package/plugins/dsh-memory-bridge/lib/github.js +473 -0
  87. package/plugins/dsh-memory-bridge/lib/merge.js +205 -0
  88. package/plugins/dsh-memory-bridge/lib/portable.js +314 -0
  89. package/plugins/dsh-memory-bridge/lib/state.js +565 -0
  90. package/plugins/dsh-memory-bridge/lib/sync.js +984 -0
  91. package/plugins/dsh-memory-bridge/locale/en.json +6 -0
  92. package/plugins/dsh-memory-bridge/locale/ru.json +6 -0
  93. package/plugins/dsh-memory-bridge/package.json +67 -0
  94. package/plugins/dsh-peak-guard/README.md +16 -3
  95. package/plugins/dsh-peak-guard/client.js +223 -8
  96. package/plugins/dsh-peak-guard/package.json +1 -1
  97. package/plugins/dsh-plan-mode/README.md +24 -0
  98. package/plugins/dsh-plan-mode/cordis.patch.yml +3 -0
  99. package/plugins/dsh-plan-mode/index.js +103 -0
  100. package/plugins/dsh-plan-mode/lib/plan.js +89 -0
  101. package/plugins/dsh-plan-mode/package.json +18 -0
  102. package/plugins/dsh-profile-transfer/client.js +277 -17
  103. package/plugins/dsh-profile-transfer/index.js +10 -0
  104. package/plugins/dsh-profile-transfer/package.json +3 -1
  105. package/plugins/dsh-profile-transfer/transfer.js +101 -2
  106. package/plugins/dsh-project-memory/index.js +92 -2
  107. package/plugins/dsh-project-memory/package.json +1 -1
  108. package/plugins/dsh-repo-map/README.md +26 -0
  109. package/plugins/dsh-repo-map/cordis.patch.yml +3 -0
  110. package/plugins/dsh-repo-map/index.js +99 -0
  111. package/plugins/dsh-repo-map/lib/repo-map.js +170 -0
  112. package/plugins/dsh-repo-map/package.json +25 -0
  113. package/plugins/dsh-skill-autopilot/index.js +135 -11
  114. package/skills/delivery-gate/SKILL.md +31 -0
  115. package/skills/git-and-ci/SKILL.md +5 -1
  116. package/skills/manifest.json +31 -6
  117. package/skills/requirements-and-planning/SKILL.md +3 -0
  118. package/skills/research-first/SKILL.md +35 -0
  119. package/skills/research-first/references/query-patterns.md +20 -0
  120. package/skills/subagent-playbook/SKILL.md +36 -0
  121. package/skills/subagent-playbook/references/prompts.md +39 -0
  122. package/skills/ui-craft/SKILL.md +43 -0
  123. package/skills/visual-qa/SKILL.md +29 -0
  124. package/skills/visual-qa/references/commands.md +32 -0
  125. package/skills-baseline.json +219 -0
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Project discovery for agent extensions.
3
+ *
4
+ * Everything is optional and project-local: `.dsh/hooks.json`,
5
+ * `.dsh/agents/*.md`, `.dsh/commands/*.md`, `.dsh/verify.json`. A project
6
+ * without `.dsh` simply gets no extra hooks/commands/agents.
7
+ */
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+
11
+ export const BUILTIN_COMMANDS = new Set(["init", "doctor", "verify", "hooks", "checkpoint", "rewind", "project"]);
12
+
13
+ export function findDshDir(start) {
14
+ let current = path.resolve(start || process.cwd());
15
+ for (let depth = 0; depth < 8; depth += 1) {
16
+ const candidate = path.join(current, ".dsh");
17
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) return { root: current, dsh: candidate };
18
+ const parent = path.dirname(current);
19
+ if (parent === current) break;
20
+ current = parent;
21
+ }
22
+ return { root: path.resolve(start || process.cwd()), dsh: null };
23
+ }
24
+
25
+ export function projectFor(cwd) {
26
+ if (typeof cwd !== "string" || cwd.trim().length === 0) return { root: process.cwd(), dsh: null };
27
+ return findDshDir(cwd);
28
+ }
29
+
30
+ export function parseFrontmatter(text) {
31
+ const source = String(text ?? "").replace(/^\uFEFF/u, "");
32
+ const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/u.exec(source);
33
+ if (!match) return { meta: {}, body: source.trim() };
34
+ const meta = {};
35
+ for (const line of match[1].split(/\r?\n/u)) {
36
+ const pair = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/u.exec(line.trim());
37
+ if (!pair) continue;
38
+ let value = pair[2].trim();
39
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
40
+ value = value.slice(1, -1);
41
+ }
42
+ if (value === "true") value = true;
43
+ else if (value === "false") value = false;
44
+ meta[pair[1]] = value;
45
+ }
46
+ return { meta, body: source.slice(match[0].length).trim() };
47
+ }
48
+
49
+ function listMarkdown(directory) {
50
+ try {
51
+ return fs
52
+ .readdirSync(directory, { withFileTypes: true })
53
+ .filter((entry) => entry.isFile() && /\.md$/iu.test(entry.name))
54
+ .map((entry) => entry.name)
55
+ .sort((left, right) => left.localeCompare(right));
56
+ } catch {
57
+ return [];
58
+ }
59
+ }
60
+
61
+ export function loadAgents(projectDir, dshDir) {
62
+ const directory = dshDir ? path.join(dshDir, "agents") : null;
63
+ if (!directory || !fs.existsSync(directory)) return [];
64
+ const agents = [];
65
+ for (const name of listMarkdown(directory)) {
66
+ try {
67
+ const parsed = parseFrontmatter(fs.readFileSync(path.join(directory, name), "utf8"));
68
+ const id = String(parsed.meta.name || name.replace(/\.md$/iu, "")).trim();
69
+ if (!/^[a-z0-9][a-z0-9_-]{0,48}$/iu.test(id)) continue;
70
+ if (!parsed.body) continue;
71
+ agents.push({
72
+ name: id,
73
+ description: typeof parsed.meta.description === "string" ? parsed.meta.description : "",
74
+ model: typeof parsed.meta.model === "string" ? parsed.meta.model : "",
75
+ tools: typeof parsed.meta.tools === "string" ? parsed.meta.tools : "",
76
+ permission: typeof parsed.meta.permission === "string" ? parsed.meta.permission : "full",
77
+ body: parsed.body,
78
+ file: path.join(directory, name),
79
+ });
80
+ } catch {
81
+ /* unreadable agent */
82
+ }
83
+ }
84
+ return agents;
85
+ }
86
+
87
+ export function loadCommands(projectDir, dshDir) {
88
+ const directory = dshDir ? path.join(dshDir, "commands") : null;
89
+ if (!directory || !fs.existsSync(directory)) return [];
90
+ const commands = [];
91
+ for (const name of listMarkdown(directory)) {
92
+ const id = name.replace(/\.md$/iu, "").trim().toLowerCase();
93
+ if (!/^[a-z0-9][a-z0-9_-]{0,40}$/u.test(id) || BUILTIN_COMMANDS.has(id)) continue;
94
+ try {
95
+ const parsed = parseFrontmatter(fs.readFileSync(path.join(directory, name), "utf8"));
96
+ if (!parsed.body) continue;
97
+ commands.push({
98
+ name: id,
99
+ description: typeof parsed.meta.description === "string" ? parsed.meta.description : `Проектная команда /${id}`,
100
+ body: parsed.body,
101
+ file: path.join(directory, name),
102
+ });
103
+ } catch {
104
+ /* unreadable command */
105
+ }
106
+ }
107
+ return commands;
108
+ }
109
+
110
+ export function loadHooks(projectDir, dshDir) {
111
+ const file = dshDir ? path.join(dshDir, "hooks.json") : null;
112
+ if (!file || !fs.existsSync(file)) return { hooks: {}, trusted: false, file };
113
+ let parsed;
114
+ try {
115
+ parsed = JSON.parse(fs.readFileSync(file, "utf8"));
116
+ } catch {
117
+ return { hooks: {}, trusted: false, file, error: "hooks.json: invalid JSON" };
118
+ }
119
+ const hooks = parsed && typeof parsed.hooks === "object" && parsed.hooks !== null ? parsed.hooks : {};
120
+ const trusted = Boolean(dshDir && fs.existsSync(path.join(dshDir, "hooks.trusted")));
121
+ return { hooks, trusted, file };
122
+ }
123
+
124
+ export function loadVerify(projectDir, dshDir) {
125
+ const file = dshDir ? path.join(dshDir, "verify.json") : null;
126
+ if (file && fs.existsSync(file)) {
127
+ try {
128
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
129
+ if (Array.isArray(parsed?.commands)) {
130
+ return { commands: parsed.commands.filter((item) => typeof item === "string" && item.trim()).slice(0, 10), source: file };
131
+ }
132
+ } catch {
133
+ /* fall through */
134
+ }
135
+ }
136
+ return { commands: [], source: null };
137
+ }
138
+
139
+ export function projectSummary(projectDir) {
140
+ const agents = loadAgents(projectDir, path.join(projectDir, ".dsh"));
141
+ const commands = loadCommands(projectDir, path.join(projectDir, ".dsh"));
142
+ const hooks = loadHooks(projectDir, path.join(projectDir, ".dsh"));
143
+ return { agents, commands, hooks };
144
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Zero-config scaffolding for dsh-desktop-kit.
3
+ *
4
+ * Global: `~/.dsh/mcp.json` (empty, valid).
5
+ * Project: `<project>/.dsh/` with agents/, commands/, hooks.json, verify.json,
6
+ * mcp.json, PROJECT.md and rules/project.md.
7
+ *
8
+ * Trust markers are deliberately NOT created: hooks and project MCP servers
9
+ * must remain an explicit user decision.
10
+ */
11
+ import fs from "node:fs";
12
+ import os from "node:os";
13
+ import path from "node:path";
14
+
15
+ const SCAFFOLD_VERSION = 1;
16
+
17
+ function writeIfMissing(file, content) {
18
+ if (fs.existsSync(file)) return false;
19
+ fs.mkdirSync(path.dirname(file), { recursive: true });
20
+ fs.writeFileSync(file, content, "utf8");
21
+ return true;
22
+ }
23
+
24
+ export function ensureGlobalScaffold(home = process.env.DSH_HOME || path.join(os.homedir(), ".dsh")) {
25
+ const created = [];
26
+ const mcpFile = path.join(home, "mcp.json");
27
+ if (writeIfMissing(mcpFile, `${JSON.stringify({ mcpServers: {} }, null, 2)}\n`)) created.push(mcpFile);
28
+ const readme = path.join(home, "mcp.README.md");
29
+ if (
30
+ writeIfMissing(
31
+ readme,
32
+ [
33
+ "# MCP servers",
34
+ "",
35
+ "Add trusted servers to `mcp.json`:",
36
+ "",
37
+ "```json",
38
+ '{ "mcpServers": { "github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" }, "trusted": true } } }',
39
+ "```",
40
+ "",
41
+ "A server is started only with `trusted: true`. Project-level servers additionally require `.dsh/mcp.trusted` (`/mcp trust`).",
42
+ "",
43
+ ].join("\n"),
44
+ )
45
+ ) {
46
+ created.push(readme);
47
+ }
48
+ return { home, created };
49
+ }
50
+
51
+ function isKitRepo(cwd) {
52
+ try {
53
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8"));
54
+ return pkg?.name === "dsh-desktop-kit";
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ export function shouldScaffold(cwd) {
61
+ if (typeof cwd !== "string" || cwd.length === 0) return false;
62
+ let resolved;
63
+ try { resolved = fs.realpathSync(cwd); } catch { return false; }
64
+ if (!fs.statSync(resolved).isDirectory()) return false;
65
+ const home = path.resolve(os.homedir());
66
+ const root = path.parse(resolved).root;
67
+ if (resolved === home || resolved === root) return false;
68
+ if (resolved.startsWith(path.join(home, ".dsh"))) return false;
69
+ if (isKitRepo(resolved)) return false;
70
+ return true;
71
+ }
72
+
73
+ export function ensureProjectScaffold(cwd) {
74
+ if (!shouldScaffold(cwd)) return { skipped: true, created: [] };
75
+ const dsh = path.join(cwd, ".dsh");
76
+ const created = [];
77
+ fs.mkdirSync(dsh, { recursive: true });
78
+ for (const directory of ["agents", "commands", "rules"]) {
79
+ const target = path.join(dsh, directory);
80
+ if (!fs.existsSync(target)) {
81
+ fs.mkdirSync(target, { recursive: true });
82
+ created.push(target);
83
+ }
84
+ }
85
+ if (writeIfMissing(path.join(dsh, "agents", ".gitkeep"), "")) created.push(path.join(dsh, "agents", ".gitkeep"));
86
+ if (writeIfMissing(path.join(dsh, "commands", ".gitkeep"), "")) created.push(path.join(dsh, "commands", ".gitkeep"));
87
+ if (writeIfMissing(path.join(dsh, "hooks.json"), `${JSON.stringify({ hooks: {} }, null, 2)}\n`)) created.push(path.join(dsh, "hooks.json"));
88
+ if (writeIfMissing(path.join(dsh, "verify.json"), `${JSON.stringify({ commands: [] }, null, 2)}\n`)) created.push(path.join(dsh, "verify.json"));
89
+ if (writeIfMissing(path.join(dsh, "mcp.json"), `${JSON.stringify({ mcpServers: {} }, null, 2)}\n`)) created.push(path.join(dsh, "mcp.json"));
90
+ if (
91
+ writeIfMissing(
92
+ path.join(dsh, ".gitignore"),
93
+ ["hooks.trusted", "mcp.trusted", "checkpoints/", ".kit-scaffold.json", ""].join("\n"),
94
+ )
95
+ ) {
96
+ created.push(path.join(dsh, ".gitignore"));
97
+ }
98
+ if (
99
+ writeIfMissing(
100
+ path.join(dsh, "README.md"),
101
+ [
102
+ "# .dsh",
103
+ "",
104
+ "Project-local DSH extensions, created automatically by dsh-desktop-kit.",
105
+ "",
106
+ "- `agents/*.md` — custom subagents (`project_agent`).",
107
+ "- `commands/*.md` — project commands (`/project <name>`).",
108
+ "- `hooks.json` — lifecycle hooks; run `/hooks trust` after review.",
109
+ "- `verify.json` — commands for `/verify`.",
110
+ "- `mcp.json` — project MCP servers; run `/mcp trust` after review.",
111
+ "- `PROJECT.md` — project context; `/init --force` regenerates it.",
112
+ "",
113
+ ].join("\n"),
114
+ )
115
+ ) {
116
+ created.push(path.join(dsh, "README.md"));
117
+ }
118
+ const projectFile = path.join(dsh, "PROJECT.md");
119
+ if (!fs.existsSync(projectFile)) {
120
+ let pkg = {};
121
+ try { pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8")); } catch { /* optional */ }
122
+ fs.writeFileSync(
123
+ projectFile,
124
+ [
125
+ "# Project",
126
+ "",
127
+ `Root: ${cwd}`,
128
+ pkg.name ? `Package: ${pkg.name}${pkg.version ? ` ${pkg.version}` : ""}` : "",
129
+ pkg.scripts?.test ? `Tests: ${pkg.scripts.test}` : "",
130
+ "",
131
+ "Created automatically by dsh-desktop-kit. Edit freely; `/init --force` regenerates.",
132
+ "",
133
+ ]
134
+ .filter(Boolean)
135
+ .join("\n"),
136
+ "utf8",
137
+ );
138
+ created.push(projectFile);
139
+ }
140
+ const ruleFile = path.join(dsh, "rules", "project.md");
141
+ if (!fs.existsSync(ruleFile)) {
142
+ fs.writeFileSync(ruleFile, "---\nalways: true\n---\n# Правила проекта\n\n- Источник истины: `.dsh/PROJECT.md`.\n- Перед сдачей — тесты и проверка UI.\n", "utf8");
143
+ created.push(ruleFile);
144
+ }
145
+ const marker = path.join(dsh, ".kit-scaffold.json");
146
+ if (!fs.existsSync(marker)) {
147
+ fs.writeFileSync(marker, `${JSON.stringify({ version: SCAFFOLD_VERSION, createdAt: new Date().toISOString() }, null, 2)}\n`, "utf8");
148
+ created.push(marker);
149
+ }
150
+ return { skipped: false, created, dsh };
151
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "dsh-agent-extensions",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "description": "Project-local hooks, custom agents, commands and safe checkpoints for DeepSeek Harness — Claude Code-like extensibility.",
8
+ "license": "MIT"
9
+ }
@@ -0,0 +1,221 @@
1
+ # dsh-browser-qa
2
+
3
+ Headless-проверка вёрстки для DSH-агента: скриншоты и адаптивный аудит
4
+ горизонтального переполнения и ошибок консоли через протокол Chrome DevTools.
5
+ Без Playwright и без внешних зависимостей — только встроенные модули Node.js.
6
+
7
+ ## Зачем
8
+
9
+ Агент может сам проверить страницу перед сдачей: «нет ли горизонтального
10
+ скролла на 390/768/1440», «какие элементы вылезают», «что пишет консоль»,
11
+ «как это выглядит». Работает и с локальными HTML-файлами, и с http(s)-страницами.
12
+
13
+ ## Требования
14
+
15
+ - **Chrome или Chromium-браузер**. На Windows плагин ищет:
16
+ `CHROME_PATH` → `%ProgramFiles%\Google\Chrome\Application\chrome.exe` →
17
+ `%ProgramFiles(x86)%\...` → `%LOCALAPPDATA%\Google\Chrome\Application\chrome.exe` →
18
+ Chrome Canary → Edge (`msedge.exe`) как Chromium-запасной вариант.
19
+ Путь можно задать явно: `CHROME_PATH=C:\путь\chrome.exe` (переменная окружения
20
+ процесса DSH).
21
+ - **Node 22+** — нужен встроенный `WebSocket` (в Node 24 он стабилен). DSH
22
+ Desktop на Electron 44 запускает Node 24, отдельная установка Node не нужна.
23
+ Если WebSocket недоступен, `browser_qa action=status` честно сообщит об этом.
24
+ - Работает и на macOS/Linux (стандартные пути Chrome/Chromium), но основные
25
+ пути и проверки — под Windows.
26
+
27
+ ## Инструмент `browser_qa`
28
+
29
+ Регистрируется один инструмент с тремя действиями. Параметры:
30
+
31
+ | Параметр | Тип | По умолчанию | Назначение |
32
+ |---|---|---|---|
33
+ | `action` | `"shot"` \| `"audit"` \| `"status"` | — (обязателен) | Что сделать |
34
+ | `url` | string | — | `http(s)`-адрес, `file://` или путь к локальному HTML (относительный — от cwd сессии) |
35
+ | `width` | integer | `1440` | Ширина viewport для `shot`, CSS px (200..3840) |
36
+ | `height` | integer | `900` | Высота viewport, CSS px (200..2160) |
37
+ | `widths` | integer[] | `[390, 768, 1440]` | Ширины для `audit`, CSS px (до 6 значений) |
38
+ | `outDir` | string | `<cwd>/.dsh/browser-qa` | Куда писать PNG; для `audit` — включает скриншоты |
39
+ | `fullPage` | boolean | `false` | Снимать всю страницу, а не viewport (высота обрезается на 16384 px) |
40
+ | `timeoutMs` | integer | `30000` | Таймаут загрузки страницы на одну ширину (3000..120000) |
41
+
42
+ ### `status`
43
+
44
+ Находит браузер и возвращает путь, тип (Chrome/Edge), версию (через
45
+ `/json/version`), версию Node и наличие WebSocket. Ничего не сохраняет.
46
+ Пробный запуск Chrome занимает ~1–2 с и кэшируется на 5 минут.
47
+
48
+ ### `shot`
49
+
50
+ Открывает страницу, выставляет viewport, сохраняет PNG и возвращает
51
+ `output.path`, `output.width`, `output.height`, `output.bytes`, а также
52
+ краткую сводку overflow и ошибок консоли. По умолчанию PNG кладётся в
53
+ `<cwd>/.dsh/browser-qa/`.
54
+
55
+ ### `audit`
56
+
57
+ Для каждой ширины из `widths` заново открывает страницу (так корректно
58
+ срабатывают media queries и JS-перестройка), затем измеряет:
59
+
60
+ - `document.documentElement.scrollWidth` против `clientWidth` (допуск 1 px) —
61
+ `horizontalOverflow.hasOverflow`, `overflowPx`;
62
+ - элементы, чей `getBoundingClientRect().right` выходит за viewport
63
+ (`overflowingElements`: селектор, тег, id, классы, rect, насколько вылез,
64
+ текст); максимум 25 в отчёте, всего просмотрено до 5000 элементов;
65
+ - ошибки консоли: `Runtime.consoleAPICalled` с `type=error/assert`,
66
+ `Runtime.exceptionThrown` и `Log.entryAdded` уровня `error` (включая
67
+ CSP-нарушения и сетевые ошибки); дедупликация, максимум 30;
68
+ - **разметка и доступность** (`markup`): дубли `id`, `<img>` без `alt`,
69
+ поля без `label`/`aria-*`, ссылки и кнопки без доступного имени,
70
+ пропуски уровней заголовков, несколько `h1`, отсутствие `lang`/`title`/
71
+ `meta viewport`, положительный `tabindex`; до 60 issues на ширину;
72
+ - дополнительно: viewport, title, финальный URL, `loadTimedOut`, время загрузки.
73
+
74
+ Сводка: `summary.widthsChecked`, `widthsWithOverflow`, `overflowingElements`,
75
+ `consoleErrors`, `markupIssues`, `widthsWithMarkupIssues`, `failedWidths`,
76
+ `screenshots`.
77
+
78
+ **Скриншоты в audit — опционально:** они сохраняются по одной штуке на ширину
79
+ только если передан `outDir`. Без `outDir` audit работает быстрее и не пишет
80
+ файлов. `fullPage` действует и на audit, и на shot.
81
+
82
+ ## UI gate
83
+
84
+ Плагин также ставит жёсткий gate на хуках:
85
+
86
+ - `fs/write-intent` / `fs/edit-intent` запоминают правки `.html`, `.css`,
87
+ `.scss`, `.jsx`, `.tsx`, `.vue`, `.svelte`, `.astro`, `.mdx`;
88
+ - `tools/result` читает итог `browser_qa audit` (`overflow`, console, markup);
89
+ - на `agent/turn-stopping` грязная сессия получает `agent.steer(...)`:
90
+ «запусти `browser_qa audit` на 390/768/1440, исправь overflow/console/markup
91
+ и проверь снова»;
92
+ - ход закрывается только после **чистого** audit (0 overflow, 0 console,
93
+ 0 markup) или после 4 напоминаний — предохранитель от бесконечного цикла.
94
+
95
+ Это не запрет записи: агент может править, но не может молча завершить ход
96
+ после UI-правки без проверки.
97
+
98
+ ## Примеры вызовов
99
+
100
+ ```json
101
+ { "action": "status" }
102
+ ```
103
+
104
+ ```json
105
+ { "action": "shot", "url": "C:\\project\\dist\\index.html", "width": 390, "height": 844 }
106
+ ```
107
+
108
+ ```json
109
+ {
110
+ "action": "audit",
111
+ "url": "https://example.com/pricing",
112
+ "widths": [390, 768, 1440],
113
+ "outDir": "qa/browser",
114
+ "timeoutMs": 45000
115
+ }
116
+ ```
117
+
118
+ ```json
119
+ { "action": "audit", "url": "C:\\project\\index.html", "widths": [390, 1440], "fullPage": true, "outDir": "qa/browser" }
120
+ ```
121
+
122
+ ## Куда сохраняются PNG
123
+
124
+ - `shot` без `outDir`: `<cwd-сессии>/.dsh/browser-qa/`.
125
+ - `audit` без `outDir`: ничего не сохраняется (только отчёт).
126
+ - `audit` с `outDir` (или `shot` с `outDir`): указанный каталог, абсолютный или
127
+ относительно cwd сессии; создаётся при необходимости.
128
+ - Имя файла: `<slug>-<ширина>x<высота>[-full]-<YYYYMMDD-HHmmss>-<hex>.png`,
129
+ например `localhost-390x844-20261109-205501-a1b2c3.png`. `slug` берётся из
130
+ хоста и пути URL или из имени локального файла.
131
+ - Размеры PNG — ровно CSS-пиксели viewport (`deviceScaleFactor=1`), поэтому
132
+ 390-px скриншот получается 390 px шириной (а не 500, как при
133
+ `chrome --screenshot` на Windows).
134
+
135
+ ## Ограничения
136
+
137
+ - **CSP.** Плагин не обходит Content-Security-Policy страницы: если CSP
138
+ запрещает собственные inline-скрипты/шрифты/картинки сайта, в отчёте будут
139
+ соответствующие console errors. Сам замер выполняется через
140
+ `Runtime.evaluate` DevTools и CSP страницы не блокируется.
141
+ - **Авторизация.** Профиль браузера временный, cookies нет. Приватные
142
+ страницы, за логином или с SSO, вернут редирект на страницу входа — это
143
+ ожидаемо. Для проверки таких страниц нужен публичный staging/preview URL.
144
+ - **`file://`.** Локальные HTML открываются (флаг
145
+ `--allow-file-access-from-files` включён), но Chrome ограничивает часть
146
+ возможностей file-страниц: ES-модули и `fetch` к локальным файлам могут
147
+ блокироваться CORS, часть API недоступна. Относительные `src`/`href`
148
+ обычно работают — `file://`, `../../`, внешние CDN подключаются.
149
+ - **SPA и асинхронность.** Плагин ждёт `load`, `document.readyState=complete`
150
+ и шрифты (`document.fonts.ready`, не дольше ~1.75 с), затем снимает замер.
151
+ Бесконечные таймеры, ленивые секции и контент, появляющийся через 5+ секунд
152
+ после load, могут не попасть в кадр — это не полноценный E2E-раннер.
153
+ - **Интерактивность.** Нет кликов, скроллинга, ввода и навигации по шагам.
154
+ Только загрузка страницы и измерение.
155
+ - **Псевдоэлементы, `transform`, `position: fixed`.** Overflow считается по
156
+ реальным DOM-элементам; `::before/::after` не перечисляются, а элементы
157
+ внутри контейнеров с `overflow-x != visible` (карусели, таблицы со скроллом)
158
+ считаются «содержимыми» и в список вылезающих не попадают.
159
+ - **`fullPage`.** Ограничен 16384 px по высоте и ширине (лимит поверхности
160
+ Chrome); при превышении `output.clipped=true`. Full-page снимок включает и
161
+ горизонтально переполненную область, поэтому может быть шире viewport.
162
+ - **Антибот/headless-детект.** Некоторые сайты (Cloudflare и т.п.) отдают
163
+ headless-браузеру заглушку или блокируют его — аудит покажет то, что увидел.
164
+ - **Windows.** Определение путей и очистка процессов рассчитаны в первую
165
+ очередь на Windows; на Linux/macOS поддерживаются базовые пути, но тесты
166
+ прогонялись на Windows.
167
+
168
+ ## Безопасность и уборка
169
+
170
+ - Каждый вызов запускает отдельный Chrome с временным `--user-data-dir` и
171
+ случайным `--remote-debugging-port=0`; порт слушает только `127.0.0.1` и
172
+ живёт внутри одного вызова.
173
+ - Профиль пользователя не затрагивается; cookies, история и расширения из
174
+ обычного Chrome не используются.
175
+ - После каждого вызова (включая ошибку и отмену) браузер убивается
176
+ (`taskkill /T /F` при необходимости), временный профиль удаляется с
177
+ ретраями; на случай аварийного завершения DSH есть `exit`-guard.
178
+ - Внешних сетевых запросов, кроме самой проверяемой страницы, нет; телеметрии
179
+ нет.
180
+
181
+ ## Тесты
182
+
183
+ ```bash
184
+ node tools/browser-qa-tests.mjs
185
+ ```
186
+
187
+ - Чистые хелперы (всегда): параметры инструмента, viewport/widths/timeouts,
188
+ нормализация URL и путей, имена файлов, PNG-заголовок, shaping overflow- и
189
+ console-отчётов, summary и текст.
190
+ - Реальный Chrome smoke (если Chrome/Edge найден; иначе SKIP):
191
+ временный HTML с намеренным overflow 1200 px и `console.error`; проверяются
192
+ overflow на 390/768, отсутствие overflow на 1440, найденный элемент
193
+ `#wide`, пойманная ошибка консоли, PNG на каждую ширину, точные размеры
194
+ `shot`, fullPage выше viewport, `status` с путём/версией, вызов через
195
+ зарегистрированный инструмент и отсутствие оставшихся временных профилей.
196
+
197
+ ## Устройство
198
+
199
+ ```
200
+ plugins/dsh-browser-qa/
201
+ index.js — регистрация инструмента browser_qa (inject: ["tools"])
202
+ lib/qa.js — чистые хелперы: URL/viewport/widths, overflow-probe,
203
+ console-события, отчёты, текст
204
+ lib/cdp.js — минимальный CDP-клиент на встроенном WebSocket
205
+ lib/chrome.js — поиск Chrome, запуск headless с temp-профилем,
206
+ /json/version|list, очистка процессов и профиля
207
+ lib/pages.js — shot/audit/status: навигация, viewport, скриншоты, сборка отчёта
208
+ cordis.patch.yml — host-патч для standalone-установки bundle
209
+ ```
210
+
211
+ ## Диагностика
212
+
213
+ - `Chrome/Edge not found` — установите Chrome или задайте `CHROME_PATH`
214
+ (полный путь к `chrome.exe`); `status` покажет список проверенных путей.
215
+ - `has no built-in WebSocket` — Node в рантайме DSH старше 22; нужен Node 22+
216
+ (в актуальном DSH Desktop уже Node 24).
217
+ - Таймауты на медленных страницах — увеличьте `timeoutMs` (до 120000).
218
+ - `loadTimedOut: true` в отчёте означает, что `load` не успел за таймаут, но
219
+ замер всё равно сделан по текущему DOM.
220
+ - Если временный профиль не удалился (`warnings` в ответе), повторите позже:
221
+ обычно антивирус/индексатор Windows держит файл не больше секунды.
@@ -0,0 +1,4 @@
1
+ # Host half of the browser-QA plugin.
2
+ - insert:
3
+ - id: dsh-browser-qa
4
+ name: dsh-browser-qa