dsh-desktop-kit 2.2.1 → 2.7.3
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 +86 -0
- package/README.md +114 -27
- package/client.js +3270 -32
- package/docs/AGENT-HARNESS.md +144 -0
- package/docs/ARCHITECTURE.md +38 -7
- package/docs/COMPATIBILITY.md +4 -2
- package/docs/DEVELOPMENT.md +56 -0
- package/docs/INSTALL.md +20 -11
- package/docs/LAPTOP-INSTALL.md +96 -0
- package/docs/ROADMAP-3.0.md +353 -0
- package/docs/ROADMAP.md +1 -1
- package/docs/SUBAGENTS.md +25 -0
- package/docs/TEST-0.2.2.md +5 -5
- package/docs/TEST-2.3.0.md +49 -0
- package/docs/TEST-2.7.2.md +46 -0
- package/docs/TEST-2.7.3.md +20 -0
- package/docs/TOKENS.md +2 -2
- package/docs/TROUBLESHOOTING.md +27 -0
- package/docs/UI-QA.md +28 -0
- package/docs/images/peak-toast-peak.png +0 -0
- package/docs/images/peak-toast-soon.png +0 -0
- package/docs/preview/peak-toast.html +155 -0
- package/index.js +18 -1
- package/package.json +26 -6
- package/plugins/dsh-agent-extensions/README.md +107 -0
- package/plugins/dsh-agent-extensions/client.js +157 -0
- package/plugins/dsh-agent-extensions/index.js +174 -0
- package/plugins/dsh-agent-extensions/lib/agents.js +76 -0
- package/plugins/dsh-agent-extensions/lib/api.js +53 -0
- package/plugins/dsh-agent-extensions/lib/checkpoints.js +103 -0
- package/plugins/dsh-agent-extensions/lib/commands.js +255 -0
- package/plugins/dsh-agent-extensions/lib/hooks.js +182 -0
- package/plugins/dsh-agent-extensions/lib/policy.js +84 -0
- package/plugins/dsh-agent-extensions/lib/project.js +144 -0
- package/plugins/dsh-agent-extensions/lib/scaffold.js +151 -0
- package/plugins/dsh-agent-extensions/package.json +20 -0
- package/plugins/dsh-agent-extensions/screenshots/agents-1440.png +0 -0
- package/plugins/dsh-agent-extensions/screenshots/agents-390.png +0 -0
- package/plugins/dsh-browser-qa/README.md +221 -0
- package/plugins/dsh-browser-qa/cordis.patch.yml +4 -0
- package/plugins/dsh-browser-qa/index.js +165 -0
- package/plugins/dsh-browser-qa/lib/cdp.js +224 -0
- package/plugins/dsh-browser-qa/lib/chrome.js +466 -0
- package/plugins/dsh-browser-qa/lib/pages.js +552 -0
- package/plugins/dsh-browser-qa/lib/qa.js +827 -0
- package/plugins/dsh-browser-qa/lib/ui-gate.js +256 -0
- package/plugins/dsh-browser-qa/package.json +31 -0
- package/plugins/dsh-checkpoints/README.md +22 -0
- package/plugins/dsh-checkpoints/cordis.patch.yml +3 -0
- package/plugins/dsh-checkpoints/index.js +80 -0
- package/plugins/dsh-checkpoints/lib/checkpoints.js +111 -0
- package/plugins/dsh-checkpoints/package.json +25 -0
- package/plugins/dsh-github-review/README.md +131 -0
- package/plugins/dsh-github-review/cordis.patch.yml +3 -0
- package/plugins/dsh-github-review/index.js +536 -0
- package/plugins/dsh-github-review/lib/gh.js +494 -0
- package/plugins/dsh-github-review/lib/normalize.js +662 -0
- package/plugins/dsh-github-review/package.json +28 -0
- package/plugins/dsh-guardrails/README.md +264 -0
- package/plugins/dsh-guardrails/cordis.patch.yml +5 -0
- package/plugins/dsh-guardrails/index.js +389 -0
- package/plugins/dsh-guardrails/lib/hooks.js +262 -0
- package/plugins/dsh-guardrails/lib/modes.js +77 -0
- package/plugins/dsh-guardrails/lib/policy.js +592 -0
- package/plugins/dsh-guardrails/lib/scan.js +451 -0
- package/plugins/dsh-guardrails/lib/secrets.js +274 -0
- package/plugins/dsh-guardrails/package.json +26 -0
- package/plugins/dsh-mcp-manager/README.md +78 -0
- package/plugins/dsh-mcp-manager/client.js +373 -0
- package/plugins/dsh-mcp-manager/index.js +159 -0
- package/plugins/dsh-mcp-manager/lib/api.js +250 -0
- package/plugins/dsh-mcp-manager/lib/client.js +219 -0
- package/plugins/dsh-mcp-manager/lib/config.js +217 -0
- package/plugins/dsh-mcp-manager/lib/manager.js +146 -0
- package/plugins/dsh-mcp-manager/package.json +18 -0
- package/plugins/dsh-mcp-manager/screenshots/modal-390.png +0 -0
- package/plugins/dsh-mcp-manager/screenshots/settings-1440.png +0 -0
- package/plugins/dsh-mcp-manager/screenshots/settings-390.png +0 -0
- package/plugins/dsh-memory-bridge/README.md +161 -0
- package/plugins/dsh-memory-bridge/client.js +2264 -0
- package/plugins/dsh-memory-bridge/cordis.patch.yml +5 -0
- package/plugins/dsh-memory-bridge/docs/images/conflicts.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/hero.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/settings-dark.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/settings-light.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/settings-mobile.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/setup.png +0 -0
- package/plugins/dsh-memory-bridge/icon.svg +13 -0
- package/plugins/dsh-memory-bridge/index.js +378 -0
- package/plugins/dsh-memory-bridge/lib/bridge.js +636 -0
- package/plugins/dsh-memory-bridge/lib/github.js +473 -0
- package/plugins/dsh-memory-bridge/lib/merge.js +205 -0
- package/plugins/dsh-memory-bridge/lib/portable.js +314 -0
- package/plugins/dsh-memory-bridge/lib/state.js +565 -0
- package/plugins/dsh-memory-bridge/lib/sync.js +984 -0
- package/plugins/dsh-memory-bridge/locale/en.json +6 -0
- package/plugins/dsh-memory-bridge/locale/ru.json +6 -0
- package/plugins/dsh-memory-bridge/package.json +67 -0
- package/plugins/dsh-peak-guard/README.md +16 -3
- package/plugins/dsh-peak-guard/client.js +223 -8
- package/plugins/dsh-peak-guard/package.json +1 -1
- package/plugins/dsh-plan-mode/README.md +24 -0
- package/plugins/dsh-plan-mode/cordis.patch.yml +3 -0
- package/plugins/dsh-plan-mode/index.js +103 -0
- package/plugins/dsh-plan-mode/lib/plan.js +89 -0
- package/plugins/dsh-plan-mode/package.json +18 -0
- package/plugins/dsh-profile-transfer/client.js +277 -17
- package/plugins/dsh-profile-transfer/index.js +10 -0
- package/plugins/dsh-profile-transfer/package.json +3 -1
- package/plugins/dsh-profile-transfer/transfer.js +101 -2
- package/plugins/dsh-project-memory/index.js +92 -2
- package/plugins/dsh-project-memory/package.json +1 -1
- package/plugins/dsh-repo-map/README.md +26 -0
- package/plugins/dsh-repo-map/cordis.patch.yml +3 -0
- package/plugins/dsh-repo-map/index.js +99 -0
- package/plugins/dsh-repo-map/lib/repo-map.js +170 -0
- package/plugins/dsh-repo-map/package.json +25 -0
- package/plugins/dsh-skill-autopilot/index.js +135 -11
- package/skills/delivery-gate/SKILL.md +31 -0
- package/skills/git-and-ci/SKILL.md +5 -1
- package/skills/manifest.json +31 -6
- package/skills/requirements-and-planning/SKILL.md +3 -0
- package/skills/research-first/SKILL.md +35 -0
- package/skills/research-first/references/query-patterns.md +20 -0
- package/skills/subagent-playbook/SKILL.md +36 -0
- package/skills/subagent-playbook/references/prompts.md +39 -0
- package/skills/ui-craft/SKILL.md +43 -0
- package/skills/visual-qa/SKILL.md +29 -0
- package/skills/visual-qa/references/commands.md +32 -0
- package/skills-baseline.json +219 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Project memory bridge">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bridgeGradient" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#3b82f6"/>
|
|
5
|
+
<stop offset="1" stop-color="#8b5cf6"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect x="2" y="2" width="60" height="60" rx="14" fill="url(#bridgeGradient)"/>
|
|
9
|
+
<rect x="9" y="17" width="17" height="30" rx="4" fill="#ffffff" opacity="0.92"/>
|
|
10
|
+
<rect x="38" y="17" width="17" height="30" rx="4" fill="#ffffff" opacity="0.92"/>
|
|
11
|
+
<path d="M29 27h6M29 37h6" stroke="#ffffff" stroke-width="3.2" stroke-linecap="round"/>
|
|
12
|
+
<path d="M34 25.5 37.5 27 34 28.5M34 35.5 37.5 37 34 38.5" fill="none" stroke="#ffffff" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>
|
|
13
|
+
</svg>
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-memory-bridge — Host half (v2, GitHub-only).
|
|
3
|
+
*
|
|
4
|
+
* - RPC routes for the Settings page (`/api/dsh-memory-bridge/*`);
|
|
5
|
+
* - background sync: startup, timer, dossier changes, end of a turn;
|
|
6
|
+
* - hidden `/sync` diagnostics commands for recovery (the UI is the main path);
|
|
7
|
+
* - no model tools, no prompt injections, no subagents: zero token overhead.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
import { dshHome } from "./lib/bridge.js";
|
|
15
|
+
import { createSyncService } from "./lib/sync.js";
|
|
16
|
+
import * as st from "./lib/state.js";
|
|
17
|
+
|
|
18
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const VERSION = (() => {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(fs.readFileSync(path.join(here, "package.json"), "utf8")).version ?? "unknown";
|
|
22
|
+
} catch {
|
|
23
|
+
return "unknown";
|
|
24
|
+
}
|
|
25
|
+
})();
|
|
26
|
+
|
|
27
|
+
/** Best-effort boot diagnostics: tells the UI/Host load apart from an apply failure. */
|
|
28
|
+
function boot(stage, extra) {
|
|
29
|
+
try {
|
|
30
|
+
const dir = path.join(dshHome(), "memory-sync");
|
|
31
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
32
|
+
const line = `${new Date().toISOString()} v${VERSION} ${stage}${extra ? ` ${extra}` : ""}\n`;
|
|
33
|
+
fs.appendFileSync(path.join(dir, "boot.log"), line);
|
|
34
|
+
} catch {
|
|
35
|
+
/* diagnostics must never break the plugin */
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const ROUTE_BASE = "/api/dsh-memory-bridge";
|
|
40
|
+
const RPC_NAMESPACE = "dsh-memory-bridge";
|
|
41
|
+
|
|
42
|
+
export const inject = ["connection", "commands"];
|
|
43
|
+
|
|
44
|
+
class RpcError extends Error {
|
|
45
|
+
constructor(code, message, details = {}) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.code = code;
|
|
48
|
+
this.details = details;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function okEnvelope(rpcId, value) {
|
|
53
|
+
return Response.json({ type: "server-response", rpcId, result: { ok: true, value } });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function errorEnvelope(rpcId, code, message, details = {}) {
|
|
57
|
+
return Response.json({ type: "server-response", rpcId, result: { ok: false, error: { code, message, details } } });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function readEnvelope(request) {
|
|
61
|
+
let body;
|
|
62
|
+
try {
|
|
63
|
+
body = await request.json();
|
|
64
|
+
} catch {
|
|
65
|
+
throw new RpcError("memory-bridge/bad-request", "тело запроса не JSON");
|
|
66
|
+
}
|
|
67
|
+
if (body === null || typeof body !== "object" || body.type !== "client-request" || typeof body.rpcId !== "string" || typeof body.method !== "string") {
|
|
68
|
+
throw new RpcError("memory-bridge/bad-request", "некорректный конверт Connection");
|
|
69
|
+
}
|
|
70
|
+
return { rpcId: body.rpcId, method: body.method, payload: body.payload ?? {} };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function apply(ctx) {
|
|
74
|
+
boot("apply-start", ctx?.logger ? "logger" : "no-logger");
|
|
75
|
+
const logger = ctx.logger;
|
|
76
|
+
const home = dshHome();
|
|
77
|
+
let service;
|
|
78
|
+
try {
|
|
79
|
+
service = createSyncService({ home, logger });
|
|
80
|
+
await service.init();
|
|
81
|
+
boot("init-ok");
|
|
82
|
+
} catch (error) {
|
|
83
|
+
boot("init-failed", String(error));
|
|
84
|
+
logger?.warn?.(`[memory-bridge] init failed: ${String(error)}`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const log = (message) => logger?.debug?.(`[memory-bridge] ${message}`);
|
|
89
|
+
const warn = (message) => logger?.warn?.(`[memory-bridge] ${message}`);
|
|
90
|
+
let disposed = false;
|
|
91
|
+
const timeouts = new Set();
|
|
92
|
+
const intervals = new Set();
|
|
93
|
+
|
|
94
|
+
function timeout(callback, delay) {
|
|
95
|
+
if (disposed) return undefined;
|
|
96
|
+
const timer = setTimeout(() => {
|
|
97
|
+
timeouts.delete(timer);
|
|
98
|
+
callback();
|
|
99
|
+
}, delay);
|
|
100
|
+
timer.unref?.();
|
|
101
|
+
timeouts.add(timer);
|
|
102
|
+
return timer;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function interval(callback, delay) {
|
|
106
|
+
if (disposed) return undefined;
|
|
107
|
+
const timer = setInterval(callback, delay);
|
|
108
|
+
timer.unref?.();
|
|
109
|
+
intervals.add(timer);
|
|
110
|
+
return timer;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function safeSync(reason) {
|
|
114
|
+
try {
|
|
115
|
+
return await service.autoSync({ reason });
|
|
116
|
+
} catch (error) {
|
|
117
|
+
warn(`sync failed (${reason}): ${String(error)}`);
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* ---------------- background sync ---------------- */
|
|
123
|
+
|
|
124
|
+
timeout(() => void safeSync("startup"), 10_000);
|
|
125
|
+
interval(() => void safeSync("timer"), 60_000);
|
|
126
|
+
|
|
127
|
+
let syncTimer;
|
|
128
|
+
let syncPending = false;
|
|
129
|
+
function scheduleSync(reason, delayMs = 30_000) {
|
|
130
|
+
if (disposed) return;
|
|
131
|
+
if (syncPending) return;
|
|
132
|
+
syncPending = true;
|
|
133
|
+
if (syncTimer !== undefined) clearTimeout(syncTimer);
|
|
134
|
+
syncTimer = timeout(() => {
|
|
135
|
+
syncPending = false;
|
|
136
|
+
void safeSync(reason);
|
|
137
|
+
}, delayMs);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
ctx.on("session/event", (session, event) => {
|
|
142
|
+
try {
|
|
143
|
+
if (session?.header?.origin === "subagent") return;
|
|
144
|
+
if (event?.type === "turn/end") scheduleSync("turn-end", 15_000);
|
|
145
|
+
} catch {
|
|
146
|
+
/* never break a session event */
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
} catch (error) {
|
|
150
|
+
warn(`session events unavailable: ${String(error)}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let watcher;
|
|
154
|
+
try {
|
|
155
|
+
watcher = fs.watch(service.paths.projectsMemory, { recursive: true }, (eventType, filename) => {
|
|
156
|
+
try {
|
|
157
|
+
const name = typeof filename === "string" ? filename.replaceAll("\\", "/") : "";
|
|
158
|
+
if (name.length === 0 || !name.endsWith(".md")) return;
|
|
159
|
+
if (/_access\.log$|_state\.json$|scan\.json$|INDEX\.json$/u.test(name)) return;
|
|
160
|
+
scheduleSync("dossier-write", 60_000);
|
|
161
|
+
} catch {
|
|
162
|
+
/* defensive */
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
watcher.on?.("error", (error) => warn(`watcher: ${String(error)}`));
|
|
166
|
+
} catch (error) {
|
|
167
|
+
warn(`fs.watch unavailable (${String(error)}); timer sync remains active`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* ---------------- RPC for the Settings page ---------------- */
|
|
171
|
+
|
|
172
|
+
let routeCtx = null;
|
|
173
|
+
function registerOperation(operation, handler) {
|
|
174
|
+
if (routeCtx === null || typeof routeCtx.connection?.fetch?.register !== "function") return;
|
|
175
|
+
const route = {
|
|
176
|
+
path: `${ROUTE_BASE}/${operation}`,
|
|
177
|
+
methods: ["POST"],
|
|
178
|
+
requestBody: "buffered",
|
|
179
|
+
fetch: async (request) => {
|
|
180
|
+
let rpcId = "invalid-request";
|
|
181
|
+
try {
|
|
182
|
+
const envelope = await readEnvelope(request);
|
|
183
|
+
rpcId = envelope.rpcId;
|
|
184
|
+
if (envelope.method !== `${RPC_NAMESPACE}/${operation}`) {
|
|
185
|
+
return errorEnvelope(rpcId, "memory-bridge/bad-request", "метод конверта не совпадает с endpoint");
|
|
186
|
+
}
|
|
187
|
+
const value = await handler(envelope.payload);
|
|
188
|
+
return okEnvelope(rpcId, value);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
const code = typeof error?.code === "string" ? error.code : "memory-bridge/internal";
|
|
191
|
+
if (code === "memory-bridge/internal") warn(`${operation} failed: ${String(error)}`);
|
|
192
|
+
return errorEnvelope(rpcId, code, error instanceof Error ? error.message : String(error));
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
ctx.effect(() => {
|
|
197
|
+
const dispose = routeCtx.connection.fetch.register(route);
|
|
198
|
+
return () => {
|
|
199
|
+
try {
|
|
200
|
+
void Promise.resolve(dispose()).catch(() => {});
|
|
201
|
+
} catch {
|
|
202
|
+
/* carrier already unloading */
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}, `memory-bridge: ${operation} route`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function registerRoutes(injectedCtx) {
|
|
209
|
+
if (routeCtx !== null) return;
|
|
210
|
+
routeCtx = injectedCtx;
|
|
211
|
+
boot("routes-start", ctx === injectedCtx ? "direct" : "injected");
|
|
212
|
+
registerOperation("status", async () => service.status());
|
|
213
|
+
registerOperation("configure", async (payload) => {
|
|
214
|
+
await service.configure(payload?.repo, payload?.branch);
|
|
215
|
+
return { ok: true, status: await service.status() };
|
|
216
|
+
});
|
|
217
|
+
registerOperation("create-repo", async (payload) => {
|
|
218
|
+
const result = await service.createRepo(payload?.repo);
|
|
219
|
+
return { ok: true, created: result.created, repo: result.repo, owner: result.owner, status: result.status };
|
|
220
|
+
});
|
|
221
|
+
registerOperation("set-enabled", async (payload) => {
|
|
222
|
+
await service.setEnabled(payload?.enabled === true);
|
|
223
|
+
return { ok: true, status: await service.status() };
|
|
224
|
+
});
|
|
225
|
+
registerOperation("set-options", async (payload) => {
|
|
226
|
+
await service.setOptions(payload ?? {});
|
|
227
|
+
return { ok: true, status: await service.status() };
|
|
228
|
+
});
|
|
229
|
+
registerOperation("sync-now", async () => {
|
|
230
|
+
const report = await service.syncNow({ reason: "ui" });
|
|
231
|
+
return {
|
|
232
|
+
ok: true,
|
|
233
|
+
status: await service.status(),
|
|
234
|
+
summary: {
|
|
235
|
+
projects: report.projects,
|
|
236
|
+
files: report.files,
|
|
237
|
+
conflicts: report.conflicts,
|
|
238
|
+
pushed: report.pushed,
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
registerOperation("resolve-conflict", async (payload) => {
|
|
243
|
+
await service.resolveConflict(String(payload?.id ?? ""), payload?.choice);
|
|
244
|
+
return { ok: true, status: await service.status() };
|
|
245
|
+
});
|
|
246
|
+
registerOperation("history", async (payload) => {
|
|
247
|
+
const history = await service.history(String(payload?.slug ?? ""), { limit: payload?.limit });
|
|
248
|
+
return { ok: true, ...history };
|
|
249
|
+
});
|
|
250
|
+
registerOperation("restore-version", async (payload) => {
|
|
251
|
+
const restored = await service.restoreVersion(String(payload?.slug ?? ""), payload?.commit);
|
|
252
|
+
return { ok: true, ...restored, status: await service.status() };
|
|
253
|
+
});
|
|
254
|
+
registerOperation("portable-export", async (payload) => {
|
|
255
|
+
const exported = await service.portableExport(String(payload?.destination ?? ""));
|
|
256
|
+
return { ok: true, ...exported };
|
|
257
|
+
});
|
|
258
|
+
registerOperation("portable-scan", async (payload) => {
|
|
259
|
+
const scanned = await service.portableScan(String(payload?.source ?? ""));
|
|
260
|
+
return { ok: true, ...scanned };
|
|
261
|
+
});
|
|
262
|
+
registerOperation("portable-import", async (payload) => {
|
|
263
|
+
const imported = await service.portableImport(String(payload?.source ?? ""), {
|
|
264
|
+
projectSlugs: Array.isArray(payload?.projectSlugs) ? payload.projectSlugs : undefined,
|
|
265
|
+
strategy: payload?.strategy === "skip" ? "skip" : "replace",
|
|
266
|
+
});
|
|
267
|
+
return { ok: true, ...imported, status: await service.status() };
|
|
268
|
+
});
|
|
269
|
+
log("RPC routes registered");
|
|
270
|
+
boot("routes-registered", String(routeCtx === ctx ? "direct" : "injected"));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
const direct = typeof ctx.connection?.fetch?.register === "function";
|
|
275
|
+
boot("connection-check", direct ? "available" : "absent");
|
|
276
|
+
if (direct) {
|
|
277
|
+
registerRoutes(ctx);
|
|
278
|
+
} else {
|
|
279
|
+
boot("connection-missing");
|
|
280
|
+
warn("connection service unavailable; UI routes are disabled, commands still work");
|
|
281
|
+
}
|
|
282
|
+
} catch (error) {
|
|
283
|
+
boot("connection-inject-failed", String(error));
|
|
284
|
+
warn(`connection service unavailable (${String(error)}); UI routes are disabled, commands still work`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/* ---------------- hidden diagnostic commands ---------------- */
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const dispose = ctx.commands.register({
|
|
291
|
+
name: "sync",
|
|
292
|
+
description: "Диагностика моста памяти (основной интерфейс — Настройки → Память проектов)",
|
|
293
|
+
input: { hint: "status | on | off | repo <owner/name> | sync | conflicts | resolve <id> local|remote|both" },
|
|
294
|
+
recordInput: false,
|
|
295
|
+
handler: async ({ rawInput }) => {
|
|
296
|
+
const input = String(rawInput ?? "").trim();
|
|
297
|
+
const [sub, ...rest] = input.length === 0 ? ["status"] : input.split(/\s+/u);
|
|
298
|
+
try {
|
|
299
|
+
if (sub === "on" || sub === "off") {
|
|
300
|
+
await service.setEnabled(sub === "on");
|
|
301
|
+
return { kind: "success", text: `Синхронизация ${sub === "on" ? "включена" : "выключена"}.` };
|
|
302
|
+
}
|
|
303
|
+
if (sub === "repo") {
|
|
304
|
+
await service.configure(rest.join(" "));
|
|
305
|
+
return { kind: "success", text: `Репозиторий подключён: ${rest.join(" ")}` };
|
|
306
|
+
}
|
|
307
|
+
if (sub === "sync") {
|
|
308
|
+
const report = await service.syncNow({ reason: "command" });
|
|
309
|
+
return { kind: "success", text: `Синхронизация: проектов ${report.projects}, файлов ${report.files}, конфликтов ${report.conflicts}, отправлено ${report.pushed ? "да" : "нет"}.` };
|
|
310
|
+
}
|
|
311
|
+
if (sub === "conflicts") {
|
|
312
|
+
const status = await service.status();
|
|
313
|
+
return { kind: "success", text: status.conflicts.length === 0 ? "Конфликтов нет." : status.conflicts.map((conflict) => `- ${conflict.projectSlug}/${conflict.path} (${conflict.id})`).join("\n") };
|
|
314
|
+
}
|
|
315
|
+
if (sub === "resolve") {
|
|
316
|
+
await service.resolveConflict(String(rest[0] ?? ""), rest[1]);
|
|
317
|
+
return { kind: "success", text: `Конфликт ${rest[0]} разрешён: ${rest[1] ?? "local"}.` };
|
|
318
|
+
}
|
|
319
|
+
const status = await service.status();
|
|
320
|
+
return { kind: "success", text: statusText(status) };
|
|
321
|
+
} catch (error) {
|
|
322
|
+
return { kind: "error", text: `Мост памяти: ${error instanceof Error ? error.message : String(error)}` };
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
ctx.effect(() => dispose, "memory-bridge: /sync diagnostic command");
|
|
327
|
+
log("diagnostic command registered: /sync");
|
|
328
|
+
} catch (error) {
|
|
329
|
+
warn(`command registration failed: ${String(error)}`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function statusText(status) {
|
|
333
|
+
const lines = [
|
|
334
|
+
`Мост памяти проектов (${VERSION})`,
|
|
335
|
+
`Репозиторий: ${status.configured ? status.repo : "не подключён"}`,
|
|
336
|
+
`Синхронизация: ${status.enabled ? "включена" : "выключена"}${status.gitAvailable ? "" : " · git не найден"}`,
|
|
337
|
+
`Последняя синхронизация: ${status.lastSyncAt ?? "—"}`,
|
|
338
|
+
`Проекты: ${status.projects.length} · локальных правок: ${status.pending.localChanges} · конфликтов: ${status.conflicts.length}`,
|
|
339
|
+
];
|
|
340
|
+
if (status.lastError) lines.push(`Ошибка: ${status.lastError}`);
|
|
341
|
+
lines.push("Основной интерфейс: Настройки → Память проектов.");
|
|
342
|
+
return lines.join("\n");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/* ---------------- diagnostics and cleanup ---------------- */
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
globalThis.__DSH_MEMORY_BRIDGE__ = {
|
|
349
|
+
version: VERSION,
|
|
350
|
+
status: () => service.status(),
|
|
351
|
+
};
|
|
352
|
+
} catch {
|
|
353
|
+
/* ignore non-browser hosts */
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
ctx.effect(() => () => {
|
|
357
|
+
disposed = true;
|
|
358
|
+
if (syncTimer !== undefined) clearTimeout(syncTimer);
|
|
359
|
+
for (const timer of timeouts) clearTimeout(timer);
|
|
360
|
+
timeouts.clear();
|
|
361
|
+
for (const timer of intervals) clearInterval(timer);
|
|
362
|
+
intervals.clear();
|
|
363
|
+
try {
|
|
364
|
+
watcher?.close();
|
|
365
|
+
} catch {
|
|
366
|
+
/* already closed */
|
|
367
|
+
}
|
|
368
|
+
void st.logLine(home, "stopped");
|
|
369
|
+
}, "memory-bridge: cleanup");
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
const me = await service.machine();
|
|
373
|
+
const config = await service.config();
|
|
374
|
+
log(`ready: ${VERSION}, machine ${me.machineId}, repo ${config.repo || "not configured"}`);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
log(`ready: ${VERSION}; diagnostics unavailable: ${String(error)}`);
|
|
377
|
+
}
|
|
378
|
+
}
|