dsh-desktop-kit 2.1.4 → 2.2.1
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/CHANGELOG.md +52 -0
- package/README.md +26 -12
- package/client.js +16 -1
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/COMPATIBILITY.md +10 -2
- package/docs/INSTALL.md +2 -2
- package/docs/PRIVACY.md +2 -0
- package/docs/TEST-0.2.2.md +8 -1
- package/docs/TOKENS.md +1 -1
- package/docs/UPGRADING.md +70 -0
- package/docs/images/hero.png +0 -0
- package/doctor.ps1 +122 -0
- package/index.js +120 -16
- package/install.ps1 +99 -0
- package/lib/skills-sync.js +245 -0
- package/package.json +10 -3
- package/skills/manifest.json +132 -0
- package/skills-baseline.json +86 -0
- package/uninstall.ps1 +74 -0
package/index.js
CHANGED
|
@@ -15,6 +15,13 @@ import fs from "node:fs";
|
|
|
15
15
|
import os from "node:os";
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
|
+
import {
|
|
19
|
+
readManifest,
|
|
20
|
+
readBaseline,
|
|
21
|
+
syncSkills,
|
|
22
|
+
loadSkillState,
|
|
23
|
+
STATE_FILE,
|
|
24
|
+
} from "./lib/skills-sync.js";
|
|
18
25
|
|
|
19
26
|
import * as gkbRuUi from "./plugins/dsh-gkb-ru-ui/lib/index.js";
|
|
20
27
|
import * as peakGuard from "./plugins/dsh-peak-guard/index.js";
|
|
@@ -25,6 +32,13 @@ import * as tokenLab from "./plugins/dsh-token-lab/lib/index.js";
|
|
|
25
32
|
import * as skillAutopilot from "./plugins/dsh-skill-autopilot/index.js";
|
|
26
33
|
|
|
27
34
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
const KIT_VERSION = (() => {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(fs.readFileSync(path.join(here, "package.json"), "utf8")).version ?? "unknown";
|
|
38
|
+
} catch {
|
|
39
|
+
return "unknown";
|
|
40
|
+
}
|
|
41
|
+
})();
|
|
28
42
|
|
|
29
43
|
const parts = [
|
|
30
44
|
["dsh-gkb-ru-ui", gkbRuUi],
|
|
@@ -39,32 +53,73 @@ const parts = [
|
|
|
39
53
|
/** The kit itself needs no service; each child declares its own. */
|
|
40
54
|
export const inject = [];
|
|
41
55
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
56
|
+
const runtimeState = {
|
|
57
|
+
kitVersion: KIT_VERSION,
|
|
58
|
+
startedAt: new Date().toISOString(),
|
|
59
|
+
skills: null,
|
|
60
|
+
mountErrors: [],
|
|
61
|
+
mounted: [],
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
function skillsHome() {
|
|
65
|
+
return process.env.DSH_HOME || path.join(os.homedir(), ".dsh");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Synchronise bundled skills without ever overwriting a user edit.
|
|
70
|
+
* Unchanged skills update; modified skills stay and are reported.
|
|
71
|
+
*/
|
|
72
|
+
export function installSkills(logger, options = {}) {
|
|
73
|
+
const home = skillsHome();
|
|
45
74
|
const source = path.join(here, "skills");
|
|
46
75
|
const target = path.join(home, "skills");
|
|
47
|
-
if (!fs.existsSync(source)) return
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
76
|
+
if (!fs.existsSync(source)) return null;
|
|
77
|
+
const manifest = readManifest(here);
|
|
78
|
+
const baseline = readBaseline(here);
|
|
79
|
+
const result = syncSkills({
|
|
80
|
+
source,
|
|
81
|
+
target,
|
|
82
|
+
kitVersion: KIT_VERSION,
|
|
83
|
+
manifest,
|
|
84
|
+
baseline,
|
|
85
|
+
force: options.force === true,
|
|
86
|
+
logger,
|
|
87
|
+
});
|
|
88
|
+
runtimeState.skills = result;
|
|
89
|
+
if (result.conflicts.length > 0) {
|
|
90
|
+
logger?.warn?.(`[dsh-desktop-kit] local edits kept for: ${result.conflicts.join(", ")} (see /kit conflicts)`);
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function kitStatusText() {
|
|
96
|
+
const home = skillsHome();
|
|
97
|
+
const state = loadSkillState(path.join(home, "skills"));
|
|
98
|
+
const skills = runtimeState.skills;
|
|
99
|
+
const tracked = Object.keys(state.skills).length;
|
|
100
|
+
const conflicts = Object.keys(state.conflicts).length;
|
|
101
|
+
const lines = [
|
|
102
|
+
`DSH Desktop Kit ${KIT_VERSION}`,
|
|
103
|
+
`Плагины: ${runtimeState.mounted.length}/${parts.length} смонтированы${runtimeState.mountErrors.length > 0 ? `, ошибок: ${runtimeState.mountErrors.length}` : ""}`,
|
|
104
|
+
`Навыки: пакетных ${tracked} · конфликтов ${conflicts} · состояние ${state.updatedAt || "ещё не записано"}`,
|
|
105
|
+
];
|
|
106
|
+
if (skills) {
|
|
107
|
+
lines.push(`Последняя синхронизация: +${skills.added.length} ~${skills.updated.length} · конфликтов: ${skills.conflicts.length}`);
|
|
108
|
+
if (skills.conflicts.length > 0) lines.push(`Конфликты (сохранены ваши правки): ${skills.conflicts.join(", ")}`);
|
|
56
109
|
}
|
|
57
|
-
if (
|
|
58
|
-
|
|
110
|
+
if (runtimeState.mountErrors.length > 0) {
|
|
111
|
+
for (const error of runtimeState.mountErrors.slice(0, 5)) lines.push(`Ошибка ${error.name}: ${error.message}`);
|
|
59
112
|
}
|
|
60
|
-
|
|
113
|
+
lines.push(`Состояние: ${path.join(home, "skills", STATE_FILE)}`);
|
|
114
|
+
lines.push("Команды: /kit status | sync | conflicts | force (перезаписать правки)");
|
|
115
|
+
return lines.join("\n");
|
|
61
116
|
}
|
|
62
117
|
|
|
63
118
|
export function apply(ctx) {
|
|
64
119
|
try {
|
|
65
120
|
installSkills(ctx.logger);
|
|
66
121
|
} catch (error) {
|
|
67
|
-
ctx.logger?.warn?.(`[dsh-desktop-kit] skill
|
|
122
|
+
ctx.logger?.warn?.(`[dsh-desktop-kit] skill sync skipped: ${String(error)}`);
|
|
68
123
|
}
|
|
69
124
|
|
|
70
125
|
for (const [name, namespace] of parts) {
|
|
@@ -75,10 +130,59 @@ export function apply(ctx) {
|
|
|
75
130
|
} else if (typeof plugin.apply === "function") {
|
|
76
131
|
plugin.apply(ctx);
|
|
77
132
|
}
|
|
133
|
+
runtimeState.mounted.push(name);
|
|
78
134
|
ctx.logger?.debug?.(`[dsh-desktop-kit] mounted ${name}`);
|
|
79
135
|
} catch (error) {
|
|
136
|
+
runtimeState.mountErrors.push({ name, message: error instanceof Error ? error.message : String(error) });
|
|
80
137
|
ctx.logger?.warn?.(`[dsh-desktop-kit] ${name} failed to mount: ${String(error)}`);
|
|
81
138
|
console.error(`[dsh-desktop-kit] ${name} failed to mount`, error);
|
|
82
139
|
}
|
|
83
140
|
}
|
|
141
|
+
|
|
142
|
+
/* Optional diagnostics command: useful after a DSH/package update. */
|
|
143
|
+
try {
|
|
144
|
+
ctx.inject(["commands"], (commandCtx) => {
|
|
145
|
+
try {
|
|
146
|
+
const dispose = commandCtx.commands.register({
|
|
147
|
+
name: "kit",
|
|
148
|
+
description: "DSH Desktop Kit: состояние, синхронизация навыков, конфликты",
|
|
149
|
+
input: { hint: "status | sync | conflicts | force" },
|
|
150
|
+
recordInput: false,
|
|
151
|
+
handler: ({ rawInput }) => {
|
|
152
|
+
const command = String(rawInput ?? "").trim().toLowerCase();
|
|
153
|
+
try {
|
|
154
|
+
if (command === "sync" || command === "force") {
|
|
155
|
+
const force = command === "force";
|
|
156
|
+
const result = installSkills(ctx.logger, { force });
|
|
157
|
+
return {
|
|
158
|
+
kind: "success",
|
|
159
|
+
text: result
|
|
160
|
+
? `${force ? "Принудительная синхронизация" : "Синхронизация"}: добавлено ${result.added.length}, обновлено ${result.updated.length}, конфликтов ${result.conflicts.length}.\n${kitStatusText()}`
|
|
161
|
+
: "Каталог навыков пакета не найден.",
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (command === "conflicts") {
|
|
165
|
+
const skills = runtimeState.skills;
|
|
166
|
+
if (!skills || skills.conflicts.length === 0) return { kind: "success", text: "Конфликтов нет: все навыки принадлежат пакету." };
|
|
167
|
+
const lines = skills.conflicts.map((name) => {
|
|
168
|
+
const conflict = skills.state?.conflicts?.[name];
|
|
169
|
+
return `- ${name}: ${conflict?.reason ?? "изменён пользователем"} — ваша версия сохранена`;
|
|
170
|
+
});
|
|
171
|
+
return { kind: "success", text: `Конфликты (${lines.length}):\n${lines.join("\n")}\nОбновить принудительно: /kit force (ваши версии уйдут в бэкап)"` };
|
|
172
|
+
}
|
|
173
|
+
return { kind: "success", text: kitStatusText() };
|
|
174
|
+
} catch (error) {
|
|
175
|
+
return { kind: "error", text: `DSH Desktop Kit: ${error instanceof Error ? error.message : String(error)}` };
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
ctx.effect(() => dispose, "dsh-desktop-kit: /kit command");
|
|
180
|
+
} catch (error) {
|
|
181
|
+
ctx.logger?.warn?.(`[dsh-desktop-kit] /kit command skipped: ${String(error)}`);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
} catch (error) {
|
|
185
|
+
ctx.logger?.debug?.(`[dsh-desktop-kit] commands service unavailable: ${String(error)}`);
|
|
186
|
+
}
|
|
84
187
|
}
|
|
188
|
+
|
package/install.ps1
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#requires -Version 5.1
|
|
2
|
+
<#
|
|
3
|
+
.SYNOPSIS
|
|
4
|
+
Install dsh-desktop-kit into a DeepSeek Harness profile.
|
|
5
|
+
|
|
6
|
+
.DESCRIPTION
|
|
7
|
+
Copies nothing into the profile outside pnpm's own dependency install:
|
|
8
|
+
the kit is linked from this repository, the bundle is added to
|
|
9
|
+
dsh.profile.bundles, and the DSH launcher is asked to run pnpm.
|
|
10
|
+
Skills are copied by the kit itself on its first start.
|
|
11
|
+
|
|
12
|
+
.PARAMETER Profile
|
|
13
|
+
DSH profile name, default "desktop".
|
|
14
|
+
|
|
15
|
+
.PARAMETER DshHome
|
|
16
|
+
DSH home; defaults to $env:DSH_HOME or %USERPROFILE%\.dsh.
|
|
17
|
+
|
|
18
|
+
.PARAMETER RepoPath
|
|
19
|
+
Repository root; defaults to this script's folder.
|
|
20
|
+
#>
|
|
21
|
+
param(
|
|
22
|
+
[string]$Profile = "desktop",
|
|
23
|
+
[string]$DshHome = "",
|
|
24
|
+
[string]$RepoPath = ""
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
$ErrorActionPreference = "Stop"
|
|
28
|
+
|
|
29
|
+
function Write-Step([string]$Text) {
|
|
30
|
+
Write-Host ""
|
|
31
|
+
Write-Host "==> $Text" -ForegroundColor Cyan
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if ([string]::IsNullOrWhiteSpace($RepoPath)) { $RepoPath = $PSScriptRoot }
|
|
35
|
+
if (-not (Test-Path (Join-Path $RepoPath "package.json"))) {
|
|
36
|
+
throw "package.json not found in $RepoPath"
|
|
37
|
+
}
|
|
38
|
+
if ([string]::IsNullOrWhiteSpace($DshHome)) {
|
|
39
|
+
if ($env:DSH_HOME) { $DshHome = $env:DSH_HOME } else { $DshHome = Join-Path $env:USERPROFILE ".dsh" }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
$profileDir = Join-Path $DshHome "profiles\$Profile"
|
|
43
|
+
$profileJson = Join-Path $profileDir "package.json"
|
|
44
|
+
|
|
45
|
+
Write-Step "Checking the DSH environment"
|
|
46
|
+
$dsh = $null
|
|
47
|
+
$command = Get-Command dsh -ErrorAction SilentlyContinue
|
|
48
|
+
if ($command) { $dsh = $command.Source }
|
|
49
|
+
if (-not $dsh) {
|
|
50
|
+
$fallback = Join-Path ${env:LOCALAPPDATA} "Programs\DeepSeek Harness\resources\runtime\cli\bin\dsh.cmd"
|
|
51
|
+
if (Test-Path $fallback) { $dsh = $fallback }
|
|
52
|
+
}
|
|
53
|
+
if (-not $dsh) { throw "dsh CLI not found. Install DeepSeek Harness, run it once, then retry." }
|
|
54
|
+
if (-not (Test-Path $profileJson)) {
|
|
55
|
+
throw "Profile '$Profile' does not exist ($profileDir). Start DSH once, close it, then retry."
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
Write-Host "dsh: $dsh"
|
|
59
|
+
Write-Host "DSH home: $DshHome"
|
|
60
|
+
Write-Host "Profile: $Profile"
|
|
61
|
+
Write-Host "Repository: $RepoPath"
|
|
62
|
+
|
|
63
|
+
$running = Get-Process -Name "DeepSeek Harness" -ErrorAction SilentlyContinue
|
|
64
|
+
if ($running) {
|
|
65
|
+
Write-Host "DSH is running. Close it before installing so the profile is not written twice." -ForegroundColor Yellow
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
Write-Step "Backing up the profile manifest"
|
|
69
|
+
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
|
70
|
+
$backupDir = Join-Path $profileDir ("backup-kit-" + $stamp)
|
|
71
|
+
New-Item -ItemType Directory -Path $backupDir -Force | Out-Null
|
|
72
|
+
Copy-Item $profileJson (Join-Path $backupDir "package.json") -Force
|
|
73
|
+
$patchFile = Join-Path $profileDir "cordis.patch.yml"
|
|
74
|
+
if (Test-Path $patchFile) { Copy-Item $patchFile (Join-Path $backupDir "cordis.patch.yml") -Force }
|
|
75
|
+
Write-Host "Backup: $backupDir"
|
|
76
|
+
|
|
77
|
+
Write-Step "Installing the package through the DSH launcher (pnpm)"
|
|
78
|
+
& $dsh plugin --profile $Profile add $RepoPath
|
|
79
|
+
if ($LASTEXITCODE -ne 0) {
|
|
80
|
+
throw "dsh plugin add exited with code $LASTEXITCODE. See the output above."
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
Write-Step "Enabling the bundle in the profile manifest"
|
|
84
|
+
$json = Get-Content -Path $profileJson -Raw | ConvertFrom-Json
|
|
85
|
+
if ($null -eq $json.dsh) { $json | Add-Member -NotePropertyName dsh -NotePropertyValue ([pscustomobject]@{}) }
|
|
86
|
+
if ($null -eq $json.dsh.profile) { $json.dsh | Add-Member -NotePropertyName profile -NotePropertyValue ([pscustomobject]@{}) }
|
|
87
|
+
$bundles = @()
|
|
88
|
+
if ($json.dsh.profile.bundles) { $bundles = @($json.dsh.profile.bundles) }
|
|
89
|
+
if ($bundles -notcontains "dsh-desktop-kit") { $bundles += "dsh-desktop-kit" }
|
|
90
|
+
$json.dsh.profile | Add-Member -NotePropertyName bundles -NotePropertyValue $bundles -Force
|
|
91
|
+
$jsonText = $json | ConvertTo-Json -Depth 20
|
|
92
|
+
[System.IO.File]::WriteAllText($profileJson, $jsonText, (New-Object System.Text.UTF8Encoding($false)))
|
|
93
|
+
|
|
94
|
+
Write-Step "Done"
|
|
95
|
+
Write-Host "1. Start DeepSeek Harness (restart it if it was running)." -ForegroundColor Green
|
|
96
|
+
Write-Host "2. Settings -> Plugins: 'dsh-desktop-kit' is enabled." -ForegroundColor Green
|
|
97
|
+
Write-Host "3. The kit copies missing skills into $DshHome\skills on first start." -ForegroundColor Green
|
|
98
|
+
Write-Host ""
|
|
99
|
+
Write-Host "Rollback: run uninstall.ps1, or restore package.json from $backupDir." -ForegroundColor DarkGray
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update-safe skill synchronisation.
|
|
3
|
+
*
|
|
4
|
+
* Rules:
|
|
5
|
+
* - a missing skill is installed;
|
|
6
|
+
* - a skill that still matches the hash recorded at the last sync is updated
|
|
7
|
+
* (with a backup);
|
|
8
|
+
* - a skill the user edited is never overwritten — it is reported as a
|
|
9
|
+
* conflict and stays untouched;
|
|
10
|
+
* - an installation without a state file is adopted only when its content
|
|
11
|
+
* matches a known baseline hash; otherwise it is treated as user-owned.
|
|
12
|
+
*
|
|
13
|
+
* The state file lives next to the skills and is written atomically.
|
|
14
|
+
*/
|
|
15
|
+
import crypto from "node:crypto";
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
|
|
19
|
+
export const SKILLS_SYNC_SCHEMA = 1;
|
|
20
|
+
export const STATE_FILE = ".dsh-kit-skills.json";
|
|
21
|
+
export const BACKUP_DIR = ".dsh-kit-backups";
|
|
22
|
+
const SKIP_FILES = new Set([".DS_Store", "Thumbs.db", STATE_FILE]);
|
|
23
|
+
|
|
24
|
+
function sha256(data) {
|
|
25
|
+
return crypto.createHash("sha256").update(data).digest("hex");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function hashText(text) {
|
|
29
|
+
return sha256(String(text ?? ""));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Deterministic directory hash: sorted relative paths + bytes. */
|
|
33
|
+
export function hashSkillDir(directory) {
|
|
34
|
+
const hash = crypto.createHash("sha256");
|
|
35
|
+
let files = 0;
|
|
36
|
+
let bytes = 0;
|
|
37
|
+
const walk = (current, relative) => {
|
|
38
|
+
let entries;
|
|
39
|
+
try {
|
|
40
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
41
|
+
} catch {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
entries.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (SKIP_FILES.has(entry.name)) continue;
|
|
47
|
+
const full = path.join(current, entry.name);
|
|
48
|
+
const rel = relative.length > 0 ? `${relative}/${entry.name}` : entry.name;
|
|
49
|
+
if (entry.isDirectory()) {
|
|
50
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
51
|
+
walk(full, rel);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (!entry.isFile()) continue;
|
|
55
|
+
try {
|
|
56
|
+
const content = fs.readFileSync(full);
|
|
57
|
+
hash.update(rel);
|
|
58
|
+
hash.update("\0");
|
|
59
|
+
hash.update(String(content.length));
|
|
60
|
+
hash.update("\0");
|
|
61
|
+
hash.update(content);
|
|
62
|
+
files += 1;
|
|
63
|
+
bytes += content.length;
|
|
64
|
+
} catch {
|
|
65
|
+
/* unreadable file: ignore, the hash stays stable */
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
walk(directory, "");
|
|
70
|
+
return { hash: hash.digest("hex"), files, bytes };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function readManifest(packageRoot) {
|
|
74
|
+
try {
|
|
75
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(packageRoot, "skills", "manifest.json"), "utf8"));
|
|
76
|
+
return parsed && parsed.schema === SKILLS_SYNC_SCHEMA && parsed.skills ? parsed : null;
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function readBaseline(packageRoot) {
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(packageRoot, "skills-baseline.json"), "utf8"));
|
|
85
|
+
return parsed && parsed.versions ? parsed.versions : {};
|
|
86
|
+
} catch {
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function loadSkillState(target) {
|
|
92
|
+
try {
|
|
93
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(target, STATE_FILE), "utf8"));
|
|
94
|
+
if (!parsed || parsed.schema !== SKILLS_SYNC_SCHEMA) return emptyState();
|
|
95
|
+
return {
|
|
96
|
+
...emptyState(),
|
|
97
|
+
...parsed,
|
|
98
|
+
skills: parsed.skills && typeof parsed.skills === "object" ? parsed.skills : {},
|
|
99
|
+
conflicts: parsed.conflicts && typeof parsed.conflicts === "object" ? parsed.conflicts : {},
|
|
100
|
+
};
|
|
101
|
+
} catch {
|
|
102
|
+
return emptyState();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function emptyState() {
|
|
107
|
+
return { schema: SKILLS_SYNC_SCHEMA, kitVersion: "", updatedAt: "", skills: {}, conflicts: {} };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function writeAtomic(file, text) {
|
|
111
|
+
const temp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
112
|
+
fs.writeFileSync(temp, text, "utf8");
|
|
113
|
+
fs.renameSync(temp, file);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function saveSkillState(target, state) {
|
|
117
|
+
try {
|
|
118
|
+
fs.mkdirSync(target, { recursive: true });
|
|
119
|
+
writeAtomic(path.join(target, STATE_FILE), `${JSON.stringify(state, null, 2)}\n`);
|
|
120
|
+
} catch {
|
|
121
|
+
/* state is an optimization; a failed write must not break skills */
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function copySkill(source, destination) {
|
|
126
|
+
const parent = path.dirname(destination);
|
|
127
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
128
|
+
const temp = path.join(parent, `.${path.basename(destination)}.kit-tmp-${process.pid}-${Date.now()}`);
|
|
129
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
130
|
+
try {
|
|
131
|
+
fs.cpSync(source, temp, { recursive: true });
|
|
132
|
+
} catch (error) {
|
|
133
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
return temp;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function moveWithBackup(target, source, name) {
|
|
140
|
+
const hadTarget = fs.existsSync(target);
|
|
141
|
+
let backup = null;
|
|
142
|
+
if (hadTarget) {
|
|
143
|
+
const backupRoot = path.join(path.dirname(target), BACKUP_DIR, "skills");
|
|
144
|
+
fs.mkdirSync(backupRoot, { recursive: true });
|
|
145
|
+
const stamp = new Date().toISOString().replace(/[:.]/gu, "-");
|
|
146
|
+
backup = path.join(backupRoot, `${name}-${stamp}`);
|
|
147
|
+
fs.renameSync(target, backup);
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
fs.renameSync(source, target);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (backup && !fs.existsSync(target)) {
|
|
153
|
+
try { fs.renameSync(backup, target); } catch { /* best effort */ }
|
|
154
|
+
}
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
return backup;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Synchronise bundled skills into `target`.
|
|
162
|
+
* @returns {{added:string[],updated:string[],adopted:string[],kept:string[],conflicts:string[],removed:string[],state:object}}
|
|
163
|
+
*/
|
|
164
|
+
export function syncSkills({ source, target, kitVersion, manifest, baseline, force = false, logger } = {}) {
|
|
165
|
+
const result = { added: [], updated: [], adopted: [], kept: [], conflicts: [], removed: [], state: emptyState() };
|
|
166
|
+
if (typeof source !== "string" || typeof target !== "string" || !fs.existsSync(source)) return result;
|
|
167
|
+
fs.mkdirSync(target, { recursive: true });
|
|
168
|
+
const state = loadSkillState(target);
|
|
169
|
+
state.kitVersion = kitVersion ?? state.kitVersion;
|
|
170
|
+
state.updatedAt = new Date().toISOString();
|
|
171
|
+
state.schema = SKILLS_SYNC_SCHEMA;
|
|
172
|
+
const knownBaselines = new Map();
|
|
173
|
+
for (const version of Object.values(baseline ?? {})) {
|
|
174
|
+
for (const [name, hash] of Object.entries(version ?? {})) {
|
|
175
|
+
if (!knownBaselines.has(name)) knownBaselines.set(name, new Set());
|
|
176
|
+
knownBaselines.get(name).add(hash);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const bundled = [];
|
|
180
|
+
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
181
|
+
if (entry.isDirectory()) bundled.push(entry.name);
|
|
182
|
+
}
|
|
183
|
+
bundled.sort();
|
|
184
|
+
|
|
185
|
+
for (const name of bundled) {
|
|
186
|
+
const sourceDir = path.join(source, name);
|
|
187
|
+
const targetDir = path.join(target, name);
|
|
188
|
+
const sourceInfo = hashSkillDir(sourceDir);
|
|
189
|
+
const manifestHash = manifest?.skills?.[name]?.hash;
|
|
190
|
+
const sourceHash = sourceInfo.hash;
|
|
191
|
+
const targetExists = fs.existsSync(targetDir);
|
|
192
|
+
const entry = state.skills[name];
|
|
193
|
+
const targetHash = targetExists ? hashSkillDir(targetDir).hash : "";
|
|
194
|
+
|
|
195
|
+
if (!targetExists) {
|
|
196
|
+
const temp = copySkill(sourceDir, targetDir);
|
|
197
|
+
moveWithBackup(targetDir, temp, name);
|
|
198
|
+
state.skills[name] = { hash: sourceHash, updatedAt: state.updatedAt, kitVersion: state.kitVersion };
|
|
199
|
+
delete state.conflicts[name];
|
|
200
|
+
result.added.push(name);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (targetHash === sourceHash || (manifestHash && targetHash === manifestHash && sourceHash === manifestHash)) {
|
|
204
|
+
state.skills[name] = { hash: sourceHash, updatedAt: entry?.updatedAt ?? state.updatedAt, kitVersion: state.kitVersion };
|
|
205
|
+
delete state.conflicts[name];
|
|
206
|
+
if (!entry) result.adopted.push(name);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const unchangedSinceSync = Boolean(entry && targetHash === entry.hash);
|
|
210
|
+
const knownBaseline = !entry && Boolean(knownBaselines.get(name)?.has(targetHash));
|
|
211
|
+
if (force || unchangedSinceSync || knownBaseline) {
|
|
212
|
+
const temp = copySkill(sourceDir, targetDir);
|
|
213
|
+
moveWithBackup(targetDir, temp, name);
|
|
214
|
+
state.skills[name] = { hash: sourceHash, updatedAt: state.updatedAt, kitVersion: state.kitVersion };
|
|
215
|
+
delete state.conflicts[name];
|
|
216
|
+
result.updated.push(name);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
state.conflicts[name] = {
|
|
220
|
+
reason: entry ? "modified-since-last-sync" : "modified-unknown-base",
|
|
221
|
+
hash: targetHash,
|
|
222
|
+
at: state.updatedAt,
|
|
223
|
+
};
|
|
224
|
+
result.kept.push(name);
|
|
225
|
+
result.conflicts.push(name);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* Skills removed from the bundle are never deleted: they may be user-owned. */
|
|
229
|
+
for (const name of Object.keys(state.skills)) {
|
|
230
|
+
if (!bundled.includes(name) && fs.existsSync(path.join(target, name))) result.removed.push(name);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
saveSkillState(target, state);
|
|
234
|
+
result.state = state;
|
|
235
|
+
if (result.added.length + result.updated.length > 0) {
|
|
236
|
+
logger?.info?.(`[dsh-desktop-kit] skills: +${result.added.length} ~${result.updated.length}, conflicts ${result.conflicts.length}`);
|
|
237
|
+
}
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function kitManifestSummary(manifest) {
|
|
242
|
+
if (!manifest?.skills) return { count: 0, bytes: 0 };
|
|
243
|
+
const values = Object.values(manifest.skills);
|
|
244
|
+
return { count: values.length, bytes: values.reduce((sum, skill) => sum + (Number(skill.bytes) || 0), 0) };
|
|
245
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-desktop-kit",
|
|
3
|
-
"version": "2.1
|
|
4
|
-
"description": "DeepSeek Harness desktop kit 2.
|
|
3
|
+
"version": "2.2.1",
|
|
4
|
+
"description": "DeepSeek Harness desktop kit 2.2: update-safe self-applying skills, live token lab, Skills Studio, self-scanning project memory, Russian UI, peak guard, chat move and profile transfer.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
7
7
|
"dsh",
|
|
@@ -32,8 +32,14 @@
|
|
|
32
32
|
"client.js",
|
|
33
33
|
"cordis.patch.yml",
|
|
34
34
|
"README.md",
|
|
35
|
+
"CHANGELOG.md",
|
|
35
36
|
"LICENSE",
|
|
37
|
+
"doctor.ps1",
|
|
38
|
+
"install.ps1",
|
|
39
|
+
"uninstall.ps1",
|
|
40
|
+
"lib/**",
|
|
36
41
|
"docs/**",
|
|
42
|
+
"skills-baseline.json",
|
|
37
43
|
"plugins/*/package.json",
|
|
38
44
|
"plugins/*/*.js",
|
|
39
45
|
"plugins/*/lib/**",
|
|
@@ -47,8 +53,9 @@
|
|
|
47
53
|
],
|
|
48
54
|
"scripts": {
|
|
49
55
|
"build:client": "node tools/build-client.mjs",
|
|
56
|
+
"build:skills": "node tools/build-skill-manifest.mjs",
|
|
50
57
|
"check:client": "node tools/check-client.mjs client.js",
|
|
51
|
-
"test": "node tools/smoke-tests.mjs && node tools/memory-tests.mjs",
|
|
58
|
+
"test": "node tools/smoke-tests.mjs && node tools/memory-tests.mjs && node tools/skills-sync-tests.mjs && node tools/kit-upgrade-tests.mjs",
|
|
52
59
|
"skills:cost": "node tools/skill-cost.mjs",
|
|
53
60
|
"skills:check": "node tools/skill-cost.mjs --check"
|
|
54
61
|
},
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": 1,
|
|
3
|
+
"kitVersion": "2.2.1",
|
|
4
|
+
"generatedAt": "2026-11-06T12:03:12.122Z",
|
|
5
|
+
"skills": {
|
|
6
|
+
"code-review-checklist": {
|
|
7
|
+
"hash": "e665bf72cfbfb77948a3cd6df6ebced38777f9aff543d7b9c80b2ced8cccc662",
|
|
8
|
+
"files": 1,
|
|
9
|
+
"bytes": 4483
|
|
10
|
+
},
|
|
11
|
+
"content-writing": {
|
|
12
|
+
"hash": "a21b26ff814e0337d03eeb587b200a1dc66e7de91bfea0f32840ddf8884deeee",
|
|
13
|
+
"files": 28,
|
|
14
|
+
"bytes": 271717
|
|
15
|
+
},
|
|
16
|
+
"context-economy": {
|
|
17
|
+
"hash": "bc8fa2e15327d21cf9ebd187b0df5f620d6a5b617976fa88910ae7877eb46b75",
|
|
18
|
+
"files": 4,
|
|
19
|
+
"bytes": 12766
|
|
20
|
+
},
|
|
21
|
+
"database-safety": {
|
|
22
|
+
"hash": "56f43f6bac270aefce34355e2a932192e4d17725d8faec5f91f476792b28c617",
|
|
23
|
+
"files": 1,
|
|
24
|
+
"bytes": 4448
|
|
25
|
+
},
|
|
26
|
+
"debugging-playbook": {
|
|
27
|
+
"hash": "0d70b8102f04d29774e466efd81abb448c84a6537a2ca3438f6931a2d6fe0c3a",
|
|
28
|
+
"files": 1,
|
|
29
|
+
"bytes": 4443
|
|
30
|
+
},
|
|
31
|
+
"design-critique": {
|
|
32
|
+
"hash": "bbc6ca34b0b76798d581dcd78e83a59040dfac028b5e10d8e7a9be28225410b8",
|
|
33
|
+
"files": 6,
|
|
34
|
+
"bytes": 25754
|
|
35
|
+
},
|
|
36
|
+
"dev-workflow": {
|
|
37
|
+
"hash": "832d36da6d3dc3e6e7d1feb18c38260ed8669c7702dffbd2d7a96fc9bc93d22f",
|
|
38
|
+
"files": 4,
|
|
39
|
+
"bytes": 17194
|
|
40
|
+
},
|
|
41
|
+
"git-and-ci": {
|
|
42
|
+
"hash": "0681041edb935da2c124265bbdacd3b6a0a96c7f163323960cff8c8ea67fd2f4",
|
|
43
|
+
"files": 1,
|
|
44
|
+
"bytes": 4192
|
|
45
|
+
},
|
|
46
|
+
"pg-upgrade-verify": {
|
|
47
|
+
"hash": "d734178c5dad317fc55842215bbeeba85230cdd8e7b0ea46d74eed92910b8dea",
|
|
48
|
+
"files": 1,
|
|
49
|
+
"bytes": 4496
|
|
50
|
+
},
|
|
51
|
+
"project-deep-dive": {
|
|
52
|
+
"hash": "a70355f2a40b42a6ff03e745ba42389549b4efe4f392c98033fe5073f946452e",
|
|
53
|
+
"files": 4,
|
|
54
|
+
"bytes": 26290
|
|
55
|
+
},
|
|
56
|
+
"project-memory-maintenance": {
|
|
57
|
+
"hash": "648f175093f6a86e20157156d8731b8e9602badedc79188e77160b18832f4893",
|
|
58
|
+
"files": 4,
|
|
59
|
+
"bytes": 23469
|
|
60
|
+
},
|
|
61
|
+
"release-and-deploy": {
|
|
62
|
+
"hash": "e6ed27856b00e8759d31659162290307c16e203d2b5d28fcacb574be17fc795b",
|
|
63
|
+
"files": 1,
|
|
64
|
+
"bytes": 4409
|
|
65
|
+
},
|
|
66
|
+
"requirements-and-planning": {
|
|
67
|
+
"hash": "532cbccfb2d0f7eca06343927aba100bd653145ccd9b9173ca50299194631a05",
|
|
68
|
+
"files": 4,
|
|
69
|
+
"bytes": 12964
|
|
70
|
+
},
|
|
71
|
+
"security-review": {
|
|
72
|
+
"hash": "8665b2c469de8e83aca670021d5c335fa8c9b93b8dc6bb866759e6062b1abde4",
|
|
73
|
+
"files": 1,
|
|
74
|
+
"bytes": 4257
|
|
75
|
+
},
|
|
76
|
+
"seo": {
|
|
77
|
+
"hash": "ed48ebc4c1278f857ff430f9a5ec55f638c2f9ba07cc406185cfb94be18ee80d",
|
|
78
|
+
"files": 29,
|
|
79
|
+
"bytes": 386169
|
|
80
|
+
},
|
|
81
|
+
"service-desk-conventions": {
|
|
82
|
+
"hash": "22ae6c29b7debb0154d183bc732795b67557aeaa0ac906d7b038134aea8ba39a",
|
|
83
|
+
"files": 1,
|
|
84
|
+
"bytes": 4488
|
|
85
|
+
},
|
|
86
|
+
"service-desk-db-safety": {
|
|
87
|
+
"hash": "672c08d25e0a9262aeb0eec27b6daa2a619fade6ff3269bc2b9dc27e22c83d28",
|
|
88
|
+
"files": 1,
|
|
89
|
+
"bytes": 4482
|
|
90
|
+
},
|
|
91
|
+
"service-desk-release": {
|
|
92
|
+
"hash": "1bd0c03f772bf4b79ec4e887f5d0ebd564653bafd3d0d33d70f6c934afdcce47",
|
|
93
|
+
"files": 1,
|
|
94
|
+
"bytes": 4418
|
|
95
|
+
},
|
|
96
|
+
"service-desk-testing": {
|
|
97
|
+
"hash": "eaff64154a596590684308cdccb3f55099240589b57c0f506a85e5b38ddf5ac5",
|
|
98
|
+
"files": 1,
|
|
99
|
+
"bytes": 4129
|
|
100
|
+
},
|
|
101
|
+
"service-desk-ui": {
|
|
102
|
+
"hash": "a3a0e78ab147f4837e4ca9736f4d87cf77a6638c2db7c5859629df70abdd2c1c",
|
|
103
|
+
"files": 1,
|
|
104
|
+
"bytes": 4201
|
|
105
|
+
},
|
|
106
|
+
"skills-router": {
|
|
107
|
+
"hash": "63c2dcdeb07dd4af464a30a8065465bd06b73e4caa8aa874802e5188d4d5a4ae",
|
|
108
|
+
"files": 3,
|
|
109
|
+
"bytes": 13888
|
|
110
|
+
},
|
|
111
|
+
"testing-strategy": {
|
|
112
|
+
"hash": "773fdf1e389e59841b7766862f8890a11fba4a2fce42a2022b9c4bfaae8f6ecc",
|
|
113
|
+
"files": 1,
|
|
114
|
+
"bytes": 4279
|
|
115
|
+
},
|
|
116
|
+
"ui-design-system": {
|
|
117
|
+
"hash": "19244221171995b758dbad02f2e3602859308370b50cc37d93c6f2a6d44cd6f1",
|
|
118
|
+
"files": 4,
|
|
119
|
+
"bytes": 19454
|
|
120
|
+
},
|
|
121
|
+
"web-research": {
|
|
122
|
+
"hash": "9bf4d8ba6d2a0fc39c241cdd9f5fe54835af28872473aa617941589c526387c8",
|
|
123
|
+
"files": 3,
|
|
124
|
+
"bytes": 14150
|
|
125
|
+
},
|
|
126
|
+
"wow-design": {
|
|
127
|
+
"hash": "81139f32fc759f438a36eb4c0212ed3ea17df9d8075e909580e12bc47a0848dd",
|
|
128
|
+
"files": 52,
|
|
129
|
+
"bytes": 913669
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|