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,89 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ const PLAN_FILE = ".dsh/PLAN.md";
5
+
6
+ export function planPath(cwd) {
7
+ return path.join(cwd, PLAN_FILE);
8
+ }
9
+
10
+ export function parsePlan(text) {
11
+ const match = String(text ?? "").match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/u);
12
+ const front = match ? match[1] : "";
13
+ const body = (match ? String(text).slice(match[0].length) : String(text ?? "")).trim();
14
+ const field = (name) => {
15
+ const result = front.match(new RegExp(`^${name}:\\s*(.+)$`, "mu"));
16
+ return result ? result[1].trim().replace(/^"|"$/gu, "") : "";
17
+ };
18
+ const status = field("status") === "approved" ? "approved" : "draft";
19
+ return {
20
+ status,
21
+ createdAt: field("createdAt") || null,
22
+ updatedAt: field("updatedAt") || null,
23
+ approvedAt: field("approvedAt") || null,
24
+ body,
25
+ };
26
+ }
27
+
28
+ export function readPlan(cwd) {
29
+ const file = planPath(cwd);
30
+ try {
31
+ const text = fs.readFileSync(file, "utf8");
32
+ return { exists: true, path: file, ...parsePlan(text) };
33
+ } catch {
34
+ return { exists: false, path: file, status: "draft", body: "" };
35
+ }
36
+ }
37
+
38
+ function serialize(plan) {
39
+ return [
40
+ "---",
41
+ `status: ${plan.status}`,
42
+ `createdAt: ${plan.createdAt ?? new Date().toISOString()}`,
43
+ `updatedAt: ${new Date().toISOString()}`,
44
+ `approvedAt: ${plan.approvedAt ?? ""}`,
45
+ "---",
46
+ "",
47
+ plan.body.trim(),
48
+ "",
49
+ ].join("\n");
50
+ }
51
+
52
+ export function writePlan(cwd, body, { status = "draft", approvedAt = null, force = false } = {}) {
53
+ const file = planPath(cwd);
54
+ if (fs.existsSync(file) && !force) {
55
+ throw new Error("PLAN.md уже существует; используйте update или force");
56
+ }
57
+ const now = new Date().toISOString();
58
+ fs.mkdirSync(path.dirname(file), { recursive: true });
59
+ const plan = { status, createdAt: now, updatedAt: now, approvedAt, body: String(body ?? "").trim() };
60
+ fs.writeFileSync(file, serialize(plan), "utf8");
61
+ return { path: file, ...plan };
62
+ }
63
+
64
+ export function updatePlan(cwd, body) {
65
+ const current = readPlan(cwd);
66
+ return writePlan(cwd, body, {
67
+ status: current.status,
68
+ approvedAt: current.approvedAt,
69
+ force: true,
70
+ });
71
+ }
72
+
73
+ /**
74
+ * Approve only with explicit confirmation from the caller. The tool layer must
75
+ * pass `confirm: true` (the model is instructed to ask the user first).
76
+ */
77
+ export function approvePlan(cwd, { confirm = false } = {}) {
78
+ if (confirm !== true) throw new Error("approve требует confirm: true после подтверждения пользователем");
79
+ const current = readPlan(cwd);
80
+ if (!current.exists) throw new Error("PLAN.md не найден");
81
+ const plan = writePlan(cwd, current.body, { status: "approved", approvedAt: new Date().toISOString(), force: true });
82
+ return plan;
83
+ }
84
+
85
+ export function planSummary(plan, maxChars = 1200) {
86
+ if (!plan.exists) return "";
87
+ const body = plan.body.length <= maxChars ? plan.body : `${plan.body.slice(0, maxChars)}\n…`;
88
+ return body;
89
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "dsh-plan-mode",
3
+ "version": "1.0.0",
4
+ "description": "PLAN.md workflow for DSH: create/read/update a project plan and block code changes until the user explicitly approves it.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "index.js",
8
+ "exports": {
9
+ ".": "./index.js",
10
+ "./lib": "./lib/plan.js",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "files": ["index.js", "lib/**", "cordis.patch.yml", "README.md"],
14
+ "dsh": {
15
+ "manifestVersion": 1,
16
+ "bundle": { "patch": "./cordis.patch.yml" }
17
+ }
18
+ }
@@ -79,8 +79,17 @@ window.__ModuleLoader__.load({
79
79
  groupSkillsDesc: "All file-based skills (without node_modules)",
80
80
  groupMemory: "Memory",
81
81
  groupMemoryDesc: "memory/ — session log, project dossiers, environment facts",
82
- groupProjectsMemory: "Project memory",
83
- groupProjectsMemoryDesc: "projects-memory/ — dossiers and journals",
82
+ groupProjectsMemory: "Project memory (full backup)",
83
+ groupProjectsMemoryDesc: "projects-memory/ — dossiers and journals. For a USB drive or GitHub use Settings → Project memory; this item is only part of a full profile backup.",
84
+ modeTitle: "Transfer mode",
85
+ modeFull: "Profile and environment",
86
+ modeFullDesc: "Settings, plugins, skills and workspaces; project memory is separate",
87
+ modeProject: "Projects and code",
88
+ modeProjectDesc: "Source folders and project memory; settings stay untouched",
89
+ projectExportTitle: "Save projects and code",
90
+ projectExportSub: "Workspaces + source folders. Project memory is transferred separately in Project memory → USB.",
91
+ projectExportButton: "Save to USB",
92
+ projectModeHint: "Project memory dossiers live in Settings → Project memory → USB. This mode carries code and workspace folders.",
84
93
  groupConfig: "Global configs",
85
94
  groupConfigDesc: "AGENTS.md, dream-skin.json",
86
95
  groupSessions: "Session history",
@@ -96,6 +105,18 @@ window.__ModuleLoader__.load({
96
105
  noteLabel: "Note",
97
106
  copiedPath: "Path copied",
98
107
  archivePathHint: "Use the path shown after export, or type the archive path from a USB drive / cloud folder.",
108
+ usbTitle: "USB: projects and code",
109
+ usbHint: "Save workspaces and source folders to a USB drive, then find the archive on the other machine.",
110
+ usbPreset: "Select projects + code",
111
+ usbPresetHint: "Workspaces, workspace folders and project memory; profile settings are left out.",
112
+ usbPathLabel: "USB drive path",
113
+ usbPathPlaceholder: "E:\\",
114
+ usbScan: "Find archives",
115
+ usbScanning: "Scanning…",
116
+ usbNoArchives: "No dsh-profile-*.tar.gz archives found on this drive.",
117
+ usbFound: "{count} archive(s) found",
118
+ usbPick: "Check",
119
+ usbArchiveMeta: "{size} · {date}",
99
120
  };
100
121
  const RU = {
101
122
  nav: "Перенос и резервные копии",
@@ -156,8 +177,17 @@ window.__ModuleLoader__.load({
156
177
  groupSkillsDesc: "Все файловые навыки (без node_modules)",
157
178
  groupMemory: "Память",
158
179
  groupMemoryDesc: "memory/ — журнал сессий, досье проектов, факты окружения",
159
- groupProjectsMemory: "Память проектов",
160
- groupProjectsMemoryDesc: "projects-memory/ — досье и журналы",
180
+ groupProjectsMemory: "Память проектов (полный бэкап)",
181
+ groupProjectsMemoryDesc: "projects-memory/ — досье и журналы. Для флешки и GitHub используйте «Память проектов»; этот пункт нужен только для полного бэкапа профиля.",
182
+ modeTitle: "Режим переноса",
183
+ modeFull: "Профиль и окружение",
184
+ modeFullDesc: "Настройки, плагины, навыки и рабочие области; память проектов — отдельно",
185
+ modeProject: "Проекты и код",
186
+ modeProjectDesc: "Папки с исходниками и память проектов; настройки не трогаем",
187
+ projectExportTitle: "Сохранить проекты и код",
188
+ projectExportSub: "Рабочие области + папки с исходниками. Память проектов переносится отдельно, в «Память проектов → Флешка».",
189
+ projectExportButton: "Сохранить на флешку",
190
+ projectModeHint: "Досье памяти проектов — в «Память проектов → Флешка». Здесь — код и рабочие папки.",
161
191
  groupConfig: "Глобальные конфиги",
162
192
  groupConfigDesc: "AGENTS.md, dream-skin.json",
163
193
  groupSessions: "История сессий",
@@ -173,6 +203,18 @@ window.__ModuleLoader__.load({
173
203
  noteLabel: "Примечание",
174
204
  copiedPath: "Путь скопирован",
175
205
  archivePathHint: "Используйте путь из результата экспорта или впишите путь к архиву с флешки / из облака.",
206
+ usbTitle: "Флешка: проекты и код",
207
+ usbHint: "Сохраните рабочие области и папки с исходниками на флешку, а на другой машине найдите архив и восстановите.",
208
+ usbPreset: "Выбрать «Проекты и код»",
209
+ usbPresetHint: "Рабочие области, папки с кодом и память проектов; настройки профиля не затрагиваются.",
210
+ usbPathLabel: "Путь к флешке",
211
+ usbPathPlaceholder: "E:\\",
212
+ usbScan: "Найти архивы",
213
+ usbScanning: "Сканирую…",
214
+ usbNoArchives: "На флешке не найдено архивов dsh-profile-*.tar.gz.",
215
+ usbFound: "Найдено архивов: {count}",
216
+ usbPick: "Проверить",
217
+ usbArchiveMeta: "{size} · {date}",
176
218
  };
177
219
  const ZH = {
178
220
  nav: "迁移与备份",
@@ -233,8 +275,17 @@ window.__ModuleLoader__.load({
233
275
  groupSkillsDesc: "全部文件型技能(不含 node_modules)",
234
276
  groupMemory: "记忆",
235
277
  groupMemoryDesc: "memory/ — 会话日志、项目档案、环境事实",
236
- groupProjectsMemory: "项目记忆",
237
- groupProjectsMemoryDesc: "projects-memory/ — 档案与日志",
278
+ groupProjectsMemory: "项目记忆(完整备份)",
279
+ groupProjectsMemoryDesc: "projects-memory/ — 档案与日志。U 盘或 GitHub 请使用「项目记忆」;此项仅用于完整配置备份。",
280
+ modeTitle: "传输模式",
281
+ modeFull: "配置与环境",
282
+ modeFullDesc: "设置、插件、技能和工作区;项目记忆单独传输",
283
+ modeProject: "项目与代码",
284
+ modeProjectDesc: "源码文件夹和项目记忆;不修改设置",
285
+ projectExportTitle: "保存项目与代码",
286
+ projectExportSub: "工作区 + 源码文件夹。项目记忆在「项目记忆 → U 盘」中单独传输。",
287
+ projectExportButton: "保存到 U 盘",
288
+ projectModeHint: "项目记忆档案在「项目记忆 → U 盘」。此处传输代码和工作区文件夹。",
238
289
  groupConfig: "全局配置",
239
290
  groupConfigDesc: "AGENTS.md、dream-skin.json",
240
291
  groupSessions: "会话历史",
@@ -250,6 +301,18 @@ window.__ModuleLoader__.load({
250
301
  noteLabel: "说明",
251
302
  copiedPath: "路径已复制",
252
303
  archivePathHint: "使用导出结果中的路径,或输入 U 盘 / 云盘中的归档路径。",
304
+ usbTitle: "U 盘:项目与代码",
305
+ usbHint: "将工作区和源码文件夹保存到 U 盘,在另一台机器上找到归档并恢复。",
306
+ usbPreset: "选择「项目 + 代码」",
307
+ usbPresetHint: "工作区、工作区文件夹和项目记忆;不包含配置设置。",
308
+ usbPathLabel: "U 盘路径",
309
+ usbPathPlaceholder: "E:\\",
310
+ usbScan: "查找归档",
311
+ usbScanning: "扫描中…",
312
+ usbNoArchives: "U 盘上没有 dsh-profile-*.tar.gz 归档。",
313
+ usbFound: "找到 {count} 个归档",
314
+ usbPick: "检查",
315
+ usbArchiveMeta: "{size} · {date}",
253
316
  };
254
317
 
255
318
  const inject = ["slots", "locale", "connection"];
@@ -316,9 +379,34 @@ window.__ModuleLoader__.load({
316
379
  .dpt-step-num{font-variant-numeric:tabular-nums;font-size:11px;font-weight:700;letter-spacing:.08em;color:var(--dsw-alias-brand-primary,#2f7df6)}
317
380
  .dpt-step-label{font-size:12px;font-weight:600;letter-spacing:.04em;color:var(--dsw-alias-label-secondary,#5b6472);text-transform:uppercase}
318
381
  .dpt-card{display:flex;flex-direction:column;gap:14px;padding:18px 18px 16px;border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.1));border-radius:14px;background:var(--dsw-alias-bg-layer-1,rgba(0,0,0,.02))}
319
- .dpt-card-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}
382
+ .dpt-card-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;flex-wrap:wrap}
320
383
  .dpt-card-title{font-size:15px;line-height:20px;font-weight:650;color:var(--dsw-alias-label-primary,#111)}
321
384
  .dpt-card-sub{font-size:12.5px;line-height:18px;color:var(--dsw-alias-label-secondary,#5b6472);margin-top:3px;max-width:62ch}
385
+ .dpt-modes{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:2px 0 4px}
386
+ .dpt-mode{display:flex;align-items:flex-start;gap:10px;text-align:left;padding:11px 12px;border-radius:12px;border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.1));background:var(--dsw-alias-bg-base,#fff);color:var(--dsw-alias-label-primary,#111);cursor:pointer;font:inherit;transition:border-color .16s ease,background .16s ease,box-shadow .16s ease}
387
+ .dpt-mode:hover{border-color:color-mix(in srgb,var(--dsw-alias-brand-primary,#2f7df6) 35%,transparent)}
388
+ .dpt-mode:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2f7df6);outline-offset:2px}
389
+ .dpt-mode[data-active="true"]{border-color:color-mix(in srgb,var(--dsw-alias-brand-primary,#2f7df6) 48%,transparent);background:color-mix(in srgb,var(--dsw-alias-brand-primary,#2f7df6) 6%,var(--dsw-alias-bg-base,#fff));box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dsw-alias-brand-primary,#2f7df6) 14%,transparent)}
390
+ .dpt-mode-icon{flex:none;width:30px;height:30px;border-radius:9px;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--dsw-alias-label-primary,#111) 6%,transparent);color:var(--dsw-alias-label-secondary,#5b6472)}
391
+ .dpt-mode[data-active="true"] .dpt-mode-icon{background:color-mix(in srgb,var(--dsw-alias-brand-primary,#2f7df6) 13%,transparent);color:var(--dsw-alias-brand-primary,#2f7df6)}
392
+ .dpt-mode-copy{display:flex;flex-direction:column;gap:2px;min-width:0}
393
+ .dpt-mode-title{font-size:12.5px;line-height:17px;font-weight:650}
394
+ .dpt-mode-desc{font-size:10.5px;line-height:14px;color:var(--dsw-alias-label-secondary,#6b7280)}
395
+ @media (max-width:640px){.dpt-modes{grid-template-columns:1fr}}
396
+ .dpt-kv{flex-wrap:wrap;min-width:0}
397
+ .dpt-kv span{min-width:0}
398
+ .dpt-mono{overflow-wrap:anywhere}
399
+ .dpt-row-inline{min-width:0}
400
+ @media (max-width:560px){
401
+ .dpt-kv{flex-direction:column;align-items:flex-start;gap:3px}
402
+ .dpt-kv span:last-child{text-align:left}
403
+ .dpt-row-inline{width:100%;justify-content:flex-start}
404
+ .dpt-card{padding:14px 13px 13px}
405
+ .dpt-input,.dpt-btn{max-width:100%}
406
+ .dpt-row{flex-wrap:wrap}
407
+ .dpt-row-text{flex:1 1 calc(100% - 27px);min-width:0}
408
+ .dpt-row-size{width:100%;padding-left:27px;text-align:left;white-space:normal}
409
+ }
322
410
  .dpt-chip{flex:none;font-size:11px;font-weight:600;letter-spacing:.04em;padding:3px 8px;border-radius:999px;background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,.06));color:var(--dsw-alias-label-secondary,#4b5563);font-variant-numeric:tabular-nums;white-space:nowrap}
323
411
  .dpt-list{display:flex;flex-direction:column;border-top:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07))}
324
412
  .dpt-row{display:flex;align-items:flex-start;gap:12px;padding:10px 2px;border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.07));cursor:pointer}
@@ -355,6 +443,7 @@ window.__ModuleLoader__.load({
355
443
  .dpt-kv{display:flex;justify-content:space-between;gap:16px;font-size:12px;line-height:18px}
356
444
  .dpt-kv span:first-child{color:var(--dsw-alias-label-secondary,#6b7280)}
357
445
  .dpt-kv span:last-child{color:var(--dsw-alias-label-primary,#111);font-variant-numeric:tabular-nums;text-align:right}
446
+ .dpt-row-inline{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap}
358
447
  .dpt-divider{height:1px;background:var(--dsw-alias-border-l1,rgba(0,0,0,.08));margin:2px 0}
359
448
  .dpt-grid2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
360
449
  @media (max-width:560px){.dpt-grid2{grid-template-columns:1fr}.dpt-steps{flex-direction:column}.dpt-step + .dpt-step{border-left:none;border-top:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.08));padding-left:0}}
@@ -427,6 +516,10 @@ window.__ModuleLoader__.load({
427
516
  const [confirmed, setConfirmed] = useState(false);
428
517
  const [imported, setImported] = useState(null);
429
518
  const [copied, setCopied] = useState(false);
519
+ const [usbPath, setUsbPath] = useState("");
520
+ const [usbArchives, setUsbArchives] = useState([]);
521
+ const [usbBusy, setUsbBusy] = useState(false);
522
+ const [mode, setMode] = useState("full");
430
523
 
431
524
  useEffect(() => {
432
525
  let alive = true;
@@ -440,7 +533,7 @@ window.__ModuleLoader__.load({
440
533
  }
441
534
  const value = result.value;
442
535
  setOptions(value);
443
- setSelected({ ...value.selection });
536
+ setSelected({ ...value.selection, projectsMemory: false });
444
537
  setDestination(value.destination ?? "");
445
538
  setPhase("ready");
446
539
  })
@@ -461,7 +554,42 @@ window.__ModuleLoader__.load({
461
554
  .filter((row) => row !== undefined);
462
555
  }, [options]);
463
556
 
464
- const selectedCount = Object.values(selected).filter(Boolean).length;
557
+ const visibleGroups = useMemo(
558
+ () =>
559
+ groups.filter(
560
+ (group) =>
561
+ group.id !== "projectsMemory" &&
562
+ (mode === "full" || group.id === "workspaces" || group.id === "workspaceFolders"),
563
+ ),
564
+ [groups, mode],
565
+ );
566
+
567
+ const selectedCount = visibleGroups.filter((group) => selected[group.id] === true).length;
568
+
569
+ function switchMode(next) {
570
+ setMode(next);
571
+ if (next === "project") {
572
+ setSelected({
573
+ profile: false,
574
+ localPlugins: false,
575
+ workspaces: true,
576
+ workspaceFolders: true,
577
+ skills: false,
578
+ memory: false,
579
+ projectsMemory: false,
580
+ config: false,
581
+ sessions: false,
582
+ attachments: false,
583
+ credentials: false,
584
+ });
585
+ } else {
586
+ setSelected({ ...(options?.selection ?? {}), projectsMemory: false });
587
+ }
588
+ setPreview(null);
589
+ setImported(null);
590
+ setMessage(null);
591
+ if (next === "project" && usbPath.trim().length > 0) setDestination(usbPath.trim());
592
+ }
465
593
 
466
594
  function toggle(id, value) {
467
595
  setSelected((current) => ({ ...current, [id]: value }));
@@ -491,18 +619,20 @@ window.__ModuleLoader__.load({
491
619
  }
492
620
  }
493
621
 
494
- async function doInspect() {
622
+ async function doInspect(targetPath) {
495
623
  if (busy !== null) return;
496
- if (archivePath.trim().length === 0) {
624
+ const requested = typeof targetPath === "string" ? targetPath.trim() : archivePath.trim();
625
+ if (requested.length === 0) {
497
626
  setMessage({ tone: "error", text: t("missingArchive") });
498
627
  return;
499
628
  }
629
+ setArchivePath(requested);
500
630
  setBusy("inspect");
501
631
  setMessage(null);
502
632
  setPreview(null);
503
633
  setImported(null);
504
634
  try {
505
- const result = await call("inspect", { archivePath: archivePath.trim() });
635
+ const result = await call("inspect", { archivePath: requested });
506
636
  if (!result?.ok) {
507
637
  setMessage({ tone: "error", text: `${t("errorPrefix")} ${result?.error?.message ?? "unknown"}` });
508
638
  return;
@@ -515,6 +645,34 @@ window.__ModuleLoader__.load({
515
645
  }
516
646
  }
517
647
 
648
+ async function findArchives() {
649
+ if (busy !== null || usbBusy) return;
650
+ if (usbPath.trim().length === 0) {
651
+ setMessage({ tone: "error", text: t("usbPathPlaceholder") });
652
+ return;
653
+ }
654
+ setUsbBusy(true);
655
+ setMessage(null);
656
+ try {
657
+ const result = await call("find-archives", { directory: usbPath.trim() });
658
+ if (!result?.ok) {
659
+ setMessage({ tone: "error", text: `${t("errorPrefix")} ${result?.error?.message ?? "unknown"}` });
660
+ setUsbArchives([]);
661
+ return;
662
+ }
663
+ const archives = Array.isArray(result.value?.archives) ? result.value.archives : [];
664
+ setUsbArchives(archives);
665
+ setMessage({
666
+ tone: archives.length > 0 ? "ok" : "error",
667
+ text: archives.length > 0 ? t("usbFound").replace("{count}", String(archives.length)) : t("usbNoArchives"),
668
+ });
669
+ } catch (error) {
670
+ setMessage({ tone: "error", text: `${t("errorPrefix")} ${String(error?.message ?? error)}` });
671
+ } finally {
672
+ setUsbBusy(false);
673
+ }
674
+ }
675
+
518
676
  async function doImport() {
519
677
  if (busy !== null) return;
520
678
  if (!confirmed) {
@@ -571,7 +729,7 @@ window.__ModuleLoader__.load({
571
729
  const stepOneGroups = h(
572
730
  "div",
573
731
  { className: "dpt-list" },
574
- groups.map((group) => h(GroupRow, {
732
+ visibleGroups.map((group) => h(GroupRow, {
575
733
  key: group.id,
576
734
  group,
577
735
  checked: selected[group.id] === true,
@@ -600,12 +758,48 @@ window.__ModuleLoader__.load({
600
758
  "div",
601
759
  { className: "dpt-card-head" },
602
760
  h("div", null,
603
- h("div", { className: "dpt-card-title", id: "dpt-export-title" }, t("exportTitle")),
604
- h("div", { className: "dpt-card-sub" }, t("exportSubtitle")),
761
+ h("div", { className: "dpt-card-title", id: "dpt-export-title" }, mode === "project" ? t("projectExportTitle") : t("exportTitle")),
762
+ h("div", { className: "dpt-card-sub" }, mode === "project" ? t("projectExportSub") : t("exportSubtitle")),
605
763
  ),
606
- h("span", { className: "dpt-chip" }, phase === "loading" ? "…" : `${selectedCount} / ${groups.length}`),
764
+ h("span", { className: "dpt-chip" }, phase === "loading" ? "…" : `${selectedCount} / ${visibleGroups.length}`),
607
765
  ),
608
766
  phase === "loading" && h("div", { className: "dpt-hint" }, t("optionsLoading")),
767
+ phase !== "loading"
768
+ ? h(
769
+ "div",
770
+ { className: "dpt-modes", role: "tablist", "aria-label": t("modeTitle") },
771
+ [
772
+ ["full", "modeFull", "modeFullDesc"],
773
+ ["project", "modeProject", "modeProjectDesc"],
774
+ ].map(([id, titleKey, descKey]) =>
775
+ h(
776
+ "button",
777
+ {
778
+ key: id,
779
+ type: "button",
780
+ role: "tab",
781
+ className: "dpt-mode",
782
+ "data-active": mode === id ? "true" : "false",
783
+ "aria-selected": mode === id ? "true" : "false",
784
+ onClick: () => switchMode(id),
785
+ },
786
+ h(
787
+ "span",
788
+ { className: "dpt-mode-icon", "aria-hidden": "true" },
789
+ id === "full"
790
+ ? h("svg", { viewBox: "0 0 20 20", width: 17, height: 17, fill: "none" }, h("path", { d: "M3.5 6.5h13v8h-13zM3.5 6.5 5 3.5h10l1.5 3M6.5 14v2h7v-2", stroke: "currentColor", strokeWidth: 1.4, strokeLinejoin: "round" }))
791
+ : h("svg", { viewBox: "0 0 20 20", width: 17, height: 17, fill: "none" }, h("path", { d: "M3.5 5.5h4l1.5 2h7.5v7.5h-13zM3.5 5.5V4h5l1.5 1.5", stroke: "currentColor", strokeWidth: 1.4, strokeLinejoin: "round" })),
792
+ ),
793
+ h(
794
+ "span",
795
+ { className: "dpt-mode-copy" },
796
+ h("span", { className: "dpt-mode-title" }, t(titleKey)),
797
+ h("span", { className: "dpt-mode-desc" }, t(descKey)),
798
+ ),
799
+ ),
800
+ ),
801
+ )
802
+ : null,
609
803
  phase !== "loading" && stepOneGroups,
610
804
  h(
611
805
  "div",
@@ -628,7 +822,7 @@ window.__ModuleLoader__.load({
628
822
  "button",
629
823
  { type: "button", className: "dpt-btn", "data-tone": "primary", disabled: busy !== null || phase !== "ready", onClick: doExport },
630
824
  busy === "export" ? h(Spinner) : null,
631
- busy === "export" ? t("exporting") : t("exportButton"),
825
+ busy === "export" ? t("exporting") : mode === "project" ? t("projectExportButton") : t("exportButton"),
632
826
  ),
633
827
  exported !== null
634
828
  ? h(
@@ -653,6 +847,72 @@ window.__ModuleLoader__.load({
653
847
  : null,
654
848
  ),
655
849
 
850
+ /* ---------------- USB / projects and code ---------------- */
851
+ mode === "project"
852
+ ? h(
853
+ "section",
854
+ { className: "dpt-card", "aria-labelledby": "dpt-usb-title" },
855
+ h(
856
+ "div",
857
+ { className: "dpt-card-head" },
858
+ h(
859
+ "div",
860
+ null,
861
+ h("div", { className: "dpt-card-title", id: "dpt-usb-title" }, t("usbTitle")),
862
+ h("div", { className: "dpt-card-sub" }, t("usbHint")),
863
+ ),
864
+ h("span", { className: "dpt-chip" }, "USB"),
865
+ ),
866
+ h(
867
+ "div",
868
+ { className: "dpt-field" },
869
+ h("label", { className: "dpt-label", htmlFor: "dpt-usb-path" }, t("usbPathLabel")),
870
+ h("input", {
871
+ id: "dpt-usb-path",
872
+ className: "dpt-input dpt-input--path",
873
+ type: "text",
874
+ value: usbPath,
875
+ placeholder: t("usbPathPlaceholder"),
876
+ spellCheck: false,
877
+ onChange: (event) => setUsbPath(event.target.value),
878
+ }),
879
+ ),
880
+ h(
881
+ "div",
882
+ { className: "dpt-actions" },
883
+ h(
884
+ "button",
885
+ { type: "button", className: "dpt-btn", disabled: busy !== null || usbBusy, onClick: findArchives },
886
+ usbBusy ? h(Spinner) : null,
887
+ usbBusy ? t("usbScanning") : t("usbScan"),
888
+ ),
889
+ ),
890
+ usbArchives.length > 0
891
+ ? h(
892
+ "div",
893
+ { className: "dpt-preview" },
894
+ usbArchives.map((archive) =>
895
+ h(
896
+ "div",
897
+ { className: "dpt-kv", key: archive.path },
898
+ h("span", { className: "dpt-mono" }, archive.name),
899
+ h(
900
+ "span",
901
+ { className: "dpt-row-inline" },
902
+ h("span", null, t("usbArchiveMeta").replace("{size}", archive.human ?? "").replace("{date}", new Date(archive.mtimeMs).toLocaleDateString())),
903
+ h(
904
+ "button",
905
+ { type: "button", className: "dpt-btn", disabled: busy !== null, onClick: () => void doInspect(archive.path) },
906
+ t("usbPick"),
907
+ ),
908
+ ),
909
+ ),
910
+ ),
911
+ )
912
+ : null,
913
+ )
914
+ : null,
915
+
656
916
  /* ---------------- import ---------------- */
657
917
  h(
658
918
  "section",
@@ -21,6 +21,7 @@ import {
21
21
  defaultHome,
22
22
  describeSources,
23
23
  exportBundle,
24
+ findTransferArchives,
24
25
  humanSize,
25
26
  inspectBundle,
26
27
  normalizeSelection,
@@ -204,6 +205,14 @@ async function handleExport(ctx, payload) {
204
205
  };
205
206
  }
206
207
 
208
+ async function handleFindArchives(ctx, payload) {
209
+ const directory = typeof payload?.directory === "string" ? payload.directory.trim() : "";
210
+ if (directory.length === 0) throw new TransferError("transfer/bad-request", "укажите путь к флешке");
211
+ if (!fs.existsSync(directory)) throw new TransferError("transfer/not-found", `папка не найдена: ${directory}`);
212
+ const result = findTransferArchives(directory);
213
+ return { ok: true, root: result.root, archives: result.archives };
214
+ }
215
+
207
216
  async function handleInspect(ctx, payload) {
208
217
  const archivePath = typeof payload?.archivePath === "string" ? payload.archivePath.trim() : "";
209
218
  if (archivePath.length === 0) throw new TransferError("transfer/bad-request", "укажите путь к архиву");
@@ -364,6 +373,7 @@ export function apply(ctx) {
364
373
  }
365
374
  registerOperation(ctx, "options", (context) => handleOptions(context));
366
375
  registerOperation(ctx, "export", (context, payload) => handleExport(context, payload));
376
+ registerOperation(ctx, "find-archives", (context, payload) => handleFindArchives(context, payload));
367
377
  registerOperation(ctx, "inspect", (context, payload) => handleInspect(context, payload));
368
378
  registerOperation(ctx, "import", (context, payload) => handleImport(context, payload));
369
379
  registerOperation(ctx, "install", (context) => handleInstall(context));
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-profile-transfer",
3
- "version": "1.3.0",
3
+ "version": "1.3.3",
4
4
  "private": true,
5
5
  "description": "Move a complete DSH setup between machines: pick what to export (profile, skills, memory, local plugins, sessions, secrets), pack one archive, restore it on a fresh install.",
6
6
  "type": "module",
@@ -34,3 +34,5 @@
34
34
  }
35
35
  }
36
36
  }
37
+
38
+