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.
- package/LICENSE +21 -0
- package/README.md +152 -2
- package/client.js +4101 -0
- package/cordis.patch.yml +5 -0
- package/docs/ARCHITECTURE.md +114 -0
- package/docs/COMPATIBILITY.md +61 -0
- package/docs/INSTALL.md +84 -0
- package/docs/PRIVACY.md +50 -0
- package/docs/ROADMAP.md +57 -0
- package/docs/TOKENS.md +118 -0
- package/docs/TROUBLESHOOTING.md +68 -0
- package/docs/images/hero.png +0 -0
- package/index.js +84 -0
- package/package.json +70 -4
- package/plugins/dsh-gkb-ru-ui/cordis.patch.yml +6 -0
- package/plugins/dsh-gkb-ru-ui/lib/client.js +1331 -0
- package/plugins/dsh-gkb-ru-ui/lib/index.js +186 -0
- package/plugins/dsh-gkb-ru-ui/lib/skills.js +570 -0
- package/plugins/dsh-gkb-ru-ui/package.json +28 -0
- package/plugins/dsh-gkb-ru-ui/screenshots/01-home-tiles-1440-light.png +0 -0
- package/plugins/dsh-gkb-ru-ui/screenshots/02-skills-1440-light.png +0 -0
- package/plugins/dsh-gkb-ru-ui/screenshots/03-skills-editor-1440-light.png +0 -0
- package/plugins/dsh-gkb-ru-ui/screenshots/04-skills-1440-dark.png +0 -0
- package/plugins/dsh-gkb-ru-ui/screenshots/05-skills-390-light.png +0 -0
- package/plugins/dsh-gkb-ru-ui/screenshots/06-home-tiles-1440-dark.png +0 -0
- package/plugins/dsh-peak-guard/README.md +70 -0
- package/plugins/dsh-peak-guard/client.js +741 -0
- package/plugins/dsh-peak-guard/cordis.patch.yml +5 -0
- package/plugins/dsh-peak-guard/icon.svg +12 -0
- package/plugins/dsh-peak-guard/index.js +2 -0
- package/plugins/dsh-peak-guard/locale/en.json +6 -0
- package/plugins/dsh-peak-guard/locale/ru.json +6 -0
- package/plugins/dsh-peak-guard/locale/zh.json +6 -0
- package/plugins/dsh-peak-guard/package.json +33 -0
- package/plugins/dsh-peak-guard/screenshots/01-real-time-peak-start.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/02-real-time-peak-end.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/03-peak-card-1440-light.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/04-peak-card-1440-dark.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/05-peak-card-390-light.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/05-peak-card-768-light.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/06-peak-card-390-light.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/07-live-preview-check.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/07-pre-peak-1440-light.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/08-peak-start-1440-light.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/09-peak-end-1440-light.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/10-pre-peak-card-1440-light.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/11-pre-peak-card-1440-dark.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/12-pre-peak-card-390-light.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/qa-report.json +74 -0
- package/plugins/dsh-profile-transfer/README.md +62 -0
- package/plugins/dsh-profile-transfer/bootstrap/01-INSTALL.cmd +7 -0
- package/plugins/dsh-profile-transfer/bootstrap/README-ARCHIVE.md +54 -0
- 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
- package/plugins/dsh-profile-transfer/bootstrap/install-on-clean-machine.ps1 +119 -0
- 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
- package/plugins/dsh-profile-transfer/client.js +806 -0
- package/plugins/dsh-profile-transfer/cordis.patch.yml +3 -0
- package/plugins/dsh-profile-transfer/icon.svg +13 -0
- package/plugins/dsh-profile-transfer/index.js +375 -0
- package/plugins/dsh-profile-transfer/locale/en.json +6 -0
- package/plugins/dsh-profile-transfer/locale/ru.json +6 -0
- package/plugins/dsh-profile-transfer/locale/zh.json +6 -0
- package/plugins/dsh-profile-transfer/package.json +36 -0
- package/plugins/dsh-profile-transfer/screenshots/01-settings-transfer-1440-light.png +0 -0
- package/plugins/dsh-profile-transfer/screenshots/02-settings-transfer-preview-1440-light.png +0 -0
- package/plugins/dsh-profile-transfer/screenshots/03-settings-transfer-1440-dark.png +0 -0
- package/plugins/dsh-profile-transfer/screenshots/04-settings-transfer-390-light.png +0 -0
- package/plugins/dsh-profile-transfer/screenshots/05-settings-transfer-768-light.png +0 -0
- package/plugins/dsh-profile-transfer/screenshots/06-settings-transfer-390-dark.png +0 -0
- package/plugins/dsh-profile-transfer/screenshots/07-settings-transfer-export-result.png +0 -0
- package/plugins/dsh-profile-transfer/screenshots/08-settings-transfer-workspaces-1440-light.png +0 -0
- package/plugins/dsh-profile-transfer/screenshots/09-settings-transfer-workspaces-1440-dark.png +0 -0
- package/plugins/dsh-profile-transfer/screenshots/10-settings-transfer-workspaces-390-light.png +0 -0
- package/plugins/dsh-profile-transfer/transfer.js +1004 -0
- package/plugins/dsh-project-memory/README.md +71 -0
- package/plugins/dsh-project-memory/cordis.patch.yml +4 -0
- package/plugins/dsh-project-memory/icon.svg +12 -0
- package/plugins/dsh-project-memory/index.js +1240 -0
- package/plugins/dsh-project-memory/locale/en.json +6 -0
- package/plugins/dsh-project-memory/locale/ru.json +6 -0
- package/plugins/dsh-project-memory/locale/zh.json +6 -0
- package/plugins/dsh-project-memory/package.json +26 -0
- package/plugins/dsh-session-move/README.md +67 -0
- package/plugins/dsh-session-move/client.js +719 -0
- package/plugins/dsh-session-move/cordis.patch.yml +5 -0
- package/plugins/dsh-session-move/icon.svg +14 -0
- package/plugins/dsh-session-move/index.js +497 -0
- package/plugins/dsh-session-move/locale/en.json +6 -0
- package/plugins/dsh-session-move/locale/ru.json +6 -0
- package/plugins/dsh-session-move/locale/zh.json +6 -0
- package/plugins/dsh-session-move/package.json +31 -0
- package/plugins/dsh-session-move/screenshots/01-row-menu-1440-light.png +0 -0
- package/plugins/dsh-session-move/screenshots/02-dialog-1440-light.png +0 -0
- package/plugins/dsh-session-move/screenshots/03-dialog-390-light.png +0 -0
- package/plugins/dsh-session-move/screenshots/04-dialog-1440-dark.png +0 -0
- package/plugins/dsh-session-move/screenshots/05-drag-highlight-1440-light.png +0 -0
- package/plugins/dsh-session-move/screenshots/06-drop-ungrouped-highlight.png +0 -0
- package/plugins/dsh-session-move/screenshots/07-after-drop-ungrouped.png +0 -0
- package/plugins/dsh-session-move/screenshots/08-move-dialog.png +0 -0
- package/plugins/dsh-session-move/screenshots/09-moved-to-second-workspace.png +0 -0
- package/plugins/dsh-session-move/screenshots/qa-report.json +62 -0
- package/plugins/dsh-skill-autopilot/cordis.patch.yml +3 -0
- package/plugins/dsh-skill-autopilot/index.js +444 -0
- package/plugins/dsh-skill-autopilot/package.json +19 -0
- package/plugins/dsh-token-lab/cordis.patch.yml +3 -0
- package/plugins/dsh-token-lab/lib/client.js +504 -0
- package/plugins/dsh-token-lab/lib/index.js +275 -0
- package/plugins/dsh-token-lab/package.json +26 -0
- package/plugins/dsh-token-lab/screenshots/01-token-lab-1440-light.png +0 -0
- package/plugins/dsh-token-lab/screenshots/02-token-lab-1440-dark.png +0 -0
- package/skills/code-review-checklist/SKILL.md +55 -0
- package/skills/content-writing/README.md +87 -0
- package/skills/content-writing/SKILL.md +85 -0
- package/skills/content-writing/assets/checklists/fact-check.md +53 -0
- package/skills/content-writing/assets/checklists/publish.md +65 -0
- package/skills/content-writing/assets/checklists/social.md +40 -0
- package/skills/content-writing/assets/templates/article.md +47 -0
- package/skills/content-writing/assets/templates/brief.md +44 -0
- package/skills/content-writing/assets/templates/doctor-card.md +41 -0
- package/skills/content-writing/assets/templates/image-prompts.md +128 -0
- package/skills/content-writing/assets/templates/post-telegram.md +36 -0
- package/skills/content-writing/assets/templates/post-vk.md +42 -0
- package/skills/content-writing/assets/templates/press-release.md +56 -0
- package/skills/content-writing/assets/templates/science-pop-note.md +55 -0
- package/skills/content-writing/assets/templates/service-card.md +46 -0
- package/skills/content-writing/references/anti-ai-style.md +248 -0
- package/skills/content-writing/references/editing.md +185 -0
- package/skills/content-writing/references/fact-checking.md +205 -0
- package/skills/content-writing/references/government-medical.md +246 -0
- package/skills/content-writing/references/images.md +212 -0
- package/skills/content-writing/references/journalism.md +252 -0
- package/skills/content-writing/references/process.md +123 -0
- package/skills/content-writing/references/quick-recipes.md +99 -0
- package/skills/content-writing/references/rules.md +85 -0
- package/skills/content-writing/references/science-popular.md +218 -0
- package/skills/content-writing/references/seo-content.md +172 -0
- package/skills/content-writing/references/social-media.md +253 -0
- package/skills/content-writing/references/sources.md +49 -0
- package/skills/content-writing/references/templates.md +291 -0
- package/skills/context-economy/SKILL.md +60 -0
- package/skills/context-economy/references/long-session.md +35 -0
- package/skills/context-economy/references/token-math.md +42 -0
- package/skills/context-economy/references/tool-recipes.md +34 -0
- package/skills/database-safety/SKILL.md +47 -0
- package/skills/debugging-playbook/SKILL.md +42 -0
- package/skills/design-critique/SKILL.md +65 -0
- package/skills/design-critique/references/a11y.md +25 -0
- package/skills/design-critique/references/diagnoses.md +33 -0
- package/skills/design-critique/references/quick-tests.md +30 -0
- package/skills/design-critique/references/report-template.md +63 -0
- package/skills/design-critique/references/rubric-fast.md +73 -0
- package/skills/dev-workflow/SKILL.md +48 -0
- package/skills/dev-workflow/references/definition-of-done.md +44 -0
- package/skills/dev-workflow/references/memory-and-boundaries.md +31 -0
- package/skills/dev-workflow/references/stages.md +67 -0
- package/skills/git-and-ci/SKILL.md +60 -0
- package/skills/pg-upgrade-verify/SKILL.md +67 -0
- package/skills/project-deep-dive/SKILL.md +45 -0
- package/skills/project-deep-dive/references/recon-commands.md +90 -0
- package/skills/project-deep-dive/references/section-templates.md +104 -0
- package/skills/project-deep-dive/references/tool-and-process.md +86 -0
- package/skills/project-memory-maintenance/SKILL.md +55 -0
- package/skills/project-memory-maintenance/references/api-and-checklist.md +99 -0
- package/skills/project-memory-maintenance/references/journal-template.md +50 -0
- package/skills/project-memory-maintenance/references/section-rules.md +59 -0
- package/skills/release-and-deploy/SKILL.md +53 -0
- package/skills/requirements-and-planning/SKILL.md +49 -0
- package/skills/requirements-and-planning/references/antipatterns-and-checklist.md +20 -0
- package/skills/requirements-and-planning/references/process-steps.md +52 -0
- package/skills/requirements-and-planning/references/templates-and-estimates.md +29 -0
- package/skills/security-review/SKILL.md +40 -0
- package/skills/seo/README.md +60 -0
- package/skills/seo/SKILL.md +77 -0
- package/skills/seo/assets/README.md +66 -0
- package/skills/seo/assets/scripts/LICENSE-playwright-core.txt +202 -0
- package/skills/seo/assets/scripts/README.md +179 -0
- package/skills/seo/assets/scripts/lib/args.mjs +105 -0
- package/skills/seo/assets/scripts/lib/browser.mjs +620 -0
- package/skills/seo/assets/scripts/lib/zip.mjs +191 -0
- package/skills/seo/assets/scripts/seo-audit.mjs +1675 -0
- package/skills/seo/assets/snippets/jsonld-breadcrumb.json +30 -0
- package/skills/seo/assets/snippets/jsonld-faq.json +39 -0
- package/skills/seo/assets/snippets/jsonld-medical-clinic.json +49 -0
- package/skills/seo/assets/snippets/jsonld-medical-webpage.json +33 -0
- package/skills/seo/assets/snippets/jsonld-organization.json +74 -0
- package/skills/seo/assets/snippets/jsonld-physician.json +46 -0
- package/skills/seo/assets/snippets/meta.blade.php +61 -0
- package/skills/seo/assets/snippets/robots.txt +61 -0
- package/skills/seo/assets/snippets/sitemap-urlset-example.xml +72 -0
- package/skills/seo/assets/snippets/sitemap.xml +36 -0
- package/skills/seo/references/antipatterns.md +66 -0
- package/skills/seo/references/audit-checklist.md +319 -0
- package/skills/seo/references/content-seo.md +204 -0
- package/skills/seo/references/eeat-medical.md +219 -0
- package/skills/seo/references/measurement.md +250 -0
- package/skills/seo/references/on-page.md +214 -0
- package/skills/seo/references/page-templates.md +56 -0
- package/skills/seo/references/stack-recipes.md +695 -0
- package/skills/seo/references/structured-data.md +487 -0
- package/skills/seo/references/technical-seo.md +296 -0
- package/skills/service-desk-conventions/SKILL.md +52 -0
- package/skills/service-desk-db-safety/SKILL.md +63 -0
- package/skills/service-desk-release/SKILL.md +61 -0
- package/skills/service-desk-testing/SKILL.md +61 -0
- package/skills/service-desk-ui/SKILL.md +52 -0
- package/skills/skills-router/SKILL.md +53 -0
- package/skills/skills-router/references/applying-skills.md +25 -0
- package/skills/skills-router/references/routing-matrix.md +52 -0
- package/skills/testing-strategy/SKILL.md +52 -0
- package/skills/ui-design-system/SKILL.md +50 -0
- package/skills/ui-design-system/references/accessibility-and-responsive.md +50 -0
- package/skills/ui-design-system/references/tokens-and-components.md +67 -0
- package/skills/ui-design-system/references/visual-qa.md +44 -0
- package/skills/web-research/SKILL.md +47 -0
- package/skills/web-research/references/answer-and-citations.md +60 -0
- package/skills/web-research/references/query-recipes.md +44 -0
- package/skills/wow-design/SKILL.md +89 -0
- package/skills/wow-design/assets/kit/README.md +195 -0
- package/skills/wow-design/assets/kit/assets/css/base.css +158 -0
- package/skills/wow-design/assets/kit/assets/css/components.css +1001 -0
- package/skills/wow-design/assets/kit/assets/css/fonts.css +73 -0
- package/skills/wow-design/assets/kit/assets/css/pages.css +737 -0
- package/skills/wow-design/assets/kit/assets/css/tokens.css +218 -0
- package/skills/wow-design/assets/kit/assets/fonts/LICENSE-golos-text.txt +93 -0
- package/skills/wow-design/assets/kit/assets/fonts/LICENSE-lora.txt +93 -0
- package/skills/wow-design/assets/kit/assets/fonts/LICENSE-unbounded.txt +93 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-cyrillic-400-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-cyrillic-500-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-cyrillic-600-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-cyrillic-700-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-latin-400-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-latin-500-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-latin-600-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-latin-700-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/lora-cyrillic-400-italic.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/lora-cyrillic-400-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/lora-cyrillic-500-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/lora-cyrillic-600-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/lora-latin-400-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/lora-latin-500-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/lora-latin-600-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/unbounded-cyrillic-400-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/unbounded-cyrillic-500-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/unbounded-cyrillic-600-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/unbounded-latin-400-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/unbounded-latin-500-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/unbounded-latin-600-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/js/kit.js +550 -0
- package/skills/wow-design/assets/kit/pages/dashboard.html +539 -0
- package/skills/wow-design/assets/kit/pages/hospital.html +430 -0
- package/skills/wow-design/assets/kit/showcase.html +1164 -0
- package/skills/wow-design/assets/scripts/LICENSE-axe-core.txt +362 -0
- package/skills/wow-design/assets/scripts/LICENSE-playwright-core.txt +202 -0
- package/skills/wow-design/assets/scripts/README.md +312 -0
- package/skills/wow-design/assets/scripts/audit.mjs +1200 -0
- package/skills/wow-design/assets/scripts/capture.mjs +337 -0
- package/skills/wow-design/assets/scripts/package.json +30 -0
- package/skills/wow-design/references/anti-patterns.md +252 -0
- package/skills/wow-design/references/critique-rubric.md +262 -0
- package/skills/wow-design/references/design-brief-template.md +48 -0
- package/skills/wow-design/references/inspiration.md +203 -0
- package/skills/wow-design/references/presets.md +99 -0
- package/skills/wow-design/references/principles.md +371 -0
- package/skills/wow-design/references/quick-presets.md +50 -0
- package/skills/wow-design/references/report-template.md +21 -0
- package/skills/wow-design/references/stack-recipes.md +96 -0
- package/skills/wow-design/references/visual-qa.md +82 -0
- package/skills/wow-design/references/wow-levers.md +43 -0
- package/tools/skill-cost.mjs +76 -0
|
@@ -0,0 +1,1004 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-profile-transfer — перенос профиля DSH между машинами.
|
|
3
|
+
*
|
|
4
|
+
* Чистый Node-модуль без зависимостей: собирает выбранные части домашней папки DSH
|
|
5
|
+
* (`~/.dsh`) и профиля в один `.tar.gz`, а затем безопасно разворачивает его на
|
|
6
|
+
* другой машине (с бэкапом текущего состояния). Host-плагин в index.js даёт к этому
|
|
7
|
+
* RPC-доступ, client.js — страницу настроек.
|
|
8
|
+
*
|
|
9
|
+
* Формат архива:
|
|
10
|
+
* manifest.json
|
|
11
|
+
* payload/dsh/... файлы из домашней папки DSH
|
|
12
|
+
* payload/profiles/<profile>/... файлы профиля (package.json, patch, lock)
|
|
13
|
+
* payload/local-plugins/<package>/... исходники локальных link:-плагинов
|
|
14
|
+
* payload/skills/... навыки
|
|
15
|
+
* payload/memory/... память (memory/)
|
|
16
|
+
* payload/projects-memory/... память проектов
|
|
17
|
+
* payload/sessions/... сессии (опционально)
|
|
18
|
+
* payload/attachments/... вложения (опционально)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import fs from "node:fs";
|
|
22
|
+
import os from "node:os";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
import zlib from "node:zlib";
|
|
25
|
+
import crypto from "node:crypto";
|
|
26
|
+
import { fileURLToPath } from "node:url";
|
|
27
|
+
|
|
28
|
+
const PLUGIN_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const BOOTSTRAP_PLUGIN_FILES = ["package.json", "index.js", "client.js", "transfer.js", "cordis.patch.yml", "icon.svg", "README.md"];
|
|
30
|
+
|
|
31
|
+
export const FORMAT = "dsh-profile-transfer";
|
|
32
|
+
export const FORMAT_VERSION = 1;
|
|
33
|
+
const EXCLUDED_NAMES = new Set(["node_modules", ".git", ".DS_Store", "Thumbs.db"]);
|
|
34
|
+
const WORKSPACE_FOLDER_EXCLUDES = new Set(["pgruntime", ".venv", "venv", "__pycache__", ".pytest_cache", ".mypy_cache", ".cache", ".next", "coverage", "tmp"]);
|
|
35
|
+
const MAX_WORKSPACE_FOLDER_BYTES = 1024 * 1024 * 1024;
|
|
36
|
+
const MAX_WORKSPACE_FOLDER_FILES = 250000;
|
|
37
|
+
const SKIP_DSH_TOP_LEVEL = new Set([
|
|
38
|
+
"profiles",
|
|
39
|
+
"skills",
|
|
40
|
+
"memory",
|
|
41
|
+
"projects-memory",
|
|
42
|
+
"sessions",
|
|
43
|
+
"attachments",
|
|
44
|
+
"cache",
|
|
45
|
+
"logs",
|
|
46
|
+
"storages",
|
|
47
|
+
"dsh-runtimes",
|
|
48
|
+
"dsh-market",
|
|
49
|
+
".dsh-market",
|
|
50
|
+
"backup-import",
|
|
51
|
+
"backup-session-move-install-2026-11-04",
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
/* ------------------------------------------------------------------ */
|
|
55
|
+
/* tar (ustar) writer / reader */
|
|
56
|
+
/* ------------------------------------------------------------------ */
|
|
57
|
+
|
|
58
|
+
function pad(value, size) {
|
|
59
|
+
const buffer = Buffer.alloc(size, 0);
|
|
60
|
+
buffer.write(String(value), 0, size, "utf8");
|
|
61
|
+
return buffer;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function octal(value, size) {
|
|
65
|
+
return `${(value >>> 0).toString(8).padStart(size - 1, "0")}\0`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Split a path into ustar name (100) and prefix (155). */
|
|
69
|
+
function tarNameParts(relativePath) {
|
|
70
|
+
const normalized = relativePath.replaceAll("\\", "/").replace(/^\/+/u, "");
|
|
71
|
+
const bytes = Buffer.byteLength(normalized, "utf8");
|
|
72
|
+
if (bytes <= 100) return { name: normalized, prefix: "" };
|
|
73
|
+
const parts = normalized.split("/");
|
|
74
|
+
for (let index = parts.length - 1; index > 0; index -= 1) {
|
|
75
|
+
const prefix = parts.slice(0, index).join("/");
|
|
76
|
+
const name = parts.slice(index).join("/");
|
|
77
|
+
if (Buffer.byteLength(name, "utf8") <= 100 && Buffer.byteLength(prefix, "utf8") <= 155) return { name, prefix };
|
|
78
|
+
}
|
|
79
|
+
throw new Error(`path is too long for tar: ${relativePath}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function tarHeader({ relativePath, size, mode = 0o644, type = "0", mtime = Math.floor(Date.now() / 1000) }) {
|
|
83
|
+
const { name, prefix } = tarNameParts(relativePath);
|
|
84
|
+
const header = Buffer.alloc(512, 0);
|
|
85
|
+
header.write(name, 0, 100, "utf8");
|
|
86
|
+
header.write(octal(mode, 8), 100, 8, "ascii");
|
|
87
|
+
header.write(octal(0, 8), 108, 8, "ascii");
|
|
88
|
+
header.write(octal(0, 8), 116, 8, "ascii");
|
|
89
|
+
header.write(octal(size, 12), 124, 12, "ascii");
|
|
90
|
+
header.write(octal(mtime, 12), 136, 12, "ascii");
|
|
91
|
+
header.write(" ", 148, 8, "ascii");
|
|
92
|
+
header.write(type, 156, 1, "ascii");
|
|
93
|
+
header.write("ustar\0", 257, 6, "ascii");
|
|
94
|
+
header.write("00", 263, 2, "ascii");
|
|
95
|
+
header.write(prefix, 345, 155, "utf8");
|
|
96
|
+
let sum = 0;
|
|
97
|
+
for (const byte of header) sum += byte;
|
|
98
|
+
header.write(octal(sum, 7), 148, 7, "ascii");
|
|
99
|
+
header.write(" ", 155, 1, "ascii");
|
|
100
|
+
return header;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function createTarGz() {
|
|
104
|
+
const chunks = [];
|
|
105
|
+
const addFile = (relativePath, bytes, stat) => {
|
|
106
|
+
const buffer = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
|
|
107
|
+
chunks.push(tarHeader({
|
|
108
|
+
relativePath,
|
|
109
|
+
size: buffer.length,
|
|
110
|
+
mode: stat?.mode ? stat.mode & 0o777 : 0o644,
|
|
111
|
+
mtime: stat?.mtime ? Math.floor(stat.mtimeMs / 1000) : Math.floor(Date.now() / 1000),
|
|
112
|
+
}));
|
|
113
|
+
chunks.push(buffer);
|
|
114
|
+
const remainder = buffer.length % 512;
|
|
115
|
+
if (remainder !== 0) chunks.push(Buffer.alloc(512 - remainder, 0));
|
|
116
|
+
};
|
|
117
|
+
const addDirectory = (relativePath, stat) => {
|
|
118
|
+
chunks.push(tarHeader({
|
|
119
|
+
relativePath: relativePath.endsWith("/") ? relativePath : `${relativePath}/`,
|
|
120
|
+
size: 0,
|
|
121
|
+
mode: stat?.mode ? stat.mode & 0o777 : 0o755,
|
|
122
|
+
type: "5",
|
|
123
|
+
mtime: stat?.mtime ? Math.floor(stat.mtimeMs / 1000) : Math.floor(Date.now() / 1000),
|
|
124
|
+
}));
|
|
125
|
+
};
|
|
126
|
+
const finish = () => {
|
|
127
|
+
chunks.push(Buffer.alloc(1024, 0));
|
|
128
|
+
return zlib.gzipSync(Buffer.concat(chunks), { level: 6 });
|
|
129
|
+
};
|
|
130
|
+
return { addFile, addDirectory, finish };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function parseOctal(buffer) {
|
|
134
|
+
const text = buffer.toString("ascii").replace(/\0.*$/u, "").trim();
|
|
135
|
+
if (text.length === 0) return 0;
|
|
136
|
+
return Number.parseInt(text, 8) || 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function parseTarGz(archive) {
|
|
140
|
+
const tar = zlib.gunzipSync(archive);
|
|
141
|
+
const entries = [];
|
|
142
|
+
let offset = 0;
|
|
143
|
+
while (offset + 512 <= tar.length) {
|
|
144
|
+
const header = tar.subarray(offset, offset + 512);
|
|
145
|
+
if (header.every((byte) => byte === 0)) break;
|
|
146
|
+
const name = header.subarray(0, 100).toString("utf8").replace(/\0.*$/u, "");
|
|
147
|
+
const prefix = header.subarray(345, 500).toString("utf8").replace(/\0.*$/u, "");
|
|
148
|
+
const size = parseOctal(header.subarray(124, 136));
|
|
149
|
+
const type = String.fromCharCode(header[156] || 48);
|
|
150
|
+
const relativePath = prefix.length > 0 ? `${prefix}/${name}` : name;
|
|
151
|
+
offset += 512;
|
|
152
|
+
const data = tar.subarray(offset, offset + size);
|
|
153
|
+
offset += size + ((512 - (size % 512)) % 512);
|
|
154
|
+
if (relativePath.length === 0) continue;
|
|
155
|
+
entries.push({ path: relativePath, type, data });
|
|
156
|
+
}
|
|
157
|
+
return entries;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* ------------------------------------------------------------------ */
|
|
161
|
+
/* paths and manifest */
|
|
162
|
+
/* ------------------------------------------------------------------ */
|
|
163
|
+
|
|
164
|
+
export function defaultHome() {
|
|
165
|
+
return process.env.DSH_HOME ?? path.join(os.homedir(), ".dsh");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function safeRelative(relativePath) {
|
|
169
|
+
const normalized = String(relativePath).replaceAll("\\", "/");
|
|
170
|
+
if (normalized.startsWith("/") || /^[A-Za-z]:/u.test(normalized)) throw new Error(`unsafe archive path: ${relativePath}`);
|
|
171
|
+
const segments = normalized.split("/");
|
|
172
|
+
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
173
|
+
throw new Error(`unsafe archive path: ${relativePath}`);
|
|
174
|
+
}
|
|
175
|
+
return segments.join("/");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function walkFiles(root, relativePrefix, options = {}) {
|
|
179
|
+
const results = [];
|
|
180
|
+
results.capped = false;
|
|
181
|
+
const exclude = options.exclude ?? new Set();
|
|
182
|
+
const extraExcludedNames = options.excludeNames ?? new Set();
|
|
183
|
+
const maxFiles = Number.isFinite(options.maxFiles) ? Math.max(1, Number(options.maxFiles)) : Infinity;
|
|
184
|
+
const walk = (current, relative) => {
|
|
185
|
+
let entries;
|
|
186
|
+
try {
|
|
187
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
188
|
+
} catch {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
for (const entry of entries) {
|
|
192
|
+
if (results.length >= maxFiles) {
|
|
193
|
+
results.capped = true;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (EXCLUDED_NAMES.has(entry.name) || extraExcludedNames.has(entry.name)) continue;
|
|
197
|
+
const absolute = path.join(current, entry.name);
|
|
198
|
+
const nextRelative = relative.length === 0 ? entry.name : `${relative}/${entry.name}`;
|
|
199
|
+
if (exclude.has(nextRelative) || exclude.has(entry.name)) continue;
|
|
200
|
+
let stat;
|
|
201
|
+
try {
|
|
202
|
+
stat = entry.isSymbolicLink() ? undefined : fs.statSync(absolute);
|
|
203
|
+
} catch {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (entry.isDirectory()) walk(absolute, nextRelative);
|
|
207
|
+
else if (stat !== undefined && stat.isFile()) {
|
|
208
|
+
results.push({
|
|
209
|
+
source: absolute,
|
|
210
|
+
relative: relativePrefix.length === 0 ? nextRelative : `${relativePrefix}/${nextRelative}`,
|
|
211
|
+
size: stat.size,
|
|
212
|
+
mtimeMs: stat.mtimeMs,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
walk(root, "");
|
|
218
|
+
return results;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function sizeOf(files) {
|
|
222
|
+
return files.reduce((total, file) => total + file.size, 0);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function humanSize(bytes) {
|
|
226
|
+
if (bytes < 1024) return `${bytes} Б`;
|
|
227
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} КБ`;
|
|
228
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} МБ`;
|
|
229
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} ГБ`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function isSafePackageName(name) {
|
|
233
|
+
return typeof name === "string" && /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/iu.test(name) && !name.includes("..");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function readJson(filePath) {
|
|
237
|
+
try {
|
|
238
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
239
|
+
} catch {
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function sha256(bytes) {
|
|
245
|
+
return crypto.createHash("sha256").update(bytes).digest("hex");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* ------------------------------------------------------------------ */
|
|
249
|
+
/* collection plan */
|
|
250
|
+
/* ------------------------------------------------------------------ */
|
|
251
|
+
|
|
252
|
+
const DEFAULT_SELECTION = {
|
|
253
|
+
profile: true,
|
|
254
|
+
localPlugins: true,
|
|
255
|
+
workspaces: true,
|
|
256
|
+
workspaceFolders: false,
|
|
257
|
+
skills: true,
|
|
258
|
+
memory: true,
|
|
259
|
+
projectsMemory: true,
|
|
260
|
+
sessions: false,
|
|
261
|
+
attachments: false,
|
|
262
|
+
credentials: false,
|
|
263
|
+
config: true,
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
export function normalizeSelection(selection) {
|
|
267
|
+
const result = { ...DEFAULT_SELECTION };
|
|
268
|
+
if (selection !== null && typeof selection === "object") {
|
|
269
|
+
for (const key of Object.keys(DEFAULT_SELECTION)) {
|
|
270
|
+
if (typeof selection[key] === "boolean") result[key] = selection[key];
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function profileDirectory(home, profileName) {
|
|
277
|
+
return path.join(home, "profiles", profileName);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function localPluginTargets(home, profileName) {
|
|
281
|
+
const directory = profileDirectory(home, profileName);
|
|
282
|
+
const manifest = readJson(path.join(directory, "package.json"));
|
|
283
|
+
const dependencies = manifest?.dependencies !== null && typeof manifest?.dependencies === "object" ? manifest.dependencies : {};
|
|
284
|
+
const targets = [];
|
|
285
|
+
for (const [packageName, spec] of Object.entries(dependencies)) {
|
|
286
|
+
if (typeof spec !== "string") continue;
|
|
287
|
+
const match = spec.match(/^(?:link|file):(.+)$/u);
|
|
288
|
+
if (match === null) continue;
|
|
289
|
+
let target = match[1].replaceAll("/", path.sep);
|
|
290
|
+
if (!path.isAbsolute(target)) target = path.resolve(directory, target);
|
|
291
|
+
targets.push({ packageName, spec, target });
|
|
292
|
+
}
|
|
293
|
+
return targets;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Workspaces from the local registry that have an absolute folder path. */
|
|
297
|
+
function workspaceDirectories(home) {
|
|
298
|
+
const registry = readJson(path.join(home, "storages", "workspace.json"));
|
|
299
|
+
const tables = registry?.tables?.workspaces;
|
|
300
|
+
if (tables === null || typeof tables !== "object") return [];
|
|
301
|
+
return Object.entries(tables)
|
|
302
|
+
.map(([id, workspace]) => ({
|
|
303
|
+
id,
|
|
304
|
+
title: typeof workspace?.title === "string" && workspace.title.length > 0 ? workspace.title : id,
|
|
305
|
+
path: typeof workspace?.path === "string" ? workspace.path : null,
|
|
306
|
+
}))
|
|
307
|
+
.filter((workspace) => typeof workspace.path === "string" && workspace.path.length > 0 && path.isAbsolute(workspace.path));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function workspaceFolderName(id) {
|
|
311
|
+
return String(id).replace(/[^A-Za-z0-9_-]/gu, "_");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Files and per-workspace stats for the optional "workspace folders" group. */
|
|
315
|
+
function workspaceFolderEntries(home, options = {}) {
|
|
316
|
+
const maxFiles = Number.isFinite(options.maxFiles) ? Math.max(1, Number(options.maxFiles)) : 8000;
|
|
317
|
+
const entries = [];
|
|
318
|
+
const workspaces = [];
|
|
319
|
+
let capped = false;
|
|
320
|
+
for (const workspace of workspaceDirectories(home)) {
|
|
321
|
+
let exists = false;
|
|
322
|
+
try {
|
|
323
|
+
exists = fs.statSync(workspace.path).isDirectory();
|
|
324
|
+
} catch {
|
|
325
|
+
exists = false;
|
|
326
|
+
}
|
|
327
|
+
const relative = `payload/workspace-folders/${workspaceFolderName(workspace.id)}`;
|
|
328
|
+
const files = exists ? walkFiles(workspace.path, relative, { maxFiles, excludeNames: WORKSPACE_FOLDER_EXCLUDES }) : [];
|
|
329
|
+
if (files.capped === true) capped = true;
|
|
330
|
+
entries.push(...files);
|
|
331
|
+
workspaces.push({
|
|
332
|
+
id: workspace.id,
|
|
333
|
+
title: workspace.title,
|
|
334
|
+
path: workspace.path,
|
|
335
|
+
relative,
|
|
336
|
+
exists,
|
|
337
|
+
files: files.length,
|
|
338
|
+
bytes: sizeOf(files),
|
|
339
|
+
human: humanSize(sizeOf(files)),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
return { entries, workspaces, capped };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Describe every exportable group with its real size on disk, so the UI can
|
|
347
|
+
* show what will be included before the user presses the button.
|
|
348
|
+
*/
|
|
349
|
+
export function describeSources(options = {}) {
|
|
350
|
+
const home = options.home ?? defaultHome();
|
|
351
|
+
const profileName = options.profile ?? "desktop";
|
|
352
|
+
const selection = normalizeSelection(options.selection);
|
|
353
|
+
const groups = [];
|
|
354
|
+
|
|
355
|
+
const addGroup = (id, label, description, files, available) => {
|
|
356
|
+
groups.push({ id, label, description, available, files: files.length, bytes: sizeOf(files), human: humanSize(sizeOf(files)) });
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const profileDir = profileDirectory(home, profileName);
|
|
360
|
+
const profileFiles = ["package.json", "cordis.patch.yml", "cordis.yml", "pnpm-lock.yaml", ".npmrc"]
|
|
361
|
+
.map((name) => path.join(profileDir, name))
|
|
362
|
+
.filter((filePath) => fs.existsSync(filePath))
|
|
363
|
+
.map((filePath) => {
|
|
364
|
+
const stat = fs.statSync(filePath);
|
|
365
|
+
return { source: filePath, relative: `payload/profiles/${profileName}/${path.basename(filePath)}`, size: stat.size, mtimeMs: stat.mtimeMs };
|
|
366
|
+
});
|
|
367
|
+
addGroup("profile", "Профиль", "package.json, cordis.patch.yml, cordis.yml, pnpm-lock.yaml (без node_modules)", profileFiles, fs.existsSync(profileDir));
|
|
368
|
+
|
|
369
|
+
const plugins = localPluginTargets(home, profileName);
|
|
370
|
+
const pluginFiles = plugins.flatMap((plugin) => walkFiles(plugin.target, `payload/local-plugins/${plugin.packageName}`));
|
|
371
|
+
addGroup("localPlugins", "Локальные плагины", "исходники плагинов, установленных из папок (link:)", pluginFiles, plugins.length > 0);
|
|
372
|
+
|
|
373
|
+
const workspaceStorage = path.join(home, "storages", "workspace.json");
|
|
374
|
+
const workspaceFiles = fs.existsSync(workspaceStorage)
|
|
375
|
+
? [{ source: workspaceStorage, relative: "payload/workspaces/workspace.json", size: fs.statSync(workspaceStorage).size }]
|
|
376
|
+
: [];
|
|
377
|
+
addGroup("workspaces", "Рабочие области", "реестр рабочих областей: названия, папки, закрепление и группировка чатов", workspaceFiles, workspaceFiles.length > 0);
|
|
378
|
+
|
|
379
|
+
const workspaceFolderPlan = workspaceFolderEntries(home, { maxFiles: 8000 });
|
|
380
|
+
const workspaceFolderFiles = workspaceFolderPlan.entries;
|
|
381
|
+
const workspaceFolderGroup = {
|
|
382
|
+
id: "workspaceFolders",
|
|
383
|
+
label: "Папки рабочих областей",
|
|
384
|
+
description: "код и артефакты папок рабочих областей (кроме node_modules/pgruntime/tmp) — может быть много гигабайт",
|
|
385
|
+
available: workspaceFolderPlan.workspaces.some((workspace) => workspace.exists),
|
|
386
|
+
files: workspaceFolderFiles.length,
|
|
387
|
+
bytes: sizeOf(workspaceFolderFiles),
|
|
388
|
+
human: humanSize(sizeOf(workspaceFolderFiles)),
|
|
389
|
+
capped: workspaceFolderPlan.capped,
|
|
390
|
+
workspaces: workspaceFolderPlan.workspaces,
|
|
391
|
+
};
|
|
392
|
+
groups.push(workspaceFolderGroup);
|
|
393
|
+
|
|
394
|
+
const skillsDir = path.join(home, "skills");
|
|
395
|
+
const skillFiles = walkFiles(skillsDir, "payload/skills");
|
|
396
|
+
addGroup("skills", "Навыки", "все файловые навыки (без node_modules)", skillFiles, fs.existsSync(skillsDir));
|
|
397
|
+
|
|
398
|
+
const memoryDir = path.join(home, "memory");
|
|
399
|
+
const memoryFiles = walkFiles(memoryDir, "payload/memory");
|
|
400
|
+
addGroup("memory", "Память", "memory/ — журнал сессий, досье проектов, окружение", memoryFiles, fs.existsSync(memoryDir));
|
|
401
|
+
|
|
402
|
+
const projectsMemoryDir = path.join(home, "projects-memory");
|
|
403
|
+
const projectsMemoryFiles = walkFiles(projectsMemoryDir, "payload/projects-memory", {
|
|
404
|
+
exclude: new Set(["_access.log", "_state.json"]),
|
|
405
|
+
});
|
|
406
|
+
addGroup("projectsMemory", "Память проектов", "projects-memory/ — досье и журналы проектов", projectsMemoryFiles, fs.existsSync(projectsMemoryDir));
|
|
407
|
+
|
|
408
|
+
const configFiles = ["AGENTS.md", "dream-skin.json"]
|
|
409
|
+
.map((name) => path.join(home, name))
|
|
410
|
+
.filter((filePath) => fs.existsSync(filePath))
|
|
411
|
+
.map((filePath) => {
|
|
412
|
+
const stat = fs.statSync(filePath);
|
|
413
|
+
return { source: filePath, relative: `payload/dsh/${path.basename(filePath)}`, size: stat.size, mtimeMs: stat.mtimeMs };
|
|
414
|
+
});
|
|
415
|
+
addGroup("config", "Глобальные конфиги", "AGENTS.md, dream-skin.json", configFiles, configFiles.length > 0);
|
|
416
|
+
|
|
417
|
+
const sessionsDir = path.join(home, "sessions");
|
|
418
|
+
const sessionFiles = walkFiles(sessionsDir, "payload/sessions", { maxFiles: 4000 });
|
|
419
|
+
const sessionsGroup = { id: "sessions", label: "История сессий", description: "sessions/ — может быть очень большим", available: fs.existsSync(sessionsDir), files: sessionFiles.length, bytes: sizeOf(sessionFiles), human: humanSize(sizeOf(sessionFiles)), capped: sessionFiles.capped === true };
|
|
420
|
+
groups.push(sessionsGroup);
|
|
421
|
+
|
|
422
|
+
const attachmentsDir = path.join(home, "attachments");
|
|
423
|
+
const attachmentFiles = walkFiles(attachmentsDir, "payload/attachments", { maxFiles: 2000 });
|
|
424
|
+
groups.push({ id: "attachments", label: "Вложения", description: "attachments/ — картинки и файлы из чатов", available: fs.existsSync(attachmentsDir), files: attachmentFiles.length, bytes: sizeOf(attachmentFiles), human: humanSize(sizeOf(attachmentFiles)), capped: attachmentFiles.capped === true });
|
|
425
|
+
|
|
426
|
+
const credentialsFile = path.join(home, ".credentials.yaml");
|
|
427
|
+
const credentialsFiles = fs.existsSync(credentialsFile)
|
|
428
|
+
? [{
|
|
429
|
+
source: credentialsFile,
|
|
430
|
+
relative: "payload/dsh/.credentials.yaml",
|
|
431
|
+
size: fs.statSync(credentialsFile).size,
|
|
432
|
+
mtimeMs: fs.statSync(credentialsFile).mtimeMs,
|
|
433
|
+
}]
|
|
434
|
+
: [];
|
|
435
|
+
addGroup("credentials", "Секреты", ".credentials.yaml — токены и ключи доступа (переносить только на доверенную машину)", credentialsFiles, credentialsFiles.length > 0);
|
|
436
|
+
|
|
437
|
+
return { home, profile: profileName, selection, groups };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function collectEntries(description) {
|
|
441
|
+
const selected = description.selection;
|
|
442
|
+
const map = new Map(description.groups.map((group) => [group.id, group]));
|
|
443
|
+
const entries = [];
|
|
444
|
+
const ids = ["profile", "localPlugins", "workspaces", "skills", "memory", "projectsMemory", "config"];
|
|
445
|
+
if (selected.workspaceFolders === true) ids.push("workspaceFolders");
|
|
446
|
+
if (selected.sessions) ids.push("sessions");
|
|
447
|
+
if (selected.attachments) ids.push("attachments");
|
|
448
|
+
if (selected.credentials) ids.push("credentials");
|
|
449
|
+
const activeIds = new Set(ids.filter((id) => selected[id] !== false));
|
|
450
|
+
const fileGroups = ["profile", "localPlugins", "workspaces", "workspaceFolders", "skills", "memory", "projectsMemory", "config", "sessions", "attachments", "credentials"];
|
|
451
|
+
for (const id of fileGroups) {
|
|
452
|
+
if (!activeIds.has(id)) continue;
|
|
453
|
+
const group = map.get(id);
|
|
454
|
+
if (group === undefined) continue;
|
|
455
|
+
for (const item of groupFiles(description, id)) entries.push(item);
|
|
456
|
+
}
|
|
457
|
+
return entries;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** Re-walk one group because describeSources stores summaries only. */
|
|
461
|
+
function groupFiles(description, id) {
|
|
462
|
+
const home = description.home;
|
|
463
|
+
const profileName = description.profile;
|
|
464
|
+
const profileDir = profileDirectory(home, profileName);
|
|
465
|
+
switch (id) {
|
|
466
|
+
case "profile":
|
|
467
|
+
return ["package.json", "cordis.patch.yml", "cordis.yml", "pnpm-lock.yaml", ".npmrc"]
|
|
468
|
+
.map((name) => path.join(profileDir, name))
|
|
469
|
+
.filter((filePath) => fs.existsSync(filePath))
|
|
470
|
+
.map((filePath) => ({ source: filePath, relative: `payload/profiles/${profileName}/${path.basename(filePath)}`, size: fs.statSync(filePath).size }));
|
|
471
|
+
case "localPlugins":
|
|
472
|
+
return localPluginTargets(home, profileName).flatMap((plugin) => walkFiles(plugin.target, `payload/local-plugins/${plugin.packageName}`));
|
|
473
|
+
case "workspaces":
|
|
474
|
+
return fs.existsSync(path.join(home, "storages", "workspace.json"))
|
|
475
|
+
? [{ source: path.join(home, "storages", "workspace.json"), relative: "payload/workspaces/workspace.json", size: fs.statSync(path.join(home, "storages", "workspace.json")).size }]
|
|
476
|
+
: [];
|
|
477
|
+
case "workspaceFolders":
|
|
478
|
+
return workspaceFolderEntries(home, { maxFiles: MAX_WORKSPACE_FOLDER_FILES }).entries;
|
|
479
|
+
case "skills":
|
|
480
|
+
return walkFiles(path.join(home, "skills"), "payload/skills");
|
|
481
|
+
case "memory":
|
|
482
|
+
return walkFiles(path.join(home, "memory"), "payload/memory");
|
|
483
|
+
case "projectsMemory":
|
|
484
|
+
return walkFiles(path.join(home, "projects-memory"), "payload/projects-memory", { exclude: new Set(["_access.log", "_state.json"]) });
|
|
485
|
+
case "config":
|
|
486
|
+
return ["AGENTS.md", "dream-skin.json"]
|
|
487
|
+
.map((name) => path.join(home, name))
|
|
488
|
+
.filter((filePath) => fs.existsSync(filePath))
|
|
489
|
+
.map((filePath) => ({ source: filePath, relative: `payload/dsh/${path.basename(filePath)}`, size: fs.statSync(filePath).size }));
|
|
490
|
+
case "sessions":
|
|
491
|
+
return walkFiles(path.join(home, "sessions"), "payload/sessions");
|
|
492
|
+
case "attachments":
|
|
493
|
+
return walkFiles(path.join(home, "attachments"), "payload/attachments");
|
|
494
|
+
case "credentials":
|
|
495
|
+
return fs.existsSync(path.join(home, ".credentials.yaml"))
|
|
496
|
+
? [{ source: path.join(home, ".credentials.yaml"), relative: "payload/dsh/.credentials.yaml", size: fs.statSync(path.join(home, ".credentials.yaml")).size }]
|
|
497
|
+
: [];
|
|
498
|
+
default:
|
|
499
|
+
return [];
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/* ------------------------------------------------------------------ */
|
|
504
|
+
/* export */
|
|
505
|
+
/* ------------------------------------------------------------------ */
|
|
506
|
+
|
|
507
|
+
/** Files placed at the archive root so the clean-machine bootstrap is easy to find. */
|
|
508
|
+
function bootstrapArchiveFiles() {
|
|
509
|
+
const files = [];
|
|
510
|
+
const add = (absolute, relative) => {
|
|
511
|
+
if (!fs.existsSync(absolute)) return;
|
|
512
|
+
files.push({ absolute, relative, stat: fs.statSync(absolute) });
|
|
513
|
+
};
|
|
514
|
+
// ASCII names: Windows bsdtar mis-decodes non-ASCII tar entry names.
|
|
515
|
+
add(path.join(PLUGIN_ROOT, "bootstrap", "01-INSTALL.cmd"), "01-INSTALL.cmd");
|
|
516
|
+
add(path.join(PLUGIN_ROOT, "bootstrap", "install-on-clean-machine.ps1"), "install-on-clean-machine.ps1");
|
|
517
|
+
add(path.join(PLUGIN_ROOT, "bootstrap", "README-ARCHIVE.md"), "README-INSTALL.md");
|
|
518
|
+
for (const name of BOOTSTRAP_PLUGIN_FILES) {
|
|
519
|
+
add(path.join(PLUGIN_ROOT, name), `bootstrap-plugin/dsh-profile-transfer/${name}`);
|
|
520
|
+
}
|
|
521
|
+
const localeDirectory = path.join(PLUGIN_ROOT, "locale");
|
|
522
|
+
if (fs.existsSync(localeDirectory)) {
|
|
523
|
+
for (const name of fs.readdirSync(localeDirectory)) {
|
|
524
|
+
add(path.join(localeDirectory, name), `bootstrap-plugin/dsh-profile-transfer/locale/${name}`);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return files;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export function exportBundle(options = {}) {
|
|
531
|
+
const home = options.home ?? defaultHome();
|
|
532
|
+
const profileName = options.profile ?? "desktop";
|
|
533
|
+
const selection = normalizeSelection(options.selection);
|
|
534
|
+
const description = describeSources({ home, profile: profileName, selection });
|
|
535
|
+
const entries = collectEntries(description);
|
|
536
|
+
if (entries.length === 0) throw new Error("не выбрано ни одной группы для экспорта");
|
|
537
|
+
const workspaceFolderEntriesSelected = entries.filter((entry) => entry.relative.startsWith("payload/workspace-folders/"));
|
|
538
|
+
const workspaceFolderBytes = sizeOf(workspaceFolderEntriesSelected);
|
|
539
|
+
if (workspaceFolderEntriesSelected.length > MAX_WORKSPACE_FOLDER_FILES || workspaceFolderBytes > MAX_WORKSPACE_FOLDER_BYTES) {
|
|
540
|
+
throw new Error(
|
|
541
|
+
`Папки рабочих областей слишком большие для архива: ${workspaceFolderEntriesSelected.length} файлов, ${humanSize(workspaceFolderBytes)}. ` +
|
|
542
|
+
`Лимит — ${MAX_WORKSPACE_FOLDER_FILES} файлов и ${humanSize(MAX_WORKSPACE_FOLDER_BYTES)}. Скопируйте папки проектов вручную.`,
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
const destinationDirectory = options.destinationDirectory ?? path.join(os.homedir(), "Documents", "DSH-обмены");
|
|
546
|
+
fs.mkdirSync(destinationDirectory, { recursive: true });
|
|
547
|
+
const timestamp = new Date().toISOString().replace(/[:.]/gu, "-").slice(0, 19);
|
|
548
|
+
const archivePath = options.archivePath ?? path.join(destinationDirectory, `dsh-profile-${timestamp}.tar.gz`);
|
|
549
|
+
const tar = createTarGz();
|
|
550
|
+
// Root-level bootstrap first: on a clean machine the user sees УСТАНОВИТЬ.cmd
|
|
551
|
+
// immediately after extracting the archive, without digging into payload/.
|
|
552
|
+
const bootstrapFiles = bootstrapArchiveFiles();
|
|
553
|
+
for (const file of bootstrapFiles) {
|
|
554
|
+
try {
|
|
555
|
+
const bytes = fs.readFileSync(file.absolute);
|
|
556
|
+
tar.addFile(safeRelative(file.relative), bytes, file.stat);
|
|
557
|
+
} catch {
|
|
558
|
+
/* a missing bootstrap resource must not break the data export */
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
const files = [];
|
|
562
|
+
const seen = new Set();
|
|
563
|
+
for (const entry of entries) {
|
|
564
|
+
if (seen.has(entry.relative)) continue;
|
|
565
|
+
seen.add(entry.relative);
|
|
566
|
+
let bytes;
|
|
567
|
+
try {
|
|
568
|
+
bytes = fs.readFileSync(entry.source);
|
|
569
|
+
} catch {
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
tar.addFile(safeRelative(entry.relative), bytes, fs.statSync(entry.source));
|
|
573
|
+
files.push({ path: safeRelative(entry.relative), size: bytes.length, sha256: sha256(bytes) });
|
|
574
|
+
}
|
|
575
|
+
const manifest = {
|
|
576
|
+
format: FORMAT,
|
|
577
|
+
version: FORMAT_VERSION,
|
|
578
|
+
createdAt: new Date().toISOString(),
|
|
579
|
+
host: os.hostname(),
|
|
580
|
+
platform: `${process.platform}-${process.arch}`,
|
|
581
|
+
dshVersion: process.env.DSH_CLIENT_VERSION ?? process.env.DSH_VERSION ?? "unknown",
|
|
582
|
+
profile: profileName,
|
|
583
|
+
selection,
|
|
584
|
+
totals: { files: files.length, bytes: files.reduce((total, file) => total + file.size, 0), human: humanSize(files.reduce((total, file) => total + file.size, 0)) },
|
|
585
|
+
localPlugins: localPluginTargets(home, profileName).map((plugin) => ({ packageName: plugin.packageName, spec: plugin.spec })),
|
|
586
|
+
origin: { userHome: os.homedir(), dshHome: home, platform: process.platform },
|
|
587
|
+
workspaces: (() => {
|
|
588
|
+
const workspaceJson = readJson(path.join(home, "storages", "workspace.json"));
|
|
589
|
+
const tables = workspaceJson?.tables?.workspaces;
|
|
590
|
+
if (tables === null || typeof tables !== "object") return { count: 0, titles: [] };
|
|
591
|
+
return { count: Object.keys(tables).length, titles: Object.values(tables).map((workspace) => workspace?.title).filter((title) => typeof title === "string") };
|
|
592
|
+
})(),
|
|
593
|
+
workspaceFolders: (() => {
|
|
594
|
+
const buckets = new Map();
|
|
595
|
+
for (const entry of workspaceFolderEntriesSelected) {
|
|
596
|
+
const match = entry.relative.match(/^payload\/workspace-folders\/([^/]+)\//u);
|
|
597
|
+
if (match === null) continue;
|
|
598
|
+
const bucket = buckets.get(match[1]) ?? { folder: match[1], files: 0, bytes: 0 };
|
|
599
|
+
bucket.files += 1;
|
|
600
|
+
bucket.bytes += entry.size;
|
|
601
|
+
buckets.set(match[1], bucket);
|
|
602
|
+
}
|
|
603
|
+
return workspaceDirectories(home)
|
|
604
|
+
.filter((workspace) => buckets.has(workspaceFolderName(workspace.id)))
|
|
605
|
+
.map((workspace) => {
|
|
606
|
+
const bucket = buckets.get(workspaceFolderName(workspace.id));
|
|
607
|
+
return { id: workspace.id, title: workspace.title, path: workspace.path, relative: `payload/workspace-folders/${workspaceFolderName(workspace.id)}`, files: bucket.files, bytes: bucket.bytes, human: humanSize(bucket.bytes) };
|
|
608
|
+
});
|
|
609
|
+
})(),
|
|
610
|
+
bootstrap: {
|
|
611
|
+
installer: "01-INSTALL.cmd",
|
|
612
|
+
script: "install-on-clean-machine.ps1",
|
|
613
|
+
readme: "README-INSTALL.md",
|
|
614
|
+
pluginDirectory: "bootstrap-plugin/dsh-profile-transfer",
|
|
615
|
+
files: bootstrapFiles.length,
|
|
616
|
+
},
|
|
617
|
+
files,
|
|
618
|
+
};
|
|
619
|
+
tar.addFile("manifest.json", Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, "utf8"));
|
|
620
|
+
const archive = tar.finish();
|
|
621
|
+
fs.writeFileSync(archivePath, archive);
|
|
622
|
+
return { archivePath, manifest, bytes: archive.length, human: humanSize(archive.length) };
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/* ------------------------------------------------------------------ */
|
|
626
|
+
/* import */
|
|
627
|
+
/* ------------------------------------------------------------------ */
|
|
628
|
+
|
|
629
|
+
export function inspectBundle(archivePath) {
|
|
630
|
+
const archive = fs.readFileSync(archivePath);
|
|
631
|
+
const entries = parseTarGz(archive);
|
|
632
|
+
const manifestEntry = entries.find((entry) => entry.path === "manifest.json");
|
|
633
|
+
if (manifestEntry === undefined) throw new Error("в архиве нет manifest.json — это не архив переноса DSH");
|
|
634
|
+
const manifest = JSON.parse(manifestEntry.data.toString("utf8"));
|
|
635
|
+
if (manifest.format !== FORMAT || manifest.version !== FORMAT_VERSION) throw new Error("неизвестный формат архива переноса");
|
|
636
|
+
const files = entries.filter((entry) => entry.type === "0" && entry.path !== "manifest.json");
|
|
637
|
+
return {
|
|
638
|
+
manifest,
|
|
639
|
+
files: files.map((entry) => ({ path: entry.path, size: entry.data.length })),
|
|
640
|
+
bytes: archive.length,
|
|
641
|
+
human: humanSize(archive.length),
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/** Session ids that actually exist under the sessions directory (cwd folder / session-id). */
|
|
646
|
+
function existingSessionIds(home) {
|
|
647
|
+
const ids = new Set();
|
|
648
|
+
const root = path.join(home, "sessions");
|
|
649
|
+
let directories = [];
|
|
650
|
+
try {
|
|
651
|
+
directories = fs.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory());
|
|
652
|
+
} catch {
|
|
653
|
+
return ids;
|
|
654
|
+
}
|
|
655
|
+
for (const directory of directories) {
|
|
656
|
+
if (directory.name.startsWith("session-")) {
|
|
657
|
+
ids.add(directory.name);
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
let children = [];
|
|
661
|
+
try {
|
|
662
|
+
children = fs.readdirSync(path.join(root, directory.name), { withFileTypes: true });
|
|
663
|
+
} catch {
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
for (const child of children) {
|
|
667
|
+
if (child.isDirectory() && child.name.startsWith("session-")) ids.add(child.name);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return ids;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/** Rewrite paths from the exporting machine to this machine (user home, DSH home). */
|
|
674
|
+
function remapOriginPath(value, origin, home) {
|
|
675
|
+
if (typeof value !== "string" || value.length === 0) return value;
|
|
676
|
+
const currentUserHome = os.homedir();
|
|
677
|
+
const pairs = [];
|
|
678
|
+
if (typeof origin?.userHome === "string" && origin.userHome.length > 0 && origin.userHome !== currentUserHome) {
|
|
679
|
+
pairs.push([origin.userHome, currentUserHome]);
|
|
680
|
+
}
|
|
681
|
+
if (typeof origin?.dshHome === "string" && origin.dshHome.length > 0 && origin.dshHome !== home) {
|
|
682
|
+
pairs.push([origin.dshHome, home]);
|
|
683
|
+
}
|
|
684
|
+
let result = value;
|
|
685
|
+
for (const [from, to] of pairs) {
|
|
686
|
+
const fromLower = from.toLowerCase();
|
|
687
|
+
if (result.toLowerCase() === fromLower) {
|
|
688
|
+
result = to;
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
if (result.toLowerCase().startsWith(fromLower) && /[\\/]/u.test(result[from.length] ?? "")) {
|
|
692
|
+
result = `${to}${result.slice(from.length)}`;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return result;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Restore the workspace registry (`~/.dsh/storages/workspace.json`): workspace
|
|
700
|
+
* names, folder paths, chat grouping, archived/pinned chats. The archive is
|
|
701
|
+
* merged with the local registry so an import never silently drops local state.
|
|
702
|
+
*/
|
|
703
|
+
function restoreWorkspaceRegistry(home, incoming, manifest) {
|
|
704
|
+
if (incoming === null || typeof incoming !== "object") return 0;
|
|
705
|
+
const target = path.join(home, "storages", "workspace.json");
|
|
706
|
+
const current = readJson(target);
|
|
707
|
+
const alive = existingSessionIds(home);
|
|
708
|
+
const filterIds = (list) => (Array.isArray(list) ? list.filter((id) => typeof id === "string" && (alive.size === 0 || alive.has(id))) : []);
|
|
709
|
+
const incomingWorkspaces = incoming.tables?.workspaces !== null && typeof incoming.tables?.workspaces === "object" ? incoming.tables.workspaces : {};
|
|
710
|
+
const mergedWorkspaces = { ...(current?.tables?.workspaces ?? {}) };
|
|
711
|
+
for (const [id, workspace] of Object.entries(incomingWorkspaces)) {
|
|
712
|
+
if (workspace === null || typeof workspace !== "object") continue;
|
|
713
|
+
mergedWorkspaces[id] = {
|
|
714
|
+
...workspace,
|
|
715
|
+
path: remapOriginPath(workspace.path, manifest.origin, home),
|
|
716
|
+
sessionIds: filterIds(workspace.sessionIds),
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
const currentGlobal = current?.global ?? {};
|
|
720
|
+
const incomingGlobal = incoming.global ?? {};
|
|
721
|
+
const workspaceIds = [];
|
|
722
|
+
for (const id of [...(currentGlobal.workspaceIds ?? []), ...(incomingGlobal.workspaceIds ?? []), ...Object.keys(mergedWorkspaces)]) {
|
|
723
|
+
if (typeof id === "string" && mergedWorkspaces[id] !== undefined && !workspaceIds.includes(id)) workspaceIds.push(id);
|
|
724
|
+
}
|
|
725
|
+
let defaultWorkspaceId = null;
|
|
726
|
+
if (typeof currentGlobal.defaultWorkspaceId === "string" && mergedWorkspaces[currentGlobal.defaultWorkspaceId] !== undefined) {
|
|
727
|
+
defaultWorkspaceId = currentGlobal.defaultWorkspaceId;
|
|
728
|
+
} else if (typeof incomingGlobal.defaultWorkspaceId === "string" && mergedWorkspaces[incomingGlobal.defaultWorkspaceId] !== undefined) {
|
|
729
|
+
defaultWorkspaceId = incomingGlobal.defaultWorkspaceId;
|
|
730
|
+
} else {
|
|
731
|
+
defaultWorkspaceId = workspaceIds[0] ?? null;
|
|
732
|
+
}
|
|
733
|
+
const unique = (values) => [...new Set(values)];
|
|
734
|
+
const merged = {
|
|
735
|
+
...(current ?? {}),
|
|
736
|
+
unit: incoming.unit ?? current?.unit ?? { name: "workspace", version: 2 },
|
|
737
|
+
global: {
|
|
738
|
+
...currentGlobal,
|
|
739
|
+
initialized: true,
|
|
740
|
+
defaultWorkspaceId,
|
|
741
|
+
workspaceIds,
|
|
742
|
+
archivedSessionIds: unique([...(currentGlobal.archivedSessionIds ?? []), ...filterIds(incomingGlobal.archivedSessionIds)]),
|
|
743
|
+
pinnedSessionIds: unique([...(currentGlobal.pinnedSessionIds ?? []), ...filterIds(incomingGlobal.pinnedSessionIds)]),
|
|
744
|
+
},
|
|
745
|
+
tables: {
|
|
746
|
+
...(current?.tables ?? {}),
|
|
747
|
+
workspaces: mergedWorkspaces,
|
|
748
|
+
},
|
|
749
|
+
};
|
|
750
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
751
|
+
fs.writeFileSync(target, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
|
|
752
|
+
return Object.keys(incomingWorkspaces).length;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/** Restore the optional workspace folders next to their (remapped) workspace paths. */
|
|
756
|
+
function restoreWorkspaceFolders(home, entries, manifest, warnings) {
|
|
757
|
+
const groups = new Map();
|
|
758
|
+
for (const entry of entries) {
|
|
759
|
+
const relativePath = safeRelative(entry.path);
|
|
760
|
+
const match = relativePath.match(/^payload\/workspace-folders\/([^/]+)\/(.+)$/u);
|
|
761
|
+
if (match === null) continue;
|
|
762
|
+
const [, folderName, fileRelative] = match;
|
|
763
|
+
if (!groups.has(folderName)) groups.set(folderName, []);
|
|
764
|
+
groups.get(folderName).push({ data: entry.data, fileRelative });
|
|
765
|
+
}
|
|
766
|
+
if (groups.size === 0) return 0;
|
|
767
|
+
const registry = readJson(path.join(home, "storages", "workspace.json"));
|
|
768
|
+
const targets = new Map();
|
|
769
|
+
for (const [id, workspace] of Object.entries(registry?.tables?.workspaces ?? {})) {
|
|
770
|
+
if (typeof workspace?.path === "string" && workspace.path.length > 0) targets.set(workspaceFolderName(id), workspace.path);
|
|
771
|
+
}
|
|
772
|
+
for (const workspace of manifest.workspaceFolders ?? []) {
|
|
773
|
+
if (typeof workspace?.relative !== "string" || typeof workspace?.path !== "string") continue;
|
|
774
|
+
const folderName = workspace.relative.replace("payload/workspace-folders/", "");
|
|
775
|
+
if (!targets.has(folderName)) targets.set(folderName, remapOriginPath(workspace.path, manifest.origin, home));
|
|
776
|
+
}
|
|
777
|
+
const stamp = new Date().toISOString().replace(/[:.]/gu, "-").slice(0, 19);
|
|
778
|
+
let restored = 0;
|
|
779
|
+
for (const [folderName, files] of groups.entries()) {
|
|
780
|
+
const target = targets.get(folderName);
|
|
781
|
+
if (typeof target !== "string" || target.length === 0) {
|
|
782
|
+
warnings.push(`папка рабочей области ${folderName} пропущена: не найден целевой путь`);
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
let targetRoot = target;
|
|
786
|
+
let isEmpty = false;
|
|
787
|
+
try {
|
|
788
|
+
isEmpty = fs.statSync(targetRoot).isDirectory() && fs.readdirSync(targetRoot).length === 0;
|
|
789
|
+
} catch {
|
|
790
|
+
isEmpty = false;
|
|
791
|
+
}
|
|
792
|
+
if (!isEmpty && fs.existsSync(targetRoot)) {
|
|
793
|
+
targetRoot = `${target}-dsh-restored-${stamp}`;
|
|
794
|
+
warnings.push(`папка ${target} уже существует — восстановлено рядом: ${targetRoot}`);
|
|
795
|
+
}
|
|
796
|
+
for (const file of files) {
|
|
797
|
+
const resolved = path.resolve(targetRoot, file.fileRelative);
|
|
798
|
+
const rootResolved = path.resolve(targetRoot);
|
|
799
|
+
if (!resolved.startsWith(rootResolved + path.sep)) {
|
|
800
|
+
warnings.push(`пропущен небезопасный путь папки рабочей области: ${file.fileRelative}`);
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
try {
|
|
804
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
805
|
+
fs.writeFileSync(resolved, file.data);
|
|
806
|
+
} catch (error) {
|
|
807
|
+
warnings.push(`не удалось записать ${file.fileRelative}: ${String(error?.message ?? error)}`);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
restored += 1;
|
|
811
|
+
}
|
|
812
|
+
return restored;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function backupCurrent(home, profileName, selection) {
|
|
816
|
+
const stamp = new Date().toISOString().replace(/[:.]/gu, "-").slice(0, 19);
|
|
817
|
+
const backupDir = path.join(home, `backup-import-${stamp}`);
|
|
818
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
819
|
+
const copyInto = (source, relative) => {
|
|
820
|
+
if (!fs.existsSync(source)) return;
|
|
821
|
+
const target = path.join(backupDir, relative);
|
|
822
|
+
fs.cpSync(source, target, { recursive: true, filter: (item) => !EXCLUDED_NAMES.has(path.basename(item)) });
|
|
823
|
+
};
|
|
824
|
+
copyInto(path.join(home, "profiles", profileName), `profiles/${profileName}`);
|
|
825
|
+
if (selection.workspaces !== false) copyInto(path.join(home, "storages", "workspace.json"), "storages/workspace.json");
|
|
826
|
+
if (selection.skills !== false) copyInto(path.join(home, "skills"), "skills");
|
|
827
|
+
if (selection.memory !== false) copyInto(path.join(home, "memory"), "memory");
|
|
828
|
+
if (selection.projectsMemory !== false) copyInto(path.join(home, "projects-memory"), "projects-memory");
|
|
829
|
+
copyInto(path.join(home, "AGENTS.md"), "AGENTS.md");
|
|
830
|
+
return backupDir;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
export function applyBundle(options = {}) {
|
|
834
|
+
const home = options.home ?? defaultHome();
|
|
835
|
+
const profileName = options.profile ?? "desktop";
|
|
836
|
+
const archivePath = options.archivePath;
|
|
837
|
+
if (typeof archivePath !== "string" || !fs.existsSync(archivePath)) throw new Error(`архив не найден: ${archivePath}`);
|
|
838
|
+
const inspected = inspectBundle(archivePath);
|
|
839
|
+
const selection = normalizeSelection(options.selection ?? inspected.manifest.selection);
|
|
840
|
+
const entries = parseTarGz(fs.readFileSync(archivePath));
|
|
841
|
+
const warnings = [];
|
|
842
|
+
const restored = { profile: 0, localPlugins: 0, workspaces: 0, workspaceFolders: 0, skills: 0, memory: 0, projectsMemory: 0, config: 0, sessions: 0, attachments: 0, credentials: 0 };
|
|
843
|
+
let backupDir;
|
|
844
|
+
if (options.backup !== false) backupDir = backupCurrent(home, profileName, selection);
|
|
845
|
+
|
|
846
|
+
const writeEntry = (relativePath, data) => {
|
|
847
|
+
const target = path.join(home, relativePath);
|
|
848
|
+
const resolved = path.resolve(target);
|
|
849
|
+
if (!resolved.startsWith(path.resolve(home) + path.sep)) throw new Error(`небезопасный путь распаковки: ${relativePath}`);
|
|
850
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
851
|
+
fs.writeFileSync(resolved, data);
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
const skipSelection = (relativePath) => {
|
|
855
|
+
if (relativePath.startsWith("payload/workspaces/") && selection.workspaces === false) return true;
|
|
856
|
+
if (relativePath.startsWith("payload/workspace-folders/") && selection.workspaceFolders !== true) return true;
|
|
857
|
+
if (relativePath.startsWith("payload/skills/") && selection.skills === false) return true;
|
|
858
|
+
if (relativePath.startsWith("payload/memory/") && selection.memory === false) return true;
|
|
859
|
+
if (relativePath.startsWith("payload/projects-memory/") && selection.projectsMemory === false) return true;
|
|
860
|
+
if (relativePath.startsWith("payload/local-plugins/") && selection.localPlugins === false) return true;
|
|
861
|
+
if (relativePath.startsWith("payload/sessions/") && selection.sessions !== true) return true;
|
|
862
|
+
if (relativePath.startsWith("payload/attachments/") && selection.attachments !== true) return true;
|
|
863
|
+
if (relativePath.endsWith(".credentials.yaml") && selection.credentials !== true) return true;
|
|
864
|
+
return false;
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
const localPluginRoot = path.join(home, "local-plugins");
|
|
868
|
+
for (const entry of entries) {
|
|
869
|
+
if (entry.type !== "0" || entry.path === "manifest.json") continue;
|
|
870
|
+
const relativePath = safeRelative(entry.path);
|
|
871
|
+
if (skipSelection(relativePath)) continue;
|
|
872
|
+
if (relativePath.startsWith("payload/workspaces/")) continue;
|
|
873
|
+
if (relativePath.startsWith("payload/workspace-folders/")) continue;
|
|
874
|
+
if (relativePath === "01-INSTALL.cmd" || relativePath === "install-on-clean-machine.ps1" || relativePath === "README-INSTALL.md" || relativePath.startsWith("bootstrap-plugin/")) continue;
|
|
875
|
+
if (relativePath.startsWith("payload/dsh/")) {
|
|
876
|
+
const name = relativePath.slice("payload/dsh/".length);
|
|
877
|
+
if (name === ".credentials.yaml") {
|
|
878
|
+
writeEntry(path.join(".", name), entry.data);
|
|
879
|
+
restored.credentials += 1;
|
|
880
|
+
} else {
|
|
881
|
+
writeEntry(name, entry.data);
|
|
882
|
+
restored.config += 1;
|
|
883
|
+
}
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
if (relativePath.startsWith("payload/profiles/")) {
|
|
887
|
+
const name = relativePath.slice(`payload/profiles/${profileName}/`.length);
|
|
888
|
+
if (name === relativePath) continue;
|
|
889
|
+
writeEntry(path.join("profiles", profileName, name), entry.data);
|
|
890
|
+
restored.profile += 1;
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
if (relativePath.startsWith("payload/skills/")) {
|
|
894
|
+
writeEntry(path.join("skills", relativePath.slice("payload/skills/".length)), entry.data);
|
|
895
|
+
restored.skills += 1;
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
if (relativePath.startsWith("payload/memory/")) {
|
|
899
|
+
writeEntry(path.join("memory", relativePath.slice("payload/memory/".length)), entry.data);
|
|
900
|
+
restored.memory += 1;
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
if (relativePath.startsWith("payload/projects-memory/")) {
|
|
904
|
+
writeEntry(path.join("projects-memory", relativePath.slice("payload/projects-memory/".length)), entry.data);
|
|
905
|
+
restored.projectsMemory += 1;
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
if (relativePath.startsWith("payload/local-plugins/")) {
|
|
909
|
+
const rest = relativePath.slice("payload/local-plugins/".length);
|
|
910
|
+
const [packageName, ...segments] = rest.split("/");
|
|
911
|
+
if (!packageName || segments.length === 0) continue;
|
|
912
|
+
if (!isSafePackageName(packageName)) {
|
|
913
|
+
warnings.push(`пропущен небезопасный пакет: ${packageName}`);
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
writeEntry(path.join("local-plugins", packageName, ...segments), entry.data);
|
|
917
|
+
restored.localPlugins += 1;
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
if (relativePath.startsWith("payload/sessions/")) {
|
|
921
|
+
writeEntry(path.join("sessions", relativePath.slice("payload/sessions/".length)), entry.data);
|
|
922
|
+
restored.sessions += 1;
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
if (relativePath.startsWith("payload/attachments/")) {
|
|
926
|
+
writeEntry(path.join("attachments", relativePath.slice("payload/attachments/".length)), entry.data);
|
|
927
|
+
restored.attachments += 1;
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
warnings.push(`пропущен неизвестный путь: ${relativePath}`);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// Workspace registry: restore names, folder paths and chat grouping; merge
|
|
934
|
+
// with the local registry and drop references to sessions not present here.
|
|
935
|
+
const workspaceEntry = entries.find((entry) => entry.type === "0" && safeRelative(entry.path) === "payload/workspaces/workspace.json");
|
|
936
|
+
if (workspaceEntry !== undefined && selection.workspaces !== false) {
|
|
937
|
+
try {
|
|
938
|
+
restored.workspaces = restoreWorkspaceRegistry(home, JSON.parse(workspaceEntry.data.toString("utf8")), inspected.manifest);
|
|
939
|
+
} catch (error) {
|
|
940
|
+
warnings.push(`рабочие области не восстановлены: ${String(error?.message ?? error)}`);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Optional workspace folders: restored next to their (remapped) workspace paths.
|
|
945
|
+
if (selection.workspaceFolders === true) {
|
|
946
|
+
const folderEntries = entries.filter((entry) => entry.type === "0" && safeRelative(entry.path).startsWith("payload/workspace-folders/"));
|
|
947
|
+
if (folderEntries.length > 0) {
|
|
948
|
+
try {
|
|
949
|
+
restored.workspaceFolders = restoreWorkspaceFolders(home, folderEntries, inspected.manifest, warnings);
|
|
950
|
+
} catch (error) {
|
|
951
|
+
warnings.push(`папки рабочих областей не восстановлены: ${String(error?.message ?? error)}`);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Rewrite link: dependencies so they point to the restored local plugin folder.
|
|
957
|
+
let needsPnpm = false;
|
|
958
|
+
const profileJsonPath = path.join(home, "profiles", profileName, "package.json");
|
|
959
|
+
if (fs.existsSync(profileJsonPath) && restored.localPlugins > 0) {
|
|
960
|
+
const manifest = readJson(profileJsonPath);
|
|
961
|
+
if (manifest !== undefined && manifest.dependencies !== null && typeof manifest.dependencies === "object") {
|
|
962
|
+
let touched = false;
|
|
963
|
+
for (const plugin of inspected.manifest.localPlugins ?? []) {
|
|
964
|
+
if (!isSafePackageName(plugin.packageName)) {
|
|
965
|
+
warnings.push(`небезопасное имя пакета в манифесте: ${plugin.packageName}`);
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
const restoredDir = path.join(localPluginRoot, plugin.packageName);
|
|
969
|
+
if (!fs.existsSync(restoredDir)) continue;
|
|
970
|
+
const spec = `link:${restoredDir.replaceAll("\\", "/")}`;
|
|
971
|
+
if (manifest.dependencies[plugin.packageName] !== spec) {
|
|
972
|
+
manifest.dependencies[plugin.packageName] = spec;
|
|
973
|
+
touched = true;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
if (touched) {
|
|
977
|
+
fs.writeFileSync(profileJsonPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
978
|
+
needsPnpm = true;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
const nodeModules = path.join(home, "profiles", profileName, "node_modules");
|
|
983
|
+
if (!fs.existsSync(nodeModules)) needsPnpm = true;
|
|
984
|
+
|
|
985
|
+
const summaryPath = path.join(home, "profile-transfer-last.json");
|
|
986
|
+
fs.writeFileSync(summaryPath, `${JSON.stringify({
|
|
987
|
+
appliedAt: new Date().toISOString(),
|
|
988
|
+
archive: archivePath,
|
|
989
|
+
manifest: { createdAt: inspected.manifest.createdAt, host: inspected.manifest.host, totals: inspected.manifest.totals },
|
|
990
|
+
selection,
|
|
991
|
+
restored,
|
|
992
|
+
backupDir,
|
|
993
|
+
needsPnpm,
|
|
994
|
+
warnings,
|
|
995
|
+
}, null, 2)}\n`, "utf8");
|
|
996
|
+
|
|
997
|
+
return { restored, backupDir, needsPnpm, warnings, summaryPath, manifest: inspected.manifest, selection };
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/* ------------------------------------------------------------------ */
|
|
1001
|
+
/* small public helpers */
|
|
1002
|
+
/* ------------------------------------------------------------------ */
|
|
1003
|
+
|
|
1004
|
+
export { humanSize, profileDirectory, localPluginTargets, readJson, walkFiles };
|