dsh-code-review-graph 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +143 -10
- package/README.md +127 -9
- package/dist/backend.js +2 -2
- package/dist/backend.js.map +2 -2
- package/dist/client.js +45 -2
- package/dist/client.js.map +3 -3
- package/dist/index.js +147 -33
- package/dist/index.js.map +4 -4
- package/dist/setup.js +30 -14
- package/dist/setup.js.map +2 -2
- package/docs/release-0.1.2.md +29 -0
- package/docs/validation.md +20 -0
- package/package.json +2 -2
- package/docs/release-0.1.1.md +0 -30
package/dist/client.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/client.tsx", "../src/html.ts"],
|
|
4
|
-
"sourcesContent": ["import React, { useEffect, useState } from 'react';\nimport type { Context } from '@deepseek-ai/cordis';\nimport type {} from '@deepseek-ai/dsh-client-ui-sidebar-right/client';\nimport type {} from '@deepseek-ai/dsh-client-ui-slots';\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client';\nimport type {} from '@deepseek-ai/dsh-api-remotes/client';\nimport type {} from '@deepseek-ai/dsh-api-workspace-files/client';\nimport { inlineGraph } from './html.ts';\n\nexport const inject = ['slots', 'sidebarRightTabs', 'remote', 'remote.workspaceFiles'];\nexport const name = 'code-review-graph-client';\ntype ReadResult = { ok: true; value: { data: Uint8Array | string } } | { ok: false; error: { message: string } };\ntype WorkspaceReader = {\n readBytes?: (session: string, path: string, options: object, signal: AbortSignal) => Promise<ReadResult>;\n readAll?: (session: string, path: string, signal: AbortSignal) => Promise<ReadResult>;\n readRelated?: (session: string, base: string, path: string, signal: AbortSignal) => Promise<ReadResult>;\n};\nexport async function readAsset(reader: WorkspaceReader, session: string, path: string, signal: AbortSignal, base?: string): Promise<string> {\n const result = reader.readBytes ? await reader.readBytes(session, path, base ? { baseFile: base } : {}, signal)\n : base && reader.readRelated ? await reader.readRelated(session, base, path, signal)\n : reader.readAll ? await reader.readAll(session, path, signal) : (() => { throw new Error('Unsupported DSH file reader'); })();\n if (!result.ok) throw new Error(result.error.message);\n if (result.value.data instanceof Uint8Array) return new TextDecoder().decode(result.value.data);\n const base64 = result.value.data;\n if (typeof base64 !== 'string') throw new Error('Invalid graph asset response');\n return new TextDecoder().decode(Uint8Array.from(atob(base64), c => c.charCodeAt(0)));\n}\n\nexport function apply(ctx: Context): void {\n const id = 'dsh-code-review-graph';\n ctx.effect(() => ctx.sidebarRightTabs.register({ id, kind: 'code-review-graph', priority: 'builtin', title: () => 'Code graph / \u4EE3\u7801\u56FE\u8C31', guide: [{ id: 'graph', order: 30, title: () => 'Code graph / \u4EE3\u7801\u56FE\u8C31', description: () => 'Explore dependencies, changes and execution paths locally.' }] }));\n function Graph({ sessionId, useTabInfo }: { sessionId: string; useTabInfo: () => { tab: { signal: AbortSignal } } }) {\n const { tab } = useTabInfo();\n const [html, setHtml] = useState('');\n const [error, setError] = useState('');\n const [generation, refresh] = useState(0);\n const download = () => {\n const url = URL.createObjectURL(new Blob([html], { type: 'text/html;charset=utf-8' }));\n const link = document.createElement('a'); link.href = url; link.download = 'code-review-graph.html'; link.click();\n setTimeout(() => URL.revokeObjectURL(url), 1000);\n };\n useEffect(() => {\n const life = new AbortController();\n const signal = AbortSignal.any([life.signal, tab.signal]);\n const base = '.code-review-graph/graph.html';\n const reader = ctx.remote.workspaceFiles as unknown as WorkspaceReader;\n setError('');\n void readAsset(reader, sessionId, base, signal).then(html => inlineGraph(html, path => readAsset(reader, sessionId, path, signal, base))).then(result => {\n if (!signal.aborted) setHtml(result);\n }).catch(error => { if (!signal.aborted) setError(String(error)); });\n return () => life.abort();\n }, [sessionId, generation, tab.signal]);\n return <section style={{ height: '100%', display: 'flex', flexDirection: 'column', gap: 8 }}>\n <div
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,
|
|
6
|
-
"names": ["
|
|
4
|
+
"sourcesContent": ["import React, { useEffect, useRef, useState } from 'react';\nimport type { Context } from '@deepseek-ai/cordis';\nimport type {} from '@deepseek-ai/dsh-client-ui-sidebar-right/client';\nimport type {} from '@deepseek-ai/dsh-client-ui-slots';\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client';\nimport type {} from '@deepseek-ai/dsh-api-remotes/client';\nimport type {} from '@deepseek-ai/dsh-api-workspace-files/client';\nimport { inlineGraph } from './html.ts';\n\nexport const inject = ['slots', 'sidebarRightTabs', 'remote', 'remote.workspaceFiles', 'remote.commands'];\nexport const name = 'code-review-graph-client';\ntype ReadResult = { ok: true; value: { data: Uint8Array | string } } | { ok: false; error: { message: string } };\ntype WorkspaceReader = {\n readBytes?: (session: string, path: string, options: object, signal: AbortSignal) => Promise<ReadResult>;\n readAll?: (session: string, path: string, signal: AbortSignal) => Promise<ReadResult>;\n readRelated?: (session: string, base: string, path: string, signal: AbortSignal) => Promise<ReadResult>;\n};\nexport async function readAsset(reader: WorkspaceReader, session: string, path: string, signal: AbortSignal, base?: string): Promise<string> {\n const result = reader.readBytes ? await reader.readBytes(session, path, base ? { baseFile: base } : {}, signal)\n : base && reader.readRelated ? await reader.readRelated(session, base, path, signal)\n : reader.readAll ? await reader.readAll(session, path, signal) : (() => { throw new Error('Unsupported DSH file reader'); })();\n if (!result.ok) throw new Error(result.error.message);\n if (result.value.data instanceof Uint8Array) return new TextDecoder().decode(result.value.data);\n const base64 = result.value.data;\n if (typeof base64 !== 'string') throw new Error('Invalid graph asset response');\n return new TextDecoder().decode(Uint8Array.from(atob(base64), c => c.charCodeAt(0)));\n}\n\nexport function apply(ctx: Context): void {\n const id = 'dsh-code-review-graph';\n ctx.effect(() => ctx.sidebarRightTabs.register({ id, kind: 'code-review-graph', priority: 'builtin', title: () => 'Code graph / \u4EE3\u7801\u56FE\u8C31', guide: [{ id: 'graph', order: 30, title: () => 'Code graph / \u4EE3\u7801\u56FE\u8C31', description: () => 'Explore dependencies, changes and execution paths locally.' }] }));\n function Graph({ sessionId, useTabInfo }: { sessionId: string; useTabInfo: () => { tab: { signal: AbortSignal } } }) {\n const { tab } = useTabInfo();\n const [html, setHtml] = useState('');\n const [error, setError] = useState('');\n const [notice, setNotice] = useState('');\n const [busy, setBusy] = useState(false);\n const operation = useRef<AbortController>();\n const [generation, refresh] = useState(0);\n useEffect(() => {\n setHtml(''); setNotice(''); setError(''); setBusy(false);\n return () => { operation.current?.abort(); operation.current = undefined; };\n }, [sessionId, tab.signal]);\n const execute = async (command: 'crg-setup' | 'crg-graph') => {\n if (operation.current) return;\n const current = new AbortController(); operation.current = current;\n const signal = AbortSignal.any([current.signal, tab.signal]);\n setBusy(true); setError('');\n setNotice(command === 'crg-setup' ? '\u6B63\u5728\u51C6\u5907\u5F15\u64CE\uFF1B\u9996\u6B21\u9700\u8981\u4E0B\u8F7D\u4F9D\u8D56\u2026 / Preparing engine; first use downloads dependencies\u2026' : '\u6B63\u5728\u51C6\u5907\u5F15\u64CE\u5E76\u751F\u6210\u56FE\u8C31\u2026 / Preparing engine and generating graph\u2026');\n try {\n const result = await ctx.remote.commands.execute(sessionId as Parameters<typeof ctx.remote.commands.execute>[0], `/${command}`, [], signal);\n signal.throwIfAborted();\n if (!result.ok) throw new Error(result.error.message);\n if (!result.value) throw new Error('Graph commands are unavailable. Enable the Host component. / \u56FE\u8C31\u547D\u4EE4\u4E0D\u53EF\u7528\uFF0C\u8BF7\u542F\u7528\u63D2\u4EF6\u7EC4\u4EF6\u3002');\n if (result.value.result.kind === 'error') throw new Error(result.value.result.text);\n setNotice(result.value.result.text ?? '\u5B8C\u6210 / Done');\n if (command === 'crg-graph') refresh(n => n + 1);\n } catch (error) {\n if (operation.current !== current) return;\n if (current.signal.aborted) setNotice('\u5DF2\u53D6\u6D88\uFF0C\u53EF\u91CD\u65B0\u5C1D\u8BD5\u3002 / Cancelled. You can retry.');\n else if (!tab.signal.aborted) { setNotice(''); setError(String(error)); }\n } finally {\n if (operation.current === current) { operation.current = undefined; setBusy(false); }\n }\n };\n const download = () => {\n const url = URL.createObjectURL(new Blob([html], { type: 'text/html;charset=utf-8' }));\n const link = document.createElement('a'); link.href = url; link.download = 'code-review-graph.html'; link.click();\n setTimeout(() => URL.revokeObjectURL(url), 1000);\n };\n useEffect(() => {\n const life = new AbortController();\n const signal = AbortSignal.any([life.signal, tab.signal]);\n const base = '.code-review-graph/graph.html';\n const reader = ctx.remote.workspaceFiles as unknown as WorkspaceReader;\n setError('');\n void readAsset(reader, sessionId, base, signal).then(html => inlineGraph(html, path => readAsset(reader, sessionId, path, signal, base))).then(result => {\n if (!signal.aborted) setHtml(result);\n }).catch(error => { if (!signal.aborted) setError(String(error)); });\n return () => life.abort();\n }, [sessionId, generation, tab.signal]);\n return <section style={{ height: '100%', display: 'flex', flexDirection: 'column', gap: 8 }}>\n <div>\n <button type=\"button\" disabled={busy} onClick={() => void execute('crg-graph')}>{html ? '\u66F4\u65B0\u56FE\u8C31 / Update graph' : '\u51C6\u5907\u5F15\u64CE\u5E76\u751F\u6210\u56FE\u8C31 / Set up and generate graph'}</button>{' '}\n <button type=\"button\" disabled={busy} onClick={() => void execute('crg-setup')}>\u51C6\u5907\u5F15\u64CE / Set up engine</button>{' '}\n {busy && <button type=\"button\" onClick={() => operation.current?.abort()}>\u53D6\u6D88 / Cancel</button>}{' '}\n <button type=\"button\" disabled={busy} onClick={() => refresh(n => n + 1)}>Refresh / \u91CD\u65B0\u8BFB\u53D6</button>{' '}\n <button type=\"button\" disabled={!html} onClick={download}>Export HTML / \u5BFC\u51FA HTML</button>\n </div>\n {!html && !busy && <p>\u70B9\u51FB\u201C\u51C6\u5907\u5F15\u64CE\u5E76\u751F\u6210\u56FE\u8C31\u201D\u5373\u53EF\u5F00\u59CB\uFF0C\u65E0\u9700\u804A\u5929\u6216\u586B\u5199\u53C2\u6570\u3002\u9996\u6B21\u4F1A\u5728 DSH \u7684\u9694\u79BB\u76EE\u5F55\u4E0B\u8F7D\u5F15\u64CE\u4F9D\u8D56\uFF0C\u9700\u8981 uv \u6216 Python 3.10+\u3002 / Click Set up and generate graph. No chat or parameters needed. First use downloads isolated engine dependencies; uv or Python 3.10+ is required.</p>}\n {notice && <p role=\"status\" style={{ whiteSpace: 'pre-wrap' }}>{notice}</p>}\n {error && <details open><summary>\u65E0\u6CD5\u52A0\u8F7D\u6216\u751F\u6210\u56FE\u8C31 / Graph unavailable</summary><p role=\"alert\" style={{ whiteSpace: 'pre-wrap' }}>{error}</p></details>}\n {html && <iframe title=\"Interactive code graph / \u4EA4\u4E92\u4EE3\u7801\u56FE\u8C31\" sandbox=\"allow-scripts allow-downloads\" srcDoc={html} style={{ width: '100%', flex: 1, border: 0 }} />}\n </section>;\n }\n ctx.effect(() => ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register({ name: 'sidebar.right.pane.tab', key: id }, Graph)));\n}\n", "/** Inline only bundled scripts and local data; the iframe never fetches remote assets. */\nexport async function inlineGraph(html: string, read: (path: string) => Promise<string>): Promise<string> {\n // The upstream CDN fallback contains a literal script tag inside a JS string.\n // Local D3 is required here, so remove that loader before inspecting HTML tags.\n html = html.replace(/<script>\\s*window\\.d3\\s*\\|\\|\\s*document\\.write\\([\\s\\S]*?\\);\\s*<\\/script>/gi, '');\n const scripts = [...html.matchAll(/<script\\b([^>]*?)\\bsrc=[\"']([^\"']+)[\"']([^>]*)>[\\s\\S]*?<\\/script>/gi)];\n for (const match of scripts) {\n const path = match[2];\n if (/^(?:https?:|data:|\\/|\\\\)|\\.\\./i.test(path)) throw new Error('Graph contains a nonlocal script');\n const script = await read(path);\n html = html.replace(match[0], () => `<script>${script.replace(/<\\/script/gi, '<\\\\/script')}</script>`);\n }\n const policy = `<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; connect-src 'none'; font-src 'none'\">`;\n return html.replace(/<head>/i, `<head>${policy}`);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["error", "html", "React"]
|
|
7
7
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { Ajv } from "ajv";
|
|
4
|
-
import { readFile, readdir as readdir2, stat as stat2 } from "node:fs/promises";
|
|
5
|
-
import { join as
|
|
6
|
-
import { homedir } from "node:os";
|
|
4
|
+
import { readFile as readFile2, readdir as readdir2, stat as stat2 } from "node:fs/promises";
|
|
5
|
+
import { join as join3, basename as basename2 } from "node:path";
|
|
6
|
+
import { homedir as homedir2 } from "node:os";
|
|
7
7
|
|
|
8
8
|
// src/backend.ts
|
|
9
9
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
@@ -75,25 +75,25 @@ async function connect(engine, cwd) {
|
|
|
75
75
|
const transport = new StdioClientTransport({ command: engine.command, args: [...engine.args, "serve", "--repo", cwd], cwd, env: environment(engine), stderr: "pipe" });
|
|
76
76
|
transport.stderr?.on("data", () => {
|
|
77
77
|
});
|
|
78
|
-
const client = new Client({ name: "dsh-code-review-graph", version: "0.1.
|
|
78
|
+
const client = new Client({ name: "dsh-code-review-graph", version: "0.1.2" });
|
|
79
79
|
try {
|
|
80
80
|
await client.connect(transport);
|
|
81
81
|
return client;
|
|
82
82
|
} catch (error) {
|
|
83
83
|
await transport.close();
|
|
84
|
-
throw new Error(`CRG engine unavailable.
|
|
84
|
+
throw new Error(`CRG engine unavailable. Open Code graph and click Set up engine, or run /crg-setup (CLI: dsh-crg prepare). ${String(error)}`);
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
async function stop(child) {
|
|
88
88
|
if (!child || child.exitCode !== null || child.signalCode !== null) return;
|
|
89
|
-
await new Promise((
|
|
89
|
+
await new Promise((resolve3) => {
|
|
90
90
|
const timer = setTimeout(() => {
|
|
91
91
|
child.kill("SIGKILL");
|
|
92
|
-
|
|
92
|
+
resolve3();
|
|
93
93
|
}, 3e3);
|
|
94
94
|
child.once("exit", () => {
|
|
95
95
|
clearTimeout(timer);
|
|
96
|
-
|
|
96
|
+
resolve3();
|
|
97
97
|
});
|
|
98
98
|
child.kill();
|
|
99
99
|
});
|
|
@@ -161,8 +161,8 @@ var BackendPool = class {
|
|
|
161
161
|
signal.throwIfAborted();
|
|
162
162
|
const watcher = spawn(this.engine.command, [...this.engine.args, "watch", "--repo", root], { cwd: root, env: environment(this.engine), stdio: "ignore", windowsHide: true });
|
|
163
163
|
entry.watcher = watcher;
|
|
164
|
-
await new Promise((
|
|
165
|
-
watcher.once("spawn",
|
|
164
|
+
await new Promise((resolve3, reject) => {
|
|
165
|
+
watcher.once("spawn", resolve3);
|
|
166
166
|
watcher.once("error", reject);
|
|
167
167
|
});
|
|
168
168
|
watcher.on("error", (error) => {
|
|
@@ -212,7 +212,7 @@ var BackendPool = class {
|
|
|
212
212
|
async function run(engine, args, cwd, signal) {
|
|
213
213
|
await checkedData(cwd);
|
|
214
214
|
signal = AbortSignal.any([signal, AbortSignal.timeout(engine.timeoutMs)]);
|
|
215
|
-
return new Promise((
|
|
215
|
+
return new Promise((resolve3, reject) => {
|
|
216
216
|
let log = "";
|
|
217
217
|
const child = spawn(engine.command, [...engine.args, ...args], { cwd, env: environment(engine), stdio: ["ignore", "pipe", "pipe"], windowsHide: true, signal });
|
|
218
218
|
const capture = (bytes) => {
|
|
@@ -221,7 +221,7 @@ async function run(engine, args, cwd, signal) {
|
|
|
221
221
|
child.stdout?.on("data", capture);
|
|
222
222
|
child.stderr?.on("data", capture);
|
|
223
223
|
child.on("error", reject);
|
|
224
|
-
child.on("exit", (code) => code === 0 ?
|
|
224
|
+
child.on("exit", (code) => code === 0 ? resolve3({ log }) : reject(new Error(`Engine command exited ${code}: ${log}`)));
|
|
225
225
|
});
|
|
226
226
|
}
|
|
227
227
|
|
|
@@ -291,12 +291,99 @@ function exposed(workflow, names) {
|
|
|
291
291
|
return workflow === "all" ? names : names.filter((name2) => members[workflow].includes(name2));
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
// src/setup.ts
|
|
295
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
296
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
297
|
+
import { homedir } from "node:os";
|
|
298
|
+
import { join as join2, resolve as resolve2 } from "node:path";
|
|
299
|
+
import { fileURLToPath } from "node:url";
|
|
300
|
+
var environment2 = join2(process.env.DSH_HOME ?? join2(homedir(), ".dsh"), "code-review-graph", "engine");
|
|
301
|
+
var pythonIn = (dir) => join2(dir, process.platform === "win32" ? "Scripts/python.exe" : "bin/python");
|
|
302
|
+
async function command(exe, args, quiet = false, options = {}) {
|
|
303
|
+
options.signal?.throwIfAborted();
|
|
304
|
+
await new Promise((resolve3, reject) => {
|
|
305
|
+
let log = "";
|
|
306
|
+
const child = spawn2(exe, args, { stdio: ["ignore", "pipe", "pipe"], windowsHide: true, signal: options.signal });
|
|
307
|
+
const capture = (bytes) => {
|
|
308
|
+
const text = bytes.toString();
|
|
309
|
+
log = (log + text).slice(-16e3);
|
|
310
|
+
if (!quiet) options.onProgress?.(text);
|
|
311
|
+
};
|
|
312
|
+
child.stdout.on("data", capture);
|
|
313
|
+
child.stderr.on("data", capture);
|
|
314
|
+
child.on("error", reject);
|
|
315
|
+
child.on("close", (code) => code === 0 ? resolve3() : reject(new Error(`${exe} exited ${code}: ${log}`)));
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
async function prepare(source, python, options = {}) {
|
|
319
|
+
options = { ...options, signal: AbortSignal.any([...options.signal ? [options.signal] : [], AbortSignal.timeout(15 * 60 * 1e3)]) };
|
|
320
|
+
options.signal.throwIfAborted();
|
|
321
|
+
const execute = (exe, args, quiet = false) => command(exe, args, quiet, options);
|
|
322
|
+
const engine = JSON.parse(await readFile(new URL("../engine.json", import.meta.url), "utf8"));
|
|
323
|
+
const pinned = source ?? engine.source;
|
|
324
|
+
const stamp = join2(environment2, "dsh-crg-source.json");
|
|
325
|
+
if (await readFile(stamp, "utf8").then((value) => JSON.parse(value).source === pinned, () => false)) {
|
|
326
|
+
try {
|
|
327
|
+
await execute(pythonIn(environment2), ["-c", "from code_review_graph.refactor import get_refactor_edit_plan"], true);
|
|
328
|
+
options.onProgress?.("CRG engine is already prepared.");
|
|
329
|
+
return;
|
|
330
|
+
} catch {
|
|
331
|
+
options.signal.throwIfAborted();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
await mkdir(environment2, { recursive: true });
|
|
335
|
+
let uv = false;
|
|
336
|
+
try {
|
|
337
|
+
await execute("uv", ["--version"], true);
|
|
338
|
+
uv = true;
|
|
339
|
+
} catch {
|
|
340
|
+
options.signal.throwIfAborted();
|
|
341
|
+
}
|
|
342
|
+
if (uv && !python) {
|
|
343
|
+
await execute("uv", ["venv", "--allow-existing", "--python", ">=3.10", environment2]);
|
|
344
|
+
await execute("uv", ["pip", "install", "--python", pythonIn(environment2), pinned]);
|
|
345
|
+
} else {
|
|
346
|
+
let selected;
|
|
347
|
+
for (const candidate of python ? [{ exe: python, args: [] }] : [{ exe: "python3", args: [] }, { exe: "python", args: [] }, { exe: "py", args: ["-3"] }]) {
|
|
348
|
+
try {
|
|
349
|
+
await execute(candidate.exe, [...candidate.args, "-c", "import sys; assert sys.version_info >= (3,10)"], true);
|
|
350
|
+
selected = candidate;
|
|
351
|
+
break;
|
|
352
|
+
} catch {
|
|
353
|
+
options.signal.throwIfAborted();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (!selected) throw new Error("Install uv or real Python 3.10+; Windows Store aliases are not a Python runtime.");
|
|
357
|
+
await execute(selected.exe, [...selected.args, "-m", "venv", environment2]);
|
|
358
|
+
await execute(pythonIn(environment2), ["-m", "pip", "install", pinned]);
|
|
359
|
+
}
|
|
360
|
+
await execute(pythonIn(environment2), ["-c", "from code_review_graph.refactor import get_refactor_edit_plan"], true);
|
|
361
|
+
options.signal.throwIfAborted();
|
|
362
|
+
await writeFile(stamp, JSON.stringify({ source: pinned }), "utf8");
|
|
363
|
+
}
|
|
364
|
+
if (process.argv[1] && resolve2(fileURLToPath(import.meta.url)) === resolve2(process.argv[1])) {
|
|
365
|
+
if (process.argv[2] !== "prepare") {
|
|
366
|
+
console.error("Usage: dsh-crg prepare [engine-source] [--python executable]");
|
|
367
|
+
process.exitCode = 2;
|
|
368
|
+
} else {
|
|
369
|
+
const args = process.argv.slice(3);
|
|
370
|
+
const flag = args.indexOf("--python");
|
|
371
|
+
const python = flag >= 0 ? args.splice(flag, 2)[1] : void 0;
|
|
372
|
+
try {
|
|
373
|
+
await prepare(args[0], python, { onProgress: (text) => process.stdout.write(text.endsWith("\n") ? text : text + "\n") });
|
|
374
|
+
} catch (error) {
|
|
375
|
+
console.error(String(error));
|
|
376
|
+
process.exitCode = 1;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
294
381
|
// src/index.ts
|
|
295
382
|
var name = "code-review-graph";
|
|
296
383
|
var inject = ["tools", "agents", "fs", "sandboxPolicy", "skills", "systemPrompt"];
|
|
297
|
-
var engineDir =
|
|
384
|
+
var engineDir = join3(process.env.DSH_HOME ?? join3(homedir2(), ".dsh"), "code-review-graph", "engine");
|
|
298
385
|
var Config = z.object({
|
|
299
|
-
engineCommand: z.string().default(
|
|
386
|
+
engineCommand: z.string().default(join3(engineDir, process.platform === "win32" ? "Scripts/code-review-graph.exe" : "bin/code-review-graph")),
|
|
300
387
|
engineArgs: z.array(z.string()).default([]),
|
|
301
388
|
timeoutMs: z.number().min(1e3).default(12e4),
|
|
302
389
|
authorizedRepos: z.array(z.string()).default([]),
|
|
@@ -310,7 +397,7 @@ var output = {
|
|
|
310
397
|
async function apply(ctx, config) {
|
|
311
398
|
const engine = { command: config.engineCommand, args: config.engineArgs, timeoutMs: config.timeoutMs, env: Object.fromEntries(config.embeddingEnv.filter((key) => /^(OPENAI|GOOGLE|GEMINI|MINIMAX|VOYAGE|CRG)_/.test(key) && process.env[key] !== void 0).map((key) => [key, process.env[key]])) };
|
|
312
399
|
const pool = new BackendPool(engine);
|
|
313
|
-
const snapshot = JSON.parse(await
|
|
400
|
+
const snapshot = JSON.parse(await readFile2(new URL("../capabilities.json", import.meta.url), "utf8"));
|
|
314
401
|
const tools = snapshot.tools.filter((t) => t.name !== "get_refactor_edit_plan_tool");
|
|
315
402
|
const names = tools.map((t) => t.name);
|
|
316
403
|
const masks = /* @__PURE__ */ new Map();
|
|
@@ -358,7 +445,7 @@ async function apply(ctx, config) {
|
|
|
358
445
|
if (selected.some((n) => !aliases.some((r) => r.alias === n))) throw new Error("Repository is not authorized");
|
|
359
446
|
const results = [];
|
|
360
447
|
for (const { path: repo, alias } of aliases.filter((r) => !selected.length || selected.includes(r.alias))) {
|
|
361
|
-
if (repo !== root && !await stat2(
|
|
448
|
+
if (repo !== root && !await stat2(join3(repo, ".code-review-graph/graph.db")).then(() => true, () => false)) throw new Error(`Build ${repo} in its own session first`);
|
|
362
449
|
await pool.ensure(repo, owner, signal);
|
|
363
450
|
const data = valueOf(await pool.call(repo, owner, "semantic_search_nodes_tool", { query: args.query, kind: args.kind, limit: args.limit ?? 20, provider: "local", repo_root: repo }, signal));
|
|
364
451
|
for (const result2 of data.results) results.push({ ...result2, repo: alias });
|
|
@@ -430,6 +517,46 @@ async function apply(ctx, config) {
|
|
|
430
517
|
max_nodes: { type: "integer", minimum: 1 }
|
|
431
518
|
}, additionalProperties: false };
|
|
432
519
|
const validateVisualization = ajv.compile(visualizationSchema);
|
|
520
|
+
async function visualize(args, agent, signal) {
|
|
521
|
+
const root = await repository(agent.session.header.cwd);
|
|
522
|
+
signal = AbortSignal.any([signal, life.signal]);
|
|
523
|
+
const graphUpdate = await pool.ensure(root, agent.id, signal);
|
|
524
|
+
for (const path of args.seed_file ?? []) await checkedPath(root, path, true);
|
|
525
|
+
const command2 = ["visualize", "--repo", root];
|
|
526
|
+
for (const [key, value] of Object.entries(args)) {
|
|
527
|
+
const flag = `--${key.replaceAll("_", "-")}`;
|
|
528
|
+
if (Array.isArray(value)) for (const item of value) command2.push(flag, String(item));
|
|
529
|
+
else if (value === true) command2.push(flag);
|
|
530
|
+
else if (value !== void 0 && value !== false) command2.push(flag, String(value));
|
|
531
|
+
}
|
|
532
|
+
const { log } = await run(engine, command2, root, signal);
|
|
533
|
+
const format = String(args.format ?? "html");
|
|
534
|
+
return { status: "ok", path: join3(root, ".code-review-graph", format === "obsidian" ? "obsidian" : `graph.${format}`), ...format === "html" ? { sidebar: "code-review-graph" } : {}, ...graphUpdate ? { graph_update: graphUpdate } : {}, log };
|
|
535
|
+
}
|
|
536
|
+
let preparation;
|
|
537
|
+
const prepareDefault = (signal) => {
|
|
538
|
+
if (config.engineCommand !== join3(engineDir, process.platform === "win32" ? "Scripts/code-review-graph.exe" : "bin/code-review-graph") || config.engineArgs.length) {
|
|
539
|
+
throw new Error("A custom engine is configured. Prepare that engine, or restore the default engine configuration. / \u5F53\u524D\u914D\u7F6E\u4E86\u81EA\u5B9A\u4E49\u5F15\u64CE\uFF0C\u8BF7\u51C6\u5907\u8BE5\u5F15\u64CE\u6216\u6062\u590D\u9ED8\u8BA4\u914D\u7F6E\u3002");
|
|
540
|
+
}
|
|
541
|
+
return preparation ??= prepare(void 0, void 0, { signal: AbortSignal.any([signal, life.signal]) }).finally(() => {
|
|
542
|
+
preparation = void 0;
|
|
543
|
+
});
|
|
544
|
+
};
|
|
545
|
+
ctx.inject(["commands"], (scope) => {
|
|
546
|
+
const commands = scope.get("commands");
|
|
547
|
+
scope.effect(() => commands.register({ name: "crg-setup", description: "\u51C6\u5907\u672C\u5730\u56FE\u8C31\u5F15\u64CE / Prepare the local graph engine", handler: async ({ signal, rawInput }) => {
|
|
548
|
+
if (rawInput.trim()) throw new Error("/crg-setup does not need parameters / \u65E0\u9700\u586B\u5199\u53C2\u6570");
|
|
549
|
+
await prepareDefault(signal);
|
|
550
|
+
return { kind: "success", text: "\u56FE\u8C31\u5F15\u64CE\u5DF2\u51C6\u5907\u597D\u3002\u6253\u5F00\u53F3\u4FA7\u4EE3\u7801\u56FE\u8C31\uFF0C\u70B9\u51FB\u751F\u6210\u56FE\u8C31\u3002 / Engine ready. Open Code graph and click Generate graph." };
|
|
551
|
+
} }));
|
|
552
|
+
scope.effect(() => commands.register({ name: "crg-graph", description: "\u751F\u6210\uFF0F\u66F4\u65B0\u4EA4\u4E92\u4EE3\u7801\u56FE\u8C31\uFF0C\u65E0\u9700\u6A21\u578B / Generate or update the interactive code graph without a model", handler: async ({ agent, signal, rawInput }) => {
|
|
553
|
+
if (rawInput.trim()) throw new Error("/crg-graph does not need parameters / \u65E0\u9700\u586B\u5199\u53C2\u6570");
|
|
554
|
+
await repository(agent.session.header.cwd);
|
|
555
|
+
if (config.engineCommand === join3(engineDir, process.platform === "win32" ? "Scripts/code-review-graph.exe" : "bin/code-review-graph") && !config.engineArgs.length) await prepareDefault(signal);
|
|
556
|
+
const result = await visualize({ mode: "full", format: "html" }, agent, signal);
|
|
557
|
+
return { kind: "success", text: `\u56FE\u8C31\u5DF2\u751F\u6210 / Graph generated: ${result.path}${result.graph_update ? "\n\u89E3\u6790\u8B66\u544A / Parse warnings: " + JSON.stringify(result.graph_update) : ""}` };
|
|
558
|
+
} }));
|
|
559
|
+
});
|
|
433
560
|
ctx.effect(() => ctx.tools.register({
|
|
434
561
|
name: "crg_visualize",
|
|
435
562
|
description: "Generate the local interactive graph for the DSH Code graph sidebar, or export HTML, JSON, GraphML, Cypher, Obsidian or SVG. Supports file, community, symbol, changed-code, flow and shortest-path views.",
|
|
@@ -438,21 +565,8 @@ async function apply(ctx, config) {
|
|
|
438
565
|
timeoutMs: config.timeoutMs,
|
|
439
566
|
execute: async (value, exec) => {
|
|
440
567
|
if (!validateVisualization(value)) throw new Error(ajv.errorsText(validateVisualization.errors));
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
const signal = AbortSignal.any([exec.signal, life.signal]);
|
|
444
|
-
await pool.ensure(root, exec.agent.id, signal);
|
|
445
|
-
for (const path of args.seed_file ?? []) await checkedPath(root, path, true);
|
|
446
|
-
const command = ["visualize", "--repo", root];
|
|
447
|
-
for (const [key, value2] of Object.entries(args)) {
|
|
448
|
-
const flag = `--${key.replaceAll("_", "-")}`;
|
|
449
|
-
if (Array.isArray(value2)) for (const item of value2) command.push(flag, String(item));
|
|
450
|
-
else if (value2 === true) command.push(flag);
|
|
451
|
-
else if (value2 !== void 0 && value2 !== false) command.push(flag, String(value2));
|
|
452
|
-
}
|
|
453
|
-
const { log } = await run(engine, command, root, signal);
|
|
454
|
-
const format = String(args.format ?? "html");
|
|
455
|
-
return { status: "ok", path: join2(root, ".code-review-graph", format === "obsidian" ? "obsidian" : `graph.${format}`), ...format === "html" ? { sidebar: "code-review-graph" } : {}, log };
|
|
568
|
+
if (!exec.agent) throw new Error("Graph tools require a session-owned agent");
|
|
569
|
+
return visualize(value, exec.agent, exec.signal);
|
|
456
570
|
}
|
|
457
571
|
}));
|
|
458
572
|
const promptNames = ["review_changes", "architecture_map", "debug_issue", "onboard_developer", "pre_merge_check"];
|
|
@@ -488,12 +602,12 @@ async function apply(ctx, config) {
|
|
|
488
602
|
name: "code-review-graph",
|
|
489
603
|
order: 80,
|
|
490
604
|
interpolate: false,
|
|
491
|
-
text: "Use crg_workflow to reveal task-specific local code graph tools, then get_minimal_context_tool before broader queries. Prefer minimal detail. crg_visualize
|
|
605
|
+
text: "Use crg_workflow to reveal task-specific local code graph tools, then get_minimal_context_tool before broader queries. Prefer minimal detail. crg_visualize generates a local graph for the Code graph sidebar; users can also generate it directly from that page without a model. Refactors use session filesystem permissions."
|
|
492
606
|
}));
|
|
493
607
|
const skillsDir = new URL("../skills/", import.meta.url);
|
|
494
608
|
for (const slug of await readdir2(skillsDir)) {
|
|
495
609
|
const path = new URL(`${slug}/SKILL.md`, skillsDir);
|
|
496
|
-
const body = await
|
|
610
|
+
const body = await readFile2(path, "utf8");
|
|
497
611
|
const content = body.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "").replace(/\b([a-z_]+_tool)\b/g, (raw) => names.includes(raw) ? publicName(raw) : raw);
|
|
498
612
|
if (!skillWorkflow[slug]) throw new Error(`Unmapped CRG skill: ${slug}`);
|
|
499
613
|
ctx.effect(() => ctx.skills.register({ name: `crg-${slug}`, description: `Code review graph: ${slug}`, source: "bundled", content: `First call crg_workflow with workflow "${skillWorkflow[slug]}". Tool names use mcp__code_review_graph__ prefixes.
|